From c9318c54cd0038b72b1700aeaa86d781d2ea622a Mon Sep 17 00:00:00 2001 From: Zyy <39424122+zyycn@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:05:08 +0800 Subject: [PATCH] fix(proxy): support IPv6 egress probes (#110) Co-authored-by: Zyy Co-authored-by: Codex --- .../crates/gateway-host/src/proxy_probe.rs | 3 +- .../crates/gateway-host/tests/proxy_probe.rs | 41 ++++++++++++------- docs/api.md | 3 +- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/backend/crates/gateway-host/src/proxy_probe.rs b/backend/crates/gateway-host/src/proxy_probe.rs index 772486503..34bec3dc2 100644 --- a/backend/crates/gateway-host/src/proxy_probe.rs +++ b/backend/crates/gateway-host/src/proxy_probe.rs @@ -21,7 +21,8 @@ type ProxyClientBuilder = impl Default for HttpProxyProbe { fn default() -> Self { - Self::new("https://api.ipify.org?format=json") + // 使用双栈端点,避免仅有 IPv6 出口的代理被 IPv4 专用检测服务误判为不可用。 + Self::new("https://api64.ipify.org?format=json") } } diff --git a/backend/crates/gateway-host/tests/proxy_probe.rs b/backend/crates/gateway-host/tests/proxy_probe.rs index 6845b9726..f82415506 100644 --- a/backend/crates/gateway-host/tests/proxy_probe.rs +++ b/backend/crates/gateway-host/tests/proxy_probe.rs @@ -8,20 +8,33 @@ use wiremock::{ }; #[tokio::test] -async fn proxy_probe_sends_authentication_through_explicit_proxy() { - let proxy_server = MockServer::start().await; - Mock::given(header("proxy-authorization", "Basic dXNlcjpwYXNzd29yZA==")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({"ip": "203.0.113.8"}))) - .expect(1) - .mount(&proxy_server) - .await; - let proxy = - OutboundProxy::parse(&format!("http://user:password@{}", proxy_server.address())).unwrap(); - let result = HttpProxyProbe::new("http://unresolvable.invalid/ip") - .test(&proxy) - .await; - assert!(result.success); - assert_eq!(result.exit_ip.unwrap().to_string(), "203.0.113.8"); +async fn proxy_probe_supports_ipv4_and_ipv6_proxies_and_exit_addresses() { + for (listen_address, exit_ip) in [ + ("127.0.0.1:0", "203.0.113.8"), + ("127.0.0.1:0", "2001:db8::8"), + ("[::1]:0", "203.0.113.8"), + ("[::1]:0", "2001:db8::8"), + ] { + let listener = std::net::TcpListener::bind(listen_address).unwrap(); + let proxy_server = MockServer::builder().listener(listener).start().await; + Mock::given(header("proxy-authorization", "Basic dXNlcjpwYXNzd29yZA==")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({"ip": exit_ip}))) + .expect(1) + .mount(&proxy_server) + .await; + let proxy = + OutboundProxy::parse(&format!("http://user:password@{}", proxy_server.address())) + .unwrap(); + let result = HttpProxyProbe::new("http://unresolvable.invalid/ip") + .test(&proxy) + .await; + assert!( + result.success, + "{listen_address} -> {exit_ip}: {}", + result.message + ); + assert_eq!(result.exit_ip.unwrap().to_string(), exit_ip); + } } #[tokio::test] diff --git a/docs/api.md b/docs/api.md index b311fc715..86df329a8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -417,7 +417,8 @@ Images、独立 Search 及管理员连接测试不受该文本模型限制;连 更新省略 `proxyUrl` 保留认证;连接配置改变时清除测试结果并更新所有绑定账号。 测试结果只在请求中的版本仍匹配时保存。 -测试固定经代理访问 `https://api.ipify.org?format=json`,超时 15 秒,每进程最多同时测试 4 条。 +测试固定经代理访问双栈端点 `https://api64.ipify.org?format=json`,返回本次连接实际使用的 IPv4 或 IPv6 出口地址, +不分别验证两种地址族的连通性。超时 15 秒,每进程最多同时测试 4 条。 探测器复用 OpenAI 的证书信任配置:优先读取非空的 `CODEX_CA_CERTIFICATE`, 其次读取 `SSL_CERT_FILE`,并保留系统根证书;证书配置错误不会回退为不验证证书。 出口测试通过不表示 Provider 账号权限或额度可用;账号可用性使用账号连接测试。