Files
lolia-frp/cmd/frpc/sub/root.go

422 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2018 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sub
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/policy/featuregate"
"github.com/fatedier/frp/pkg/policy/security"
"github.com/fatedier/frp/pkg/util/banner"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/pkg/util/version"
)
var (
cfgFiles []string
cfgDir string
showVersion bool
strictConfigMode bool
allowUnsafe []string
authTokens []string
bannerDisplayed bool
)
func init() {
rootCmd.PersistentFlags().StringSliceVarP(&cfgFiles, "config", "c", []string{"./frpc.ini"}, "config files of frpc (support multiple files)")
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
rootCmd.PersistentFlags().StringSliceVarP(&authTokens, "token", "t", []string{}, "authentication tokens in format 'id:token' (LoliaFRP only)")
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
}
var rootCmd = &cobra.Command{
Use: "frpc",
Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
RunE: func(cmd *cobra.Command, args []string) error {
if showVersion {
fmt.Println(version.Full())
return nil
}
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
// If authTokens is provided, fetch config from API
if len(authTokens) > 0 {
err := runClientWithTokens(authTokens, unsafeFeatures)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
}
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
if cfgDir != "" {
_ = runMultipleClients(cfgDir, unsafeFeatures)
return nil
}
// If multiple config files are specified, run one frpc service for each file
if len(cfgFiles) > 1 {
runMultipleClientsFromFiles(cfgFiles, unsafeFeatures)
return nil
}
// Do not show command usage here.
err := runClient(cfgFiles[0], unsafeFeatures)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return nil
},
}
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
var wg sync.WaitGroup
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
wg.Add(1)
time.Sleep(time.Millisecond)
go func() {
defer wg.Done()
err := runClient(path, unsafeFeatures)
if err != nil {
fmt.Printf("frpc service error for config file [%s]\n", path)
}
}()
return nil
})
wg.Wait()
return err
}
func runMultipleClientsFromFiles(cfgFiles []string, unsafeFeatures *security.UnsafeFeatures) {
var wg sync.WaitGroup
// Display banner first
banner.DisplayBanner()
bannerDisplayed = true
log.Infof("检测到 %d 个配置文件,将启动多个 frpc 服务实例", len(cfgFiles))
for _, cfgFile := range cfgFiles {
wg.Add(1)
// Add a small delay to avoid log output mixing
time.Sleep(100 * time.Millisecond)
go func(path string) {
defer wg.Done()
err := runClient(path, unsafeFeatures)
if err != nil {
fmt.Printf("\n配置文件 [%s] 启动失败: %v\n", path, err)
}
}(cfgFile)
}
wg.Wait()
}
func Execute() {
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func handleTermSignal(svr *client.Service) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
svr.GracefulClose(500 * time.Millisecond)
}
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
if err != nil {
return err
}
if isLegacyFormat {
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
"please use yaml/json/toml format instead!\n")
}
if len(cfg.FeatureGates) > 0 {
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
return err
}
}
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
return err
}
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath, "", "")
}
func startService(
cfg *v1.ClientCommonConfig,
proxyCfgs []v1.ProxyConfigurer,
visitorCfgs []v1.VisitorConfigurer,
unsafeFeatures *security.UnsafeFeatures,
cfgFile string,
nodeName string,
tunnelRemark string,
) error {
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
// Display banner only once before starting the first service
if !bannerDisplayed {
banner.DisplayBanner()
bannerDisplayed = true
}
// Display node information if available
if nodeName != "" {
log.Info("已获取到配置文件", "隧道名称", tunnelRemark, "使用节点", nodeName)
}
if cfgFile != "" {
log.Infof("启动 frpc 服务 [%s]", cfgFile)
defer log.Infof("frpc 服务 [%s] 已停止", cfgFile)
}
svr, err := client.NewService(client.ServiceOptions{
Common: cfg,
ProxyCfgs: proxyCfgs,
VisitorCfgs: visitorCfgs,
UnsafeFeatures: unsafeFeatures,
ConfigFilePath: cfgFile,
})
if err != nil {
return err
}
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
if shouldGracefulClose {
go handleTermSignal(svr)
}
return svr.Run(context.Background())
}
// APIResponse represents the response from LoliaFRP API
type APIResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Config string `json:"config"`
NodeName string `json:"node_name"`
TunnelRemark string `json:"tunnel_remark"`
} `json:"data"`
}
// TokenInfo stores parsed id and token from the -t parameter
type TokenInfo struct {
ID string
Token string
}
func runClientWithTokens(tokens []string, unsafeFeatures *security.UnsafeFeatures) error {
// Parse all tokens (format: id:token)
tokenInfos := make([]TokenInfo, 0, len(tokens))
for _, t := range tokens {
parts := strings.SplitN(t, ":", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid token format '%s', expected 'id:token'", t)
}
tokenInfos = append(tokenInfos, TokenInfo{
ID: strings.TrimSpace(parts[0]),
Token: strings.TrimSpace(parts[1]),
})
}
// Group tokens by token value (same token can have multiple IDs)
tokenToIDs := make(map[string][]string)
for _, ti := range tokenInfos {
tokenToIDs[ti.Token] = append(tokenToIDs[ti.Token], ti.ID)
}
// If we have multiple different tokens, start one service for each token group
if len(tokenToIDs) > 1 {
return runMultipleClientsWithTokens(tokenToIDs, unsafeFeatures)
}
// Get the single token and all its IDs
var token string
var ids []string
for t, idList := range tokenToIDs {
token = t
ids = idList
break
}
return runClientWithTokenAndIDs(token, ids, unsafeFeatures)
}
func runClientWithTokenAndIDs(token string, ids []string, unsafeFeatures *security.UnsafeFeatures) error {
// Get API server address from environment variable
apiServer := os.Getenv("LOLIA_API")
if apiServer == "" {
apiServer = "https://api.lolia.link"
}
// Build URL with query parameters
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config?token=%s&id=%s", apiServer, token, strings.Join(ids, ","))
// #nosec G107 -- URL is constructed from trusted source (environment variable or hardcoded)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch config from API: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API returned status code: %d", resp.StatusCode)
}
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return fmt.Errorf("failed to decode API response: %v", err)
}
if apiResp.Code != 200 {
return fmt.Errorf("API error: %s", apiResp.Msg)
}
// Decode base64 config
configBytes, err := base64.StdEncoding.DecodeString(apiResp.Data.Config)
if err != nil {
return fmt.Errorf("failed to decode base64 config: %v", err)
}
// Load config directly from bytes
return runClientWithConfig(configBytes, unsafeFeatures, apiResp.Data.NodeName, apiResp.Data.TunnelRemark)
}
func runMultipleClientsWithTokens(tokenToIDs map[string][]string, unsafeFeatures *security.UnsafeFeatures) error {
var wg sync.WaitGroup
// Display banner first
banner.DisplayBanner()
bannerDisplayed = true
log.Infof("检测到 %d 个不同的 token将并行启动多个 frpc 服务实例", len(tokenToIDs))
index := 0
for token, ids := range tokenToIDs {
wg.Add(1)
currentIndex := index
currentToken := token
currentIDs := ids
totalCount := len(tokenToIDs)
// Add a small delay to avoid log output mixing
time.Sleep(100 * time.Millisecond)
go func() {
defer wg.Done()
maskedToken := currentToken
if len(maskedToken) > 6 {
maskedToken = maskedToken[:3] + "***" + maskedToken[len(maskedToken)-3:]
} else {
maskedToken = "***"
}
log.Infof("[%d/%d] 启动 token: %s (IDs: %v)", currentIndex+1, totalCount, maskedToken, currentIDs)
err := runClientWithTokenAndIDs(currentToken, currentIDs, unsafeFeatures)
if err != nil {
fmt.Printf("\nToken [%s] 启动失败: %v\n", maskedToken, err)
}
}()
index++
}
wg.Wait()
return nil
}
func runClientWithConfig(configBytes []byte, unsafeFeatures *security.UnsafeFeatures, nodeName, tunnelRemark string) error {
// Render template first
renderedBytes, err := config.RenderWithTemplate(configBytes, config.GetValues())
if err != nil {
return fmt.Errorf("failed to render template: %v", err)
}
var allCfg v1.ClientConfig
if err := config.LoadConfigure(renderedBytes, &allCfg, strictConfigMode); err != nil {
return fmt.Errorf("failed to parse config: %v", err)
}
cfg := &allCfg.ClientCommonConfig
proxyCfgs := make([]v1.ProxyConfigurer, 0, len(allCfg.Proxies))
for _, c := range allCfg.Proxies {
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
}
visitorCfgs := make([]v1.VisitorConfigurer, 0, len(allCfg.Visitors))
for _, c := range allCfg.Visitors {
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
}
// Call Complete to fill in default values
if err := cfg.Complete(); err != nil {
return fmt.Errorf("failed to complete config: %v", err)
}
// Call Complete for all proxies to add name prefix (e.g., user.tunnel_name)
for _, c := range proxyCfgs {
c.Complete(cfg.User)
}
for _, c := range visitorCfgs {
c.Complete(cfg)
}
if len(cfg.FeatureGates) > 0 {
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
return err
}
}
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
return err
}
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, "", nodeName, tunnelRemark)
}