diff --git a/pkg/config/load.go b/pkg/config/load.go index 38634fc5..2aaf1457 100644 --- a/pkg/config/load.go +++ b/pkg/config/load.go @@ -394,6 +394,10 @@ func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, } } + if err := validateNoDuplicateNames(result.Proxies, result.Visitors); err != nil { + return nil, err + } + return result, nil } @@ -417,6 +421,31 @@ func LoadClientConfig(path string, strict bool) ( return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil } +// validateNoDuplicateNames rejects proxies or visitors that share a name. They are +// keyed by name in the config sources, so a duplicate would otherwise be silently +// overwritten and never started, with no error or log. +func validateNoDuplicateNames(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error { + proxyNames := make(map[string]struct{}, len(proxies)) + for _, p := range proxies { + name := p.GetBaseConfig().Name + if _, ok := proxyNames[name]; ok { + return fmt.Errorf("proxy name [%s] is duplicated", name) + } + proxyNames[name] = struct{}{} + } + + visitorNames := make(map[string]struct{}, len(visitors)) + for _, v := range visitors { + name := v.GetBaseConfig().Name + if _, ok := visitorNames[name]; ok { + return fmt.Errorf("visitor name [%s] is duplicated", name) + } + visitorNames[name] = struct{}{} + } + + return nil +} + func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer { proxyCfgs := proxies for _, c := range proxyCfgs { diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go index b711a5c1..a1e29247 100644 --- a/pkg/config/load_test.go +++ b/pkg/config/load_test.go @@ -17,6 +17,8 @@ package config import ( "encoding/json" "fmt" + "os" + "path/filepath" "strings" "testing" @@ -462,6 +464,111 @@ func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) { require.Equal("keep", proxies[0].GetBaseConfig().Name) } +func TestLoadClientConfigResult_DuplicateNames(t *testing.T) { + tests := []struct { + name string + content string + errSubstr string + }{ + { + name: "duplicate proxy names", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "dup" +type = "tcp" +localPort = 22 +remotePort = 6000 + +[[proxies]] +name = "dup" +type = "tcp" +localPort = 3306 +remotePort = 6001 +`, + errSubstr: "proxy name [dup] is duplicated", + }, + { + name: "duplicate visitor names", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[visitors]] +name = "dup" +type = "stcp" +serverName = "a" +secretKey = "secret" +bindPort = 9001 + +[[visitors]] +name = "dup" +type = "stcp" +serverName = "b" +secretKey = "secret" +bindPort = 9002 +`, + errSubstr: "visitor name [dup] is duplicated", + }, + { + name: "unique names", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "p1" +type = "tcp" +localPort = 22 +remotePort = 6000 + +[[proxies]] +name = "p2" +type = "tcp" +localPort = 3306 +remotePort = 6001 +`, + }, + { + name: "same name across proxy and visitor", + content: ` +serverAddr = "127.0.0.1" +serverPort = 7000 + +[[proxies]] +name = "same" +type = "tcp" +localPort = 22 +remotePort = 6000 + +[[visitors]] +name = "same" +type = "stcp" +serverName = "a" +secretKey = "secret" +bindPort = 9001 +`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + path := filepath.Join(t.TempDir(), "frpc.toml") + require.NoError(os.WriteFile(path, []byte(tc.content), 0o600)) + + _, err := LoadClientConfigResult(path, false) + if tc.errSubstr == "" { + require.NoError(err) + } else { + require.ErrorContains(err, tc.errSubstr) + } + }) + } +} + // TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types func TestYAMLEdgeCases(t *testing.T) { require := require.New(t)