Skip to content

Commit 2008a51

Browse files
committed
fix: 增强节点检测与IP检查回退
- /check-ip 支持 SOCKS5 回退成功 - IP 检测新增多服务 fallback + SBPM_IPCHECK_URLS - TLS ALPN 逗号拆分 - ss:// Base64 全量格式兼容 - 版本升级到 1.3.13
1 parent bb37b20 commit 2008a51

9 files changed

Lines changed: 358 additions & 73 deletions

File tree

backend/api/handlers.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,22 +1201,7 @@ func (h *Handler) CheckNodeIP(c *gin.Context) {
12011201
`, id); clearErr != nil {
12021202
fmt.Printf("[API] Failed to clear node %d status after error: %v\n", id, clearErr)
12031203
}
1204-
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check IP"})
1205-
return
1206-
}
1207-
1208-
// If HTTP path failed but SOCKS5 succeeded, treat it as an error because mixed inbound should serve both.
1209-
if ipInfo.Transport != "" && ipInfo.Transport != "http" {
1210-
msg := "http proxy failed while socks5 succeeded"
1211-
if ipInfo.HTTPError != "" {
1212-
msg = ipInfo.HTTPError
1213-
}
1214-
_, _ = h.db.Exec(`
1215-
UPDATE proxy_nodes
1216-
SET node_ip = '', location = '', country_code = '', latency = 0, updated_at = CURRENT_TIMESTAMP
1217-
WHERE id = ?
1218-
`, id)
1219-
c.JSON(http.StatusBadGateway, gin.H{"error": msg})
1204+
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
12201205
return
12211206
}
12221207

backend/api/handlers_test.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func TestCheckNodeIPFailureClearsStatus(t *testing.T) {
147147

148148
handler.CheckNodeIP(ctx)
149149

150-
if rec.Code != http.StatusInternalServerError {
150+
if rec.Code != http.StatusBadGateway {
151151
t.Fatalf("unexpected status %d", rec.Code)
152152
}
153153
var ip, location, countryCode string
@@ -163,16 +163,17 @@ func TestCheckNodeIPFailureClearsStatus(t *testing.T) {
163163
}
164164
}
165165

166-
func TestCheckNodeIPRejectsSocksFallback(t *testing.T) {
166+
func TestCheckNodeIPAcceptsSocksFallback(t *testing.T) {
167167
gin.SetMode(gin.TestMode)
168168

169169
handler := newTestHandler(t, func(proxyAddr, username, password string) (*services.IPInfo, error) {
170170
return &services.IPInfo{
171-
IP: "9.9.9.9",
172-
Location: "Fallback",
173-
Latency: 40,
174-
Transport: "socks5",
175-
HTTPError: "http unavailable",
171+
IP: "9.9.9.9",
172+
Location: "Fallback",
173+
CountryCode: "XX",
174+
Latency: 40,
175+
Transport: "socks5",
176+
HTTPError: "http unavailable",
176177
}, nil
177178
})
178179

@@ -184,8 +185,8 @@ func TestCheckNodeIPRejectsSocksFallback(t *testing.T) {
184185

185186
handler.CheckNodeIP(ctx)
186187

187-
if rec.Code != http.StatusBadGateway {
188-
t.Fatalf("expected bad gateway, got %d", rec.Code)
188+
if rec.Code != http.StatusOK {
189+
t.Fatalf("expected ok, got %d", rec.Code)
189190
}
190191
var ip, location, countryCode string
191192
var latency int
@@ -195,8 +196,8 @@ func TestCheckNodeIPRejectsSocksFallback(t *testing.T) {
195196
`, nodeID).Scan(&ip, &location, &countryCode, &latency); err != nil {
196197
t.Fatalf("query node: %v", err)
197198
}
198-
if ip != "" || location != "" || countryCode != "" || latency != 0 {
199-
t.Fatalf("expected cleared status after fallback, got ip=%s location=%s country=%s latency=%d", ip, location, countryCode, latency)
199+
if ip != "9.9.9.9" || location != "Fallback" || countryCode != "XX" || latency != 40 {
200+
t.Fatalf("expected status updated after fallback, got ip=%s location=%s country=%s latency=%d", ip, location, countryCode, latency)
200201
}
201202
}
202203

backend/services/ipcheck.go

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"net"
99
"net/http"
1010
"net/url"
11+
"os"
12+
"strings"
1113
"time"
1214

1315
"golang.org/x/net/proxy"
@@ -27,6 +29,57 @@ type IPInfo struct {
2729

2830
const maxIPCheckResponseBytes = 1024 * 1024
2931

32+
func ipCheckServiceURLs() []string {
33+
if fromEnv := parseIPCheckServiceURLsFromEnv(); len(fromEnv) > 0 {
34+
return fromEnv
35+
}
36+
37+
return []string{
38+
"https://api.ip2location.io/?format=json",
39+
"http://ip-api.com/json/",
40+
"https://api.ip.sb/geoip",
41+
"https://ipwho.is/",
42+
"https://ipapi.co/json/",
43+
"https://api.myip.com",
44+
"https://api64.ipify.org?format=json",
45+
}
46+
}
47+
48+
func parseIPCheckServiceURLsFromEnv() []string {
49+
raw := strings.TrimSpace(os.Getenv("SBPM_IPCHECK_URLS"))
50+
if raw == "" {
51+
return nil
52+
}
53+
54+
parts := strings.FieldsFunc(raw, func(r rune) bool {
55+
switch r {
56+
case ',', '\n', '\r', '\t', ' ':
57+
return true
58+
default:
59+
return false
60+
}
61+
})
62+
63+
out := make([]string, 0, len(parts))
64+
seen := make(map[string]struct{}, len(parts))
65+
for _, part := range parts {
66+
part = strings.TrimSpace(part)
67+
if part == "" {
68+
continue
69+
}
70+
if !strings.HasPrefix(part, "http://") && !strings.HasPrefix(part, "https://") {
71+
continue
72+
}
73+
if _, ok := seen[part]; ok {
74+
continue
75+
}
76+
seen[part] = struct{}{}
77+
out = append(out, part)
78+
}
79+
80+
return out
81+
}
82+
3083
func waitForTCPReady(addr string, maxWait time.Duration) error {
3184
deadline := time.Now().Add(maxWait)
3285
backoff := 50 * time.Millisecond
@@ -54,13 +107,12 @@ func waitForTCPReady(addr string, maxWait time.Duration) error {
54107
func CheckProxyIP(proxyAddr string, username string, password string) (*IPInfo, error) {
55108
log.Printf("[IPCheck] Starting IP check for proxy: %s (auth: %v)", proxyAddr, username != "")
56109

57-
// Measure latency start time
58-
startTime := time.Now()
59-
60110
if err := waitForTCPReady(proxyAddr, 2*time.Second); err != nil {
61111
return nil, fmt.Errorf("proxy not ready: %w", err)
62112
}
63113

114+
httpStartTime := time.Now()
115+
64116
// Build proxy URL with authentication if provided
65117
proxyURLStr := "http://"
66118
if username != "" && password != "" {
@@ -88,11 +140,7 @@ func CheckProxyIP(proxyAddr string, username string, password string) (*IPInfo,
88140
}
89141
defer transport.CloseIdleConnections()
90142

91-
// Use ip2location.io API (free tier, no API key needed for basic usage)
92-
services := []string{
93-
"https://api.ip2location.io/?format=json",
94-
"http://ip-api.com/json/",
95-
}
143+
services := ipCheckServiceURLs()
96144

97145
var lastErr error
98146
var httpErr error
@@ -101,7 +149,7 @@ func CheckProxyIP(proxyAddr string, username string, password string) (*IPInfo,
101149
info, err := checkWithService(client, service)
102150
if err == nil && info.IP != "" {
103151
// Calculate latency in milliseconds
104-
info.Latency = int(time.Since(startTime).Milliseconds())
152+
info.Latency = int(time.Since(httpStartTime).Milliseconds())
105153
info.Transport = "http"
106154
log.Printf("[IPCheck] Success! IP: %s, Location: %s, Latency: %dms", info.IP, info.Location, info.Latency)
107155
return info, nil
@@ -115,7 +163,8 @@ func CheckProxyIP(proxyAddr string, username string, password string) (*IPInfo,
115163

116164
// Try with SOCKS5 if HTTP fails
117165
log.Printf("[IPCheck] HTTP proxy failed (%v), trying SOCKS5...", httpErr)
118-
result, err := checkWithSOCKS5(proxyAddr, username, password, services, startTime)
166+
socksStartTime := time.Now()
167+
result, err := checkWithSOCKS5(proxyAddr, username, password, services, socksStartTime)
119168
if err == nil {
120169
result.Transport = "socks5"
121170
if httpErr != nil {
@@ -178,7 +227,14 @@ func checkWithSOCKS5(proxyAddr string, username string, password string, service
178227
}
179228

180229
func checkWithService(client *http.Client, serviceURL string) (*IPInfo, error) {
181-
resp, err := client.Get(serviceURL)
230+
req, err := http.NewRequest(http.MethodGet, serviceURL, nil)
231+
if err != nil {
232+
return nil, fmt.Errorf("failed to create request: %v", err)
233+
}
234+
req.Header.Set("User-Agent", "sb-proxy-manager/1.0")
235+
req.Header.Set("Accept", "application/json")
236+
237+
resp, err := client.Do(req)
182238
if err != nil {
183239
return nil, fmt.Errorf("request failed: %v", err)
184240
}
@@ -224,6 +280,8 @@ func checkWithService(client *http.Client, serviceURL string) (*IPInfo, error) {
224280
// ip2location.io uses "country_code", ip-api uses "countryCode"
225281
if countryCode, ok := result["country_code"].(string); ok {
226282
info.CountryCode = countryCode
283+
} else if countryCode, ok := result["cc"].(string); ok {
284+
info.CountryCode = countryCode
227285
} else if countryCode, ok := result["countryCode"].(string); ok {
228286
info.CountryCode = countryCode
229287
}
@@ -277,10 +335,7 @@ func CheckDirectIP() (*IPInfo, error) {
277335
Timeout: 10 * time.Second,
278336
}
279337

280-
services := []string{
281-
"https://api.ip2location.io/?format=json",
282-
"http://ip-api.com/json/",
283-
}
338+
services := ipCheckServiceURLs()
284339

285340
var lastErr error
286341
for _, service := range services {

backend/services/ipcheck_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package services
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestCheckWithService_SendsHeaders(t *testing.T) {
11+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12+
if r.Header.Get("User-Agent") != "sb-proxy-manager/1.0" {
13+
w.WriteHeader(http.StatusBadRequest)
14+
return
15+
}
16+
if !strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json") {
17+
w.WriteHeader(http.StatusBadRequest)
18+
return
19+
}
20+
w.Header().Set("Content-Type", "application/json")
21+
_, _ = w.Write([]byte(`{"ip":"1.2.3.4","country":"Testland","cc":"TL","city":"Test City","region":"Test Region"}`))
22+
}))
23+
t.Cleanup(srv.Close)
24+
25+
client := &http.Client{}
26+
info, err := checkWithService(client, srv.URL)
27+
if err != nil {
28+
t.Fatalf("checkWithService: %v", err)
29+
}
30+
if info.IP != "1.2.3.4" {
31+
t.Fatalf("unexpected ip: %q", info.IP)
32+
}
33+
}
34+
35+
func TestCheckWithService_ParsesCountryCodeCC(t *testing.T) {
36+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37+
w.Header().Set("Content-Type", "application/json")
38+
_, _ = w.Write([]byte(`{"ip":"1.2.3.4","country":"Testland","cc":"TL"}`))
39+
}))
40+
t.Cleanup(srv.Close)
41+
42+
client := &http.Client{}
43+
info, err := checkWithService(client, srv.URL)
44+
if err != nil {
45+
t.Fatalf("checkWithService: %v", err)
46+
}
47+
if info.CountryCode != "TL" {
48+
t.Fatalf("unexpected country code: %q", info.CountryCode)
49+
}
50+
}
51+
52+
func TestCheckDirectIP_UsesEnvServicesWithFallback(t *testing.T) {
53+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
54+
switch r.URL.Path {
55+
case "/fail":
56+
w.WriteHeader(http.StatusInternalServerError)
57+
case "/ok":
58+
w.Header().Set("Content-Type", "application/json")
59+
_, _ = w.Write([]byte(`{"ip":"1.2.3.4","country":"Testland","cc":"TL"}`))
60+
default:
61+
w.WriteHeader(http.StatusNotFound)
62+
}
63+
}))
64+
t.Cleanup(srv.Close)
65+
66+
t.Setenv("SBPM_IPCHECK_URLS", srv.URL+"/fail,"+srv.URL+"/ok")
67+
info, err := CheckDirectIP()
68+
if err != nil {
69+
t.Fatalf("CheckDirectIP: %v", err)
70+
}
71+
if info.IP != "1.2.3.4" {
72+
t.Fatalf("unexpected ip: %q", info.IP)
73+
}
74+
}

0 commit comments

Comments
 (0)