From b51518630a2155d74b80273e431442a460dfffea Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 20 Sep 2026 02:02:46 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=AC=E7=BD=91=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E6=8E=A2=E6=B5=8B=E5=8D=95=E7=82=B9=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=B9=B6=E9=99=90=E5=88=B6=E5=B9=B6=E5=8F=91=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=80=BB=E8=80=97=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/utils/ip/ip.go | 45 ++++++++++++++++++++++++++++++------- backend/utils/ip/ip_test.go | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/backend/utils/ip/ip.go b/backend/utils/ip/ip.go index b67f280..b39a029 100644 --- a/backend/utils/ip/ip.go +++ b/backend/utils/ip/ip.go @@ -1,6 +1,7 @@ package ip import ( + "context" "errors" "io" "net" @@ -14,6 +15,8 @@ import ( const ( publicIPv4Endpoint = "https://api.ipify.org" publicIPv6Endpoint = "https://api6.ipify.org" + publicIPv4Fallback = "https://ipv4.icanhazip.com" + publicIPv6Fallback = "https://ipv6.icanhazip.com" maxIPResponseBytes = 64 ) @@ -33,14 +36,14 @@ func GetIp() string { } func GetIPv4() string { - return getPublicIP(publicIPv4Endpoint, false) + return getPublicIP([]string{publicIPv4Endpoint, publicIPv4Fallback}, false) } func GetIPv6() string { - return getPublicIP(publicIPv6Endpoint, true) + return getPublicIP([]string{publicIPv6Endpoint, publicIPv6Fallback}, true) } -func getPublicIP(endpoint string, wantIPv6 bool) string { +func getPublicIP(endpoints []string, wantIPv6 bool) string { cacheMu.Lock() if wantIPv6 && cachedIPv6 != "" { value := cachedIPv6 @@ -54,11 +57,9 @@ func getPublicIP(endpoint string, wantIPv6 bool) string { } cacheMu.Unlock() - request, err := http.NewRequest(http.MethodGet, endpoint, nil) - if err != nil { - return "" - } - value, err := fetchPublicIPForFamily(publicIPClient, request, wantIPv6) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + value, err := fetchPublicIPFromEndpoints(ctx, publicIPClient, endpoints, wantIPv6) if err != nil { return "" } @@ -77,6 +78,34 @@ func getPublicIP(endpoint string, wantIPv6 bool) string { return cachedIPv4 } +// 多个 HTTPS 探测共享总超时,先返回有效公网地址,并取消其余请求。 +func fetchPublicIPFromEndpoints(ctx context.Context, client *http.Client, endpoints []string, wantIPv6 bool) (string, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + results := make(chan string, len(endpoints)) + for _, endpoint := range endpoints { + go func(endpoint string) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + value := "" + if err == nil { + value, _ = fetchPublicIPForFamily(client, request, wantIPv6) + } + results <- value + }(endpoint) + } + for range endpoints { + select { + case value := <-results: + if value != "" { + return value, nil + } + case <-ctx.Done(): + return "", ctx.Err() + } + } + return "", errors.New("public IP services are unavailable") +} + func fetchPublicIPForFamily(client *http.Client, request *http.Request, wantIPv6 bool) (string, error) { value, err := fetchPublicIP(client, request) if err != nil { diff --git a/backend/utils/ip/ip_test.go b/backend/utils/ip/ip_test.go index 960b839..27b6571 100644 --- a/backend/utils/ip/ip_test.go +++ b/backend/utils/ip/ip_test.go @@ -1,6 +1,7 @@ package ip import ( + "context" "net/http" "net/http/httptest" "strings" @@ -8,6 +9,50 @@ import ( "time" ) +func TestPublicIPFallbackCancelsUnavailableService(t *testing.T) { + started := make(chan struct{}) + cancelled := make(chan struct{}) + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/slow" { + close(started) + <-r.Context().Done() + close(cancelled) + return + } + <-started + _, _ = w.Write([]byte("8.8.4.4\n")) + })) + defer server.Close() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + value, err := fetchPublicIPFromEndpoints(ctx, server.Client(), []string{server.URL + "/slow", server.URL + "/fallback"}, false) + if err != nil || value != "8.8.4.4" { + t.Fatalf("fallback address = %q, err = %v", value, err) + } + select { + case <-cancelled: + case <-ctx.Done(): + t.Fatal("unused public IP request was not cancelled") + } +} + +func TestPublicIPFallbackRejectsPrivateAddressAndHonorsDeadline(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/private" { + _, _ = w.Write([]byte("192.168.1.10")) + return + } + <-r.Context().Done() + })) + defer server.Close() + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + value, err := fetchPublicIPFromEndpoints(ctx, server.Client(), []string{server.URL + "/private", server.URL + "/slow"}, false) + if err == nil || value != "" { + t.Fatalf("invalid fallback address = %q, err = %v", value, err) + } +} + func TestFetchPublicIPAcceptsPublicAddress(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("8.8.8.8\n"))