From b0464a986303d4e4e300d5fcb5b66b0d78a51b2b Mon Sep 17 00:00:00 2001 From: hank Date: Wed, 19 Aug 2026 16:28:53 +0800 Subject: [PATCH] feat(proxy): allow configurable probe targets --- backend/internal/config/config.go | 10 +++ .../repository/proxy_probe_service.go | 69 +++++++++++++++++-- .../repository/proxy_probe_service_test.go | 16 +++++ deploy/config.example.yaml | 10 +++ 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index e437b1115..798c08017 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -830,6 +830,16 @@ type ProxyFallbackConfig struct { type ProxyProbeConfig struct { InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证 + // URLs 按优先级排列的自定义探测 URL 列表。 + // 留空时使用内置默认列表(ip-api → ipify)。 + // 某些 AI API 专用代理只允许访问特定域名,配置多个备选可提高探测成功率。 + URLs []ProbeURLConfig `mapstructure:"urls"` +} + +// ProbeURLConfig 描述一个探测端点及其响应解析方式。 +type ProbeURLConfig struct { + URL string `mapstructure:"url"` + Parser string `mapstructure:"parser"` // "ip-api" / "ipify" / "chatgpt-trace" } type BillingConfig struct { diff --git a/backend/internal/repository/proxy_probe_service.go b/backend/internal/repository/proxy_probe_service.go index 245eaade8..7ecb26cfd 100644 --- a/backend/internal/repository/proxy_probe_service.go +++ b/backend/internal/repository/proxy_probe_service.go @@ -31,11 +31,27 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber { if insecure { log.Printf("[ProxyProbe] Warning: insecure_skip_verify is not allowed and will cause probe failure.") } + // 构建探测 URL 列表:优先用配置的自定义列表,否则用内置默认列表 + probeTargets := defaultProbeURLs + if cfg != nil && len(cfg.Security.ProxyProbe.URLs) > 0 { + probeTargets = make([]probeTarget, 0, len(cfg.Security.ProxyProbe.URLs)) + for _, u := range cfg.Security.ProxyProbe.URLs { + if strings.TrimSpace(u.URL) == "" || strings.TrimSpace(u.Parser) == "" { + continue + } + probeTargets = append(probeTargets, probeTarget{url: u.URL, parser: u.Parser}) + } + if len(probeTargets) == 0 { + probeTargets = defaultProbeURLs + } + } + return &proxyProbeService{ insecureSkipVerify: insecure, allowPrivateHosts: allowPrivate, validateResolvedIP: validateResolvedIP, maxResponseBytes: maxResponseBytes, + probeURLs: probeTargets, } } @@ -44,12 +60,16 @@ const ( defaultProxyProbeResponseMaxBytes = int64(1024 * 1024) ) -// probeURLs 按优先级排列的探测 URL 列表 -// 某些 AI API 专用代理只允许访问特定域名,因此需要多个备选 -var probeURLs = []struct { +// probeTarget 描述一个探测端点及其响应解析方式。 +type probeTarget struct { url string - parser string // "ip-api" or "ipify" -}{ + parser string // "ip-api" / "ipify" / "chatgpt-trace" +} + +// defaultProbeURLs 按优先级排列的默认探测 URL 列表。 +// 某些 AI API 专用代理只允许访问特定域名,因此需要多个备选。 +// 可通过配置 security.proxy_probe.urls 覆盖。 +var defaultProbeURLs = []probeTarget{ {"http://ip-api.com/json/?lang=zh-CN", "ip-api"}, {"http://api64.ipify.org?format=json", "ipify"}, } @@ -59,6 +79,7 @@ type proxyProbeService struct { allowPrivateHosts bool validateResolvedIP bool maxResponseBytes int64 + probeURLs []probeTarget } func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*service.ProxyExitInfo, int64, error) { @@ -74,6 +95,10 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s } var lastErr error + probeURLs := s.probeURLs + if len(probeURLs) == 0 { + probeURLs = defaultProbeURLs + } for _, probe := range probeURLs { exitInfo, latencyMs, err := s.probeWithURL(ctx, client, probe.url, probe.parser) if err == nil { @@ -121,6 +146,8 @@ func (s *proxyProbeService) probeWithURL(ctx context.Context, client *http.Clien return s.parseIPAPI(body, latencyMs) case "ipify": return s.parseIPify(body, latencyMs) + case "chatgpt-trace": + return s.parseChatGPTTrace(body, latencyMs) default: return nil, latencyMs, fmt.Errorf("unknown parser: %s", parser) } @@ -179,3 +206,35 @@ func (s *proxyProbeService) parseIPify(body []byte, latencyMs int64) (*service.P IP: result.IP, }, latencyMs, nil } + +// parseChatGPTTrace 解析 Cloudflare trace 端点(如 chatgpt.com/cdn-cgi/trace)的纯文本响应。 +// 响应按行给出键值对,其中 ip= 为出口 IP,loc= 为国家代码。 +func (s *proxyProbeService) parseChatGPTTrace(body []byte, latencyMs int64) (*service.ProxyExitInfo, int64, error) { + var ip, loc string + for _, line := range strings.Split(string(body), "\n") { + key, value, found := strings.Cut(strings.TrimSpace(line), "=") + if !found { + continue + } + switch key { + case "ip": + ip = strings.TrimSpace(value) + case "loc": + loc = strings.TrimSpace(value) + } + } + if ip == "" { + preview := string(body) + if len(preview) > 200 { + preview = preview[:200] + "..." + } + return nil, latencyMs, fmt.Errorf("chatgpt-trace: no ip= found in response (body: %s)", preview) + } + info := &service.ProxyExitInfo{ + IP: ip, + } + if loc != "" { + info.CountryCode = loc + } + return info, latencyMs, nil +} diff --git a/backend/internal/repository/proxy_probe_service_test.go b/backend/internal/repository/proxy_probe_service_test.go index c778f6c1e..45c342cb4 100644 --- a/backend/internal/repository/proxy_probe_service_test.go +++ b/backend/internal/repository/proxy_probe_service_test.go @@ -166,6 +166,22 @@ func (s *ProxyProbeServiceSuite) TestParseIPify_NoIP() { require.ErrorContains(s.T(), err, "no IP found") } +func (s *ProxyProbeServiceSuite) TestParseChatGPTTrace_Success() { + body := []byte("fl=abc\nh=chatgpt.com\nip=203.0.113.5\nts=1700000000\nloc=US\ntz=UTC\n") + info, latencyMs, err := s.prober.parseChatGPTTrace(body, 320) + require.NoError(s.T(), err) + require.Equal(s.T(), int64(320), latencyMs) + require.Equal(s.T(), "203.0.113.5", info.IP) + require.Equal(s.T(), "US", info.CountryCode) +} + +func (s *ProxyProbeServiceSuite) TestParseChatGPTTrace_NoIP() { + body := []byte("fl=abc\nh=chatgpt.com\nloc=US\n") + _, _, err := s.prober.parseChatGPTTrace(body, 100) + require.Error(s.T(), err) + require.ErrorContains(s.T(), err, "chatgpt-trace: no ip= found") +} + func TestProxyProbeServiceSuite(t *testing.T) { suite.Run(t, new(ProxyProbeServiceSuite)) } diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index e8c1c84b7..8f134dd60 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -186,6 +186,16 @@ security: # Allow skipping TLS verification for proxy probe (debug only) # 允许代理探测时跳过 TLS 证书验证(仅用于调试) insecure_skip_verify: false + # Optional ordered probe targets. Leave empty to use the built-in ip-api/ipify fallback. + # parser supports: ip-api, ipify, chatgpt-trace + # 可选的有序探测目标。留空时使用内置 ip-api/ipify 回退。 + # parser 支持:ip-api、ipify、chatgpt-trace + urls: [] + # urls: + # - url: "https://chatgpt.com/cdn-cgi/trace" + # parser: "chatgpt-trace" + # - url: "https://api64.ipify.org?format=json" + # parser: "ipify" proxy_fallback: # Allow auxiliary services (update check, pricing data) to fallback to direct # connection when proxy initialization fails. Does NOT affect AI gateway connections.