Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ba5a0d47
Unverified
Commit
ba5a0d47
authored
Feb 03, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 03, 2026
Browse files
Merge pull request #460 from s-Joshua-s/fix/proxy-probe-fallback
fix(proxy): 增加代理探测的多 URL 回退机制
parents
c89bbf51
df7a3e65
Changes
2
Show whitespace changes
Inline
Side-by-side
backend/internal/repository/proxy_probe_service.go
View file @
ba5a0d47
...
@@ -28,7 +28,6 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
...
@@ -28,7 +28,6 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
log
.
Printf
(
"[ProxyProbe] Warning: insecure_skip_verify is not allowed and will cause probe failure."
)
log
.
Printf
(
"[ProxyProbe] Warning: insecure_skip_verify is not allowed and will cause probe failure."
)
}
}
return
&
proxyProbeService
{
return
&
proxyProbeService
{
ipInfoURL
:
defaultIPInfoURL
,
insecureSkipVerify
:
insecure
,
insecureSkipVerify
:
insecure
,
allowPrivateHosts
:
allowPrivate
,
allowPrivateHosts
:
allowPrivate
,
validateResolvedIP
:
validateResolvedIP
,
validateResolvedIP
:
validateResolvedIP
,
...
@@ -36,12 +35,20 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
...
@@ -36,12 +35,20 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
}
}
const
(
const
(
defaultIPInfoURL
=
"http://ip-api.com/json/?lang=zh-CN"
defaultProxyProbeTimeout
=
30
*
time
.
Second
defaultProxyProbeTimeout
=
30
*
time
.
Second
)
)
// probeURLs 按优先级排列的探测 URL 列表
// 某些 AI API 专用代理只允许访问特定域名,因此需要多个备选
var
probeURLs
=
[]
struct
{
url
string
parser
string
// "ip-api" or "httpbin"
}{
{
"http://ip-api.com/json/?lang=zh-CN"
,
"ip-api"
},
{
"http://httpbin.org/ip"
,
"httpbin"
},
}
type
proxyProbeService
struct
{
type
proxyProbeService
struct
{
ipInfoURL
string
insecureSkipVerify
bool
insecureSkipVerify
bool
allowPrivateHosts
bool
allowPrivateHosts
bool
validateResolvedIP
bool
validateResolvedIP
bool
...
@@ -60,8 +67,21 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
...
@@ -60,8 +67,21 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
return
nil
,
0
,
fmt
.
Errorf
(
"failed to create proxy client: %w"
,
err
)
return
nil
,
0
,
fmt
.
Errorf
(
"failed to create proxy client: %w"
,
err
)
}
}
var
lastErr
error
for
_
,
probe
:=
range
probeURLs
{
exitInfo
,
latencyMs
,
err
:=
s
.
probeWithURL
(
ctx
,
client
,
probe
.
url
,
probe
.
parser
)
if
err
==
nil
{
return
exitInfo
,
latencyMs
,
nil
}
lastErr
=
err
}
return
nil
,
0
,
fmt
.
Errorf
(
"all probe URLs failed, last error: %w"
,
lastErr
)
}
func
(
s
*
proxyProbeService
)
probeWithURL
(
ctx
context
.
Context
,
client
*
http
.
Client
,
url
string
,
parser
string
)
(
*
service
.
ProxyExitInfo
,
int64
,
error
)
{
startTime
:=
time
.
Now
()
startTime
:=
time
.
Now
()
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
s
.
ipInfoURL
,
nil
)
req
,
err
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
url
,
nil
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
0
,
fmt
.
Errorf
(
"failed to create request: %w"
,
err
)
return
nil
,
0
,
fmt
.
Errorf
(
"failed to create request: %w"
,
err
)
}
}
...
@@ -78,6 +98,22 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
...
@@ -78,6 +98,22 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"request failed with status: %d"
,
resp
.
StatusCode
)
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"request failed with status: %d"
,
resp
.
StatusCode
)
}
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to read response: %w"
,
err
)
}
switch
parser
{
case
"ip-api"
:
return
s
.
parseIPAPI
(
body
,
latencyMs
)
case
"httpbin"
:
return
s
.
parseHTTPBin
(
body
,
latencyMs
)
default
:
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"unknown parser: %s"
,
parser
)
}
}
func
(
s
*
proxyProbeService
)
parseIPAPI
(
body
[]
byte
,
latencyMs
int64
)
(
*
service
.
ProxyExitInfo
,
int64
,
error
)
{
var
ipInfo
struct
{
var
ipInfo
struct
{
Status
string
`json:"status"`
Status
string
`json:"status"`
Message
string
`json:"message"`
Message
string
`json:"message"`
...
@@ -89,13 +125,12 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
...
@@ -89,13 +125,12 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
CountryCode
string
`json:"countryCode"`
CountryCode
string
`json:"countryCode"`
}
}
body
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to read response: %w"
,
err
)
}
if
err
:=
json
.
Unmarshal
(
body
,
&
ipInfo
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
(
body
,
&
ipInfo
);
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to parse response: %w"
,
err
)
preview
:=
string
(
body
)
if
len
(
preview
)
>
200
{
preview
=
preview
[
:
200
]
+
"..."
}
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to parse response: %w (body: %s)"
,
err
,
preview
)
}
}
if
strings
.
ToLower
(
ipInfo
.
Status
)
!=
"success"
{
if
strings
.
ToLower
(
ipInfo
.
Status
)
!=
"success"
{
if
ipInfo
.
Message
==
""
{
if
ipInfo
.
Message
==
""
{
...
@@ -116,3 +151,19 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
...
@@ -116,3 +151,19 @@ func (s *proxyProbeService) ProbeProxy(ctx context.Context, proxyURL string) (*s
CountryCode
:
ipInfo
.
CountryCode
,
CountryCode
:
ipInfo
.
CountryCode
,
},
latencyMs
,
nil
},
latencyMs
,
nil
}
}
func
(
s
*
proxyProbeService
)
parseHTTPBin
(
body
[]
byte
,
latencyMs
int64
)
(
*
service
.
ProxyExitInfo
,
int64
,
error
)
{
// httpbin.org/ip 返回格式: {"origin": "1.2.3.4"}
var
result
struct
{
Origin
string
`json:"origin"`
}
if
err
:=
json
.
Unmarshal
(
body
,
&
result
);
err
!=
nil
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"failed to parse httpbin response: %w"
,
err
)
}
if
result
.
Origin
==
""
{
return
nil
,
latencyMs
,
fmt
.
Errorf
(
"httpbin: no IP found in response"
)
}
return
&
service
.
ProxyExitInfo
{
IP
:
result
.
Origin
,
},
latencyMs
,
nil
}
backend/internal/repository/proxy_probe_service_test.go
View file @
ba5a0d47
...
@@ -5,6 +5,7 @@ import (
...
@@ -5,6 +5,7 @@ import (
"io"
"io"
"net/http"
"net/http"
"net/http/httptest"
"net/http/httptest"
"strings"
"testing"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
...
@@ -21,7 +22,6 @@ type ProxyProbeServiceSuite struct {
...
@@ -21,7 +22,6 @@ type ProxyProbeServiceSuite struct {
func
(
s
*
ProxyProbeServiceSuite
)
SetupTest
()
{
func
(
s
*
ProxyProbeServiceSuite
)
SetupTest
()
{
s
.
ctx
=
context
.
Background
()
s
.
ctx
=
context
.
Background
()
s
.
prober
=
&
proxyProbeService
{
s
.
prober
=
&
proxyProbeService
{
ipInfoURL
:
"http://ip-api.test/json/?lang=zh-CN"
,
allowPrivateHosts
:
true
,
allowPrivateHosts
:
true
,
}
}
}
}
...
@@ -49,12 +49,16 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_UnsupportedProxyScheme() {
...
@@ -49,12 +49,16 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_UnsupportedProxyScheme() {
require
.
ErrorContains
(
s
.
T
(),
err
,
"failed to create proxy client"
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"failed to create proxy client"
)
}
}
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_Success
()
{
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_Success_IPAPI
()
{
seen
:=
make
(
chan
string
,
1
)
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
seen
<-
r
.
RequestURI
// 检查是否是 ip-api 请求
if
strings
.
Contains
(
r
.
RequestURI
,
"ip-api.com"
)
{
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
io
.
WriteString
(
w
,
`{"status":"success","query":"1.2.3.4","city":"c","regionName":"r","country":"cc","countryCode":"CC"}`
)
_
,
_
=
io
.
WriteString
(
w
,
`{"status":"success","query":"1.2.3.4","city":"c","regionName":"r","country":"cc","countryCode":"CC"}`
)
return
}
// 其他请求返回错误
w
.
WriteHeader
(
http
.
StatusServiceUnavailable
)
}))
}))
info
,
latencyMs
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
info
,
latencyMs
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
...
@@ -65,45 +69,59 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success() {
...
@@ -65,45 +69,59 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_Success() {
require
.
Equal
(
s
.
T
(),
"r"
,
info
.
Region
)
require
.
Equal
(
s
.
T
(),
"r"
,
info
.
Region
)
require
.
Equal
(
s
.
T
(),
"cc"
,
info
.
Country
)
require
.
Equal
(
s
.
T
(),
"cc"
,
info
.
Country
)
require
.
Equal
(
s
.
T
(),
"CC"
,
info
.
CountryCode
)
require
.
Equal
(
s
.
T
(),
"CC"
,
info
.
CountryCode
)
}
// Verify proxy received the request
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_Success_HTTPBinFallback
()
{
s
elect
{
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
case
uri
:=
<-
seen
:
// ip-api 失败
require
.
Contains
(
s
.
T
(),
uri
,
"ip-api.test"
,
"expected request to go through proxy"
)
if
strings
.
Contains
(
r
.
RequestURI
,
"ip-api.com"
)
{
default
:
w
.
WriteHeader
(
http
.
StatusServiceUnavailable
)
re
quire
.
Fail
(
s
.
T
(),
"expected proxy to receive request"
)
re
turn
}
}
// httpbin 成功
if
strings
.
Contains
(
r
.
RequestURI
,
"httpbin.org"
)
{
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
io
.
WriteString
(
w
,
`{"origin": "5.6.7.8"}`
)
return
}
w
.
WriteHeader
(
http
.
StatusServiceUnavailable
)
}))
info
,
latencyMs
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
require
.
NoError
(
s
.
T
(),
err
,
"ProbeProxy should fallback to httpbin"
)
require
.
GreaterOrEqual
(
s
.
T
(),
latencyMs
,
int64
(
0
),
"unexpected latency"
)
require
.
Equal
(
s
.
T
(),
"5.6.7.8"
,
info
.
IP
)
}
}
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_
NonOKStatus
()
{
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_
AllFailed
()
{
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusServiceUnavailable
)
w
.
WriteHeader
(
http
.
StatusServiceUnavailable
)
}))
}))
_
,
_
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
_
,
_
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
require
.
Error
(
s
.
T
(),
err
)
require
.
Error
(
s
.
T
(),
err
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"
status: 503
"
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"
all probe URLs failed
"
)
}
}
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_InvalidJSON
()
{
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_InvalidJSON
()
{
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
if
strings
.
Contains
(
r
.
RequestURI
,
"ip-api.com"
)
{
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
io
.
WriteString
(
w
,
"not-json"
)
return
}
// httpbin 也返回无效响应
if
strings
.
Contains
(
r
.
RequestURI
,
"httpbin.org"
)
{
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
io
.
WriteString
(
w
,
"not-json"
)
_
,
_
=
io
.
WriteString
(
w
,
"not-json"
)
return
}
w
.
WriteHeader
(
http
.
StatusServiceUnavailable
)
}))
}))
_
,
_
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
_
,
_
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
require
.
Error
(
s
.
T
(),
err
)
require
.
Error
(
s
.
T
(),
err
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"failed to parse response"
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"all probe URLs failed"
)
}
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_InvalidIPInfoURL
()
{
s
.
prober
.
ipInfoURL
=
"://invalid-url"
s
.
setupProxyServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusOK
)
}))
_
,
_
,
err
:=
s
.
prober
.
ProbeProxy
(
s
.
ctx
,
s
.
proxySrv
.
URL
)
require
.
Error
(
s
.
T
(),
err
,
"expected error for invalid ipInfoURL"
)
}
}
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_ProxyServerClosed
()
{
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_ProxyServerClosed
()
{
...
@@ -114,6 +132,40 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_ProxyServerClosed() {
...
@@ -114,6 +132,40 @@ func (s *ProxyProbeServiceSuite) TestProbeProxy_ProxyServerClosed() {
require
.
Error
(
s
.
T
(),
err
,
"expected error when proxy server is closed"
)
require
.
Error
(
s
.
T
(),
err
,
"expected error when proxy server is closed"
)
}
}
func
(
s
*
ProxyProbeServiceSuite
)
TestParseIPAPI_Success
()
{
body
:=
[]
byte
(
`{"status":"success","query":"1.2.3.4","city":"Beijing","regionName":"Beijing","country":"China","countryCode":"CN"}`
)
info
,
latencyMs
,
err
:=
s
.
prober
.
parseIPAPI
(
body
,
100
)
require
.
NoError
(
s
.
T
(),
err
)
require
.
Equal
(
s
.
T
(),
int64
(
100
),
latencyMs
)
require
.
Equal
(
s
.
T
(),
"1.2.3.4"
,
info
.
IP
)
require
.
Equal
(
s
.
T
(),
"Beijing"
,
info
.
City
)
require
.
Equal
(
s
.
T
(),
"Beijing"
,
info
.
Region
)
require
.
Equal
(
s
.
T
(),
"China"
,
info
.
Country
)
require
.
Equal
(
s
.
T
(),
"CN"
,
info
.
CountryCode
)
}
func
(
s
*
ProxyProbeServiceSuite
)
TestParseIPAPI_Failure
()
{
body
:=
[]
byte
(
`{"status":"fail","message":"rate limited"}`
)
_
,
_
,
err
:=
s
.
prober
.
parseIPAPI
(
body
,
100
)
require
.
Error
(
s
.
T
(),
err
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"rate limited"
)
}
func
(
s
*
ProxyProbeServiceSuite
)
TestParseHTTPBin_Success
()
{
body
:=
[]
byte
(
`{"origin": "9.8.7.6"}`
)
info
,
latencyMs
,
err
:=
s
.
prober
.
parseHTTPBin
(
body
,
50
)
require
.
NoError
(
s
.
T
(),
err
)
require
.
Equal
(
s
.
T
(),
int64
(
50
),
latencyMs
)
require
.
Equal
(
s
.
T
(),
"9.8.7.6"
,
info
.
IP
)
}
func
(
s
*
ProxyProbeServiceSuite
)
TestParseHTTPBin_NoIP
()
{
body
:=
[]
byte
(
`{"origin": ""}`
)
_
,
_
,
err
:=
s
.
prober
.
parseHTTPBin
(
body
,
50
)
require
.
Error
(
s
.
T
(),
err
)
require
.
ErrorContains
(
s
.
T
(),
err
,
"no IP found"
)
}
func
TestProxyProbeServiceSuite
(
t
*
testing
.
T
)
{
func
TestProxyProbeServiceSuite
(
t
*
testing
.
T
)
{
suite
.
Run
(
t
,
new
(
ProxyProbeServiceSuite
))
suite
.
Run
(
t
,
new
(
ProxyProbeServiceSuite
))
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment