diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go index abe80635..270a8a61 100644 --- a/cmd/frpc/sub/admin.go +++ b/cmd/frpc/sub/admin.go @@ -54,7 +54,11 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er Use: name, Short: short, Run: func(cmd *cobra.Command, args []string) { - cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) + if len(cfgFiles) == 0 || cfgFiles[0] == "" { + fmt.Println("frpc: the configuration file is not specified") + os.Exit(1) + } + cfg, _, _, _, err := config.LoadClientConfig(cfgFiles[0], strictConfigMode) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go index a07d6852..bcfdb02b 100644 --- a/cmd/frpc/sub/nathole.go +++ b/cmd/frpc/sub/nathole.go @@ -48,8 +48,17 @@ var natholeDiscoveryCmd = &cobra.Command{ Short: "Discover nathole information from stun server", RunE: func(cmd *cobra.Command, args []string) error { // ignore error here, because we can use command line pameters - cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) - if err != nil { + var cfg *v1.ClientCommonConfig + if len(cfgFiles) > 0 && cfgFiles[0] != "" { + _, _, _, _, err := config.LoadClientConfig(cfgFiles[0], strictConfigMode) + if err != nil { + cfg = &v1.ClientCommonConfig{} + if err := cfg.Complete(); err != nil { + fmt.Printf("failed to complete config: %v\n", err) + os.Exit(1) + } + } + } else { cfg = &v1.ClientCommonConfig{} if err := cfg.Complete(); err != nil { fmt.Printf("failed to complete config: %v\n", err) diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 8ff9c438..9e3e8de7 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -43,16 +43,18 @@ import ( ) var ( - cfgFile string + cfgFiles []string cfgDir string showVersion bool strictConfigMode bool allowUnsafe []string authTokens []string + + bannerDisplayed bool ) func init() { - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") + 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") @@ -83,14 +85,19 @@ var rootCmd = &cobra.Command{ } // If cfgDir is not empty, run multiple frpc service for each config file in cfgDir. - // Note that it's only designed for testing. It's not guaranteed to be stable. 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(cfgFile, unsafeFeatures) + err := runClient(cfgFiles[0], unsafeFeatures) if err != nil { fmt.Println(err) os.Exit(1) @@ -120,6 +127,30 @@ func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) return err } +func runMultipleClientsFromFiles(cfgFiles []string, unsafeFeatures *security.UnsafeFeatures) error { + var wg sync.WaitGroup + + // Display banner first + banner.DisplayBanner() + bannerDisplayed = true + log.Infof("检测到 %d 个配置文件,将启动多个 frpc 服务实例", len(cfgFiles)) + + for i, cfgFile := range cfgFiles { + wg.Add(1) + // Add a small delay to avoid log output mixing + time.Sleep(100 * time.Millisecond) + go func(index int, path string) { + defer wg.Done() + err := runClient(path, unsafeFeatures) + if err != nil { + fmt.Printf("\n配置文件 [%s] 启动失败: %v\n", path, err) + } + }(i, cfgFile) + } + wg.Wait() + return nil +} + func Execute() { rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc) if err := rootCmd.Execute(); err != nil { @@ -172,10 +203,11 @@ func startService( ) error { log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor) - // Display banner before starting the service - banner.DisplayBanner() - - log.Infof("Nya! %s 启动中", version.Full()) + // Display banner only once before starting the first service + if !bannerDisplayed { + banner.DisplayBanner() + bannerDisplayed = true + } // Display node information if available if nodeName != "" { @@ -242,9 +274,9 @@ func runClientWithTokens(tokens []string, unsafeFeatures *security.UnsafeFeature tokenToIDs[ti.Token] = append(tokenToIDs[ti.Token], ti.ID) } - // Verify all tokens use the same token value + // If we have multiple different tokens, start one service for each token group if len(tokenToIDs) > 1 { - return fmt.Errorf("different tokens are not supported, all ids must use the same token") + return runMultipleClientsWithTokens(tokenToIDs, unsafeFeatures) } // Get the single token and all its IDs @@ -298,6 +330,45 @@ func runClientWithTokenAndIDs(token string, ids []string, unsafeFeatures *securi 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()) diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index 830f7bf1..9f8ddccf 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -33,11 +33,12 @@ var verifyCmd = &cobra.Command{ Use: "verify", Short: "Verify that the configures is valid", RunE: func(cmd *cobra.Command, args []string) error { - if cfgFile == "" { + if len(cfgFiles) == 0 || cfgFiles[0] == "" { fmt.Println("frpc: the configuration file is not specified") return nil } + cfgFile := cfgFiles[0] cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode) if err != nil { fmt.Println(err) diff --git a/pkg/util/banner/banner.go b/pkg/util/banner/banner.go index e8cc3cff..cdbe9dc2 100644 --- a/pkg/util/banner/banner.go +++ b/pkg/util/banner/banner.go @@ -1,6 +1,11 @@ package banner -import "fmt" +import ( + "fmt" + + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/pkg/util/version" +) func DisplayBanner() { fmt.Println(" __ ___ __________ ____ ________ ____") @@ -9,4 +14,5 @@ func DisplayBanner() { fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ") fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ") fmt.Println(" ") + log.Infof("Nya! %s 启动中", version.Full()) }