From 425f23a739d7854e32ba4ce8ac13b36451935982 Mon Sep 17 00:00:00 2001 From: tw93 Date: Sat, 7 Feb 2026 17:43:59 +0800 Subject: [PATCH] fix(status): improve proxy detection and add parser tests --- cmd/status/metrics.go | 2 +- cmd/status/metrics_network.go | 183 +++++++++++++++++++++++++---- cmd/status/metrics_network_test.go | 60 ++++++++++ 3 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 cmd/status/metrics_network_test.go diff --git a/cmd/status/metrics.go b/cmd/status/metrics.go index d573aa7..508754a 100644 --- a/cmd/status/metrics.go +++ b/cmd/status/metrics.go @@ -160,7 +160,7 @@ const NetworkHistorySize = 120 // Increased history size for wider graph type ProxyStatus struct { Enabled bool - Type string // HTTP, SOCKS, System + Type string // HTTP, HTTPS, SOCKS, PAC, WPAD, TUN Host string } diff --git a/cmd/status/metrics_network.go b/cmd/status/metrics_network.go index 89320b6..d0cd469 100644 --- a/cmd/status/metrics_network.go +++ b/cmd/status/metrics_network.go @@ -2,9 +2,11 @@ package main import ( "context" + "net/url" "os" "runtime" "sort" + "strconv" "strings" "time" @@ -114,23 +116,8 @@ func isNoiseInterface(name string) bool { } func collectProxy() ProxyStatus { - // Check environment variables first. - for _, env := range []string{"https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"} { - if val := os.Getenv(env); val != "" { - proxyType := "HTTP" - if strings.HasPrefix(val, "socks") { - proxyType = "SOCKS" - } - // Extract host. - host := val - if strings.Contains(host, "://") { - host = strings.SplitN(host, "://", 2)[1] - } - if idx := strings.Index(host, "@"); idx >= 0 { - host = host[idx+1:] - } - return ProxyStatus{Enabled: true, Type: proxyType, Host: host} - } + if proxy := collectProxyFromEnv(os.Getenv); proxy.Enabled { + return proxy } // macOS: check system proxy via scutil. @@ -139,14 +126,166 @@ func collectProxy() ProxyStatus { defer cancel() out, err := runCmd(ctx, "scutil", "--proxy") if err == nil { - if strings.Contains(out, "HTTPEnable : 1") || strings.Contains(out, "HTTPSEnable : 1") { - return ProxyStatus{Enabled: true, Type: "System", Host: "System Proxy"} - } - if strings.Contains(out, "SOCKSEnable : 1") { - return ProxyStatus{Enabled: true, Type: "SOCKS", Host: "System Proxy"} + if proxy := collectProxyFromScutilOutput(out); proxy.Enabled { + return proxy } } + + if proxy := collectProxyFromTunInterfaces(); proxy.Enabled { + return proxy + } } return ProxyStatus{Enabled: false} } + +func collectProxyFromEnv(getenv func(string) string) ProxyStatus { + // Include ALL_PROXY for users running proxy tools that only export a single variable. + envKeys := []string{ + "https_proxy", "HTTPS_PROXY", + "http_proxy", "HTTP_PROXY", + "all_proxy", "ALL_PROXY", + } + for _, key := range envKeys { + val := strings.TrimSpace(getenv(key)) + if val == "" { + continue + } + + proxyType := "HTTP" + lower := strings.ToLower(val) + if strings.HasPrefix(lower, "socks") { + proxyType = "SOCKS" + } + + host := parseProxyHost(val) + if host == "" { + host = val + } + return ProxyStatus{Enabled: true, Type: proxyType, Host: host} + } + + return ProxyStatus{Enabled: false} +} + +func collectProxyFromScutilOutput(out string) ProxyStatus { + if out == "" { + return ProxyStatus{Enabled: false} + } + + if scutilProxyEnabled(out, "SOCKSEnable") { + host := joinHostPort(scutilProxyValue(out, "SOCKSProxy"), scutilProxyValue(out, "SOCKSPort")) + if host == "" { + host = "System Proxy" + } + return ProxyStatus{Enabled: true, Type: "SOCKS", Host: host} + } + + if scutilProxyEnabled(out, "HTTPSEnable") { + host := joinHostPort(scutilProxyValue(out, "HTTPSProxy"), scutilProxyValue(out, "HTTPSPort")) + if host == "" { + host = "System Proxy" + } + return ProxyStatus{Enabled: true, Type: "HTTPS", Host: host} + } + + if scutilProxyEnabled(out, "HTTPEnable") { + host := joinHostPort(scutilProxyValue(out, "HTTPProxy"), scutilProxyValue(out, "HTTPPort")) + if host == "" { + host = "System Proxy" + } + return ProxyStatus{Enabled: true, Type: "HTTP", Host: host} + } + + if scutilProxyEnabled(out, "ProxyAutoConfigEnable") { + pacURL := scutilProxyValue(out, "ProxyAutoConfigURLString") + host := parseProxyHost(pacURL) + if host == "" { + host = "PAC" + } + return ProxyStatus{Enabled: true, Type: "PAC", Host: host} + } + + if scutilProxyEnabled(out, "ProxyAutoDiscoveryEnable") { + return ProxyStatus{Enabled: true, Type: "WPAD", Host: "Auto Discovery"} + } + + return ProxyStatus{Enabled: false} +} + +func collectProxyFromTunInterfaces() ProxyStatus { + stats, err := net.IOCounters(true) + if err != nil { + return ProxyStatus{Enabled: false} + } + + var activeTun []string + for _, s := range stats { + lower := strings.ToLower(s.Name) + if strings.HasPrefix(lower, "utun") || strings.HasPrefix(lower, "tun") { + if s.BytesRecv+s.BytesSent > 0 { + activeTun = append(activeTun, s.Name) + } + } + } + if len(activeTun) == 0 { + return ProxyStatus{Enabled: false} + } + sort.Strings(activeTun) + host := activeTun[0] + if len(activeTun) > 1 { + host = activeTun[0] + "+" + } + return ProxyStatus{Enabled: true, Type: "TUN", Host: host} +} + +func scutilProxyEnabled(out, key string) bool { + return scutilProxyValue(out, key) == "1" +} + +func scutilProxyValue(out, key string) string { + prefix := key + " :" + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, prefix) { + return strings.TrimSpace(strings.TrimPrefix(line, prefix)) + } + } + return "" +} + +func parseProxyHost(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + + target := raw + if !strings.Contains(target, "://") { + target = "http://" + target + } + parsed, err := url.Parse(target) + if err != nil { + return "" + } + host := parsed.Host + if host == "" { + return "" + } + return strings.TrimPrefix(host, "@") +} + +func joinHostPort(host, port string) string { + host = strings.TrimSpace(host) + port = strings.TrimSpace(port) + if host == "" { + return "" + } + if port == "" { + return host + } + if _, err := strconv.Atoi(port); err != nil { + return host + } + return host + ":" + port +} diff --git a/cmd/status/metrics_network_test.go b/cmd/status/metrics_network_test.go new file mode 100644 index 0000000..640675a --- /dev/null +++ b/cmd/status/metrics_network_test.go @@ -0,0 +1,60 @@ +package main + +import "testing" + +func TestCollectProxyFromEnvSupportsAllProxy(t *testing.T) { + env := map[string]string{ + "ALL_PROXY": "socks5://127.0.0.1:7890", + } + getenv := func(key string) string { + return env[key] + } + + got := collectProxyFromEnv(getenv) + if !got.Enabled { + t.Fatalf("expected proxy enabled") + } + if got.Type != "SOCKS" { + t.Fatalf("expected SOCKS type, got %s", got.Type) + } + if got.Host != "127.0.0.1:7890" { + t.Fatalf("unexpected host: %s", got.Host) + } +} + +func TestCollectProxyFromScutilOutputPAC(t *testing.T) { + out := ` + { + ProxyAutoConfigEnable : 1 + ProxyAutoConfigURLString : http://127.0.0.1:6152/proxy.pac +}` + got := collectProxyFromScutilOutput(out) + if !got.Enabled { + t.Fatalf("expected proxy enabled") + } + if got.Type != "PAC" { + t.Fatalf("expected PAC type, got %s", got.Type) + } + if got.Host != "127.0.0.1:6152" { + t.Fatalf("unexpected host: %s", got.Host) + } +} + +func TestCollectProxyFromScutilOutputHTTPHostPort(t *testing.T) { + out := ` + { + HTTPEnable : 1 + HTTPProxy : 127.0.0.1 + HTTPPort : 7890 +}` + got := collectProxyFromScutilOutput(out) + if !got.Enabled { + t.Fatalf("expected proxy enabled") + } + if got.Type != "HTTP" { + t.Fatalf("expected HTTP type, got %s", got.Type) + } + if got.Host != "127.0.0.1:7890" { + t.Fatalf("unexpected host: %s", got.Host) + } +}