diff --git a/config/config.go b/config/config.go index 67bc90bb..50f7dd6c 100644 --- a/config/config.go +++ b/config/config.go @@ -955,6 +955,12 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ ) proxies["GLOBAL"] = adapter.NewProxy(global) } + + // validate dialer-proxy references + if err := validateDialerProxies(proxies); err != nil { + return nil, nil, err + } + return proxies, providersMap, nil } diff --git a/config/utils.go b/config/utils.go index 3e7f3252..7b413847 100644 --- a/config/utils.go +++ b/config/utils.go @@ -9,6 +9,7 @@ import ( "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" + C "github.com/metacubex/mihomo/constant" ) // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. @@ -143,6 +144,64 @@ func proxyGroupsDagSort(groupsConfig []map[string]any) error { return fmt.Errorf("loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements) } +// validateDialerProxies checks if all dialer-proxy references are valid +func validateDialerProxies(proxies map[string]C.Proxy) error { + graph := make(map[string]string) // proxy name -> dialer-proxy name + + // collect all proxies with dialer-proxy configured + for name, proxy := range proxies { + dialerProxy := proxy.ProxyInfo().DialerProxy + if dialerProxy != "" { + // validate each dialer-proxy reference + _, exist := proxies[dialerProxy] + if !exist { + return fmt.Errorf("proxy [%s] dialer-proxy [%s] not found", name, dialerProxy) + } + + // build dependency graph + graph[name] = dialerProxy + } + } + + // perform depth-first search to detect cycles for each proxy + for name := range graph { + visited := make(map[string]bool, len(graph)) + path := make([]string, 0, len(graph)) + if validateDialerProxiesHasCycle(name, graph, visited, path) { + return fmt.Errorf("proxy [%s] has circular dialer-proxy dependency", name) + } + } + + return nil +} + +// validateDialerProxiesHasCycle performs DFS to detect if there's a cycle starting from current proxy +func validateDialerProxiesHasCycle(current string, graph map[string]string, visited map[string]bool, path []string) bool { + // check if current is already in path (cycle detected) + for _, p := range path { + if p == current { + return true + } + } + + // already visited and no cycle + if visited[current] { + return false + } + + visited[current] = true + path = append(path, current) + + // check dialer-proxy of current proxy + if dialerProxy, exists := graph[current]; exists { + if validateDialerProxiesHasCycle(dialerProxy, graph, visited, path) { + return true + } + } + + return false +} + func verifyIP6() bool { if skip, _ := strconv.ParseBool(os.Getenv("SKIP_SYSTEM_IPV6_CHECK")); skip { return true diff --git a/config/utils_test.go b/config/utils_test.go new file mode 100644 index 00000000..542c2330 --- /dev/null +++ b/config/utils_test.go @@ -0,0 +1,79 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateDialerProxies(t *testing.T) { + testCases := []struct { + testName string + proxy []map[string]any + errContains string + }{ + { + testName: "ValidReference", + proxy: []map[string]any{ // create proxy with valid dialer-proxy reference + {"name": "base-proxy", "type": "socks5", "server": "127.0.0.1", "port": 1080}, + {"name": "proxy-with-dialer", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "base-proxy"}, + }, + errContains: "", + }, + { + testName: "NotFoundReference", + proxy: []map[string]any{ // create proxy with non-existent dialer-proxy reference + {"name": "proxy-with-dialer", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "non-existent-proxy"}, + }, + errContains: "not found", + }, + { + testName: "CircularDependency", + proxy: []map[string]any{ + // create proxy A that references B + {"name": "proxy-a", "type": "socks5", "server": "127.0.0.1", "port": 1080, "dialer-proxy": "proxy-c"}, + // create proxy B that references C + {"name": "proxy-b", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "proxy-a"}, + // create proxy C that references A (creates cycle) + {"name": "proxy-c", "type": "socks5", "server": "127.0.0.1", "port": 1082, "dialer-proxy": "proxy-a"}, + }, + errContains: "circular", + }, + { + testName: "ComplexChain", + proxy: []map[string]any{ // create a valid chain: proxy-d -> proxy-c -> proxy-b -> proxy-a + {"name": "proxy-a", "type": "socks5", "server": "127.0.0.1", "port": 1080}, + {"name": "proxy-b", "type": "socks5", "server": "127.0.0.1", "port": 1081, "dialer-proxy": "proxy-a"}, + {"name": "proxy-c", "type": "socks5", "server": "127.0.0.1", "port": 1082, "dialer-proxy": "proxy-b"}, + {"name": "proxy-d", "type": "socks5", "server": "127.0.0.1", "port": 1083, "dialer-proxy": "proxy-c"}, + }, + errContains: "", + }, + { + testName: "EmptyDialerProxy", + proxy: []map[string]any{ // create proxy without dialer-proxy + {"name": "simple-proxy", "type": "socks5", "server": "127.0.0.1", "port": 1080}, + }, + errContains: "", + }, + { + testName: "SelfReference", + proxy: []map[string]any{ // create proxy that references itself + {"name": "self-proxy", "type": "socks5", "server": "127.0.0.1", "port": 1080, "dialer-proxy": "self-proxy"}, + }, + errContains: "circular", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + config := RawConfig{Proxy: testCase.proxy} + _, _, err := parseProxies(&config) + if testCase.errContains == "" { + assert.NoError(t, err, testCase.testName) + } else { + assert.ErrorContains(t, err, testCase.errContains, testCase.testName) + } + }) + } +}