package ip import ( "context" "errors" "io" "net" "net/http" "net/netip" "strings" "sync" "time" ) const ( publicIPv4Endpoint = "https://api.ipify.org" publicIPv6Endpoint = "https://api6.ipify.org" publicIPv4Fallback = "https://ipv4.icanhazip.com" publicIPv6Fallback = "https://ipv6.icanhazip.com" maxIPResponseBytes = 64 ) var ( publicIPClient = &http.Client{Timeout: 3 * time.Second} cacheMu sync.Mutex cachedIPv4 string cachedIPv6 string ) func GetIp() string { value := GetIPv4() if value == "" { return "[Your Server IP]" } return value } func GetIPv4() string { return getPublicIP([]string{publicIPv4Endpoint, publicIPv4Fallback}, false) } func GetIPv6() string { return getPublicIP([]string{publicIPv6Endpoint, publicIPv6Fallback}, true) } func getPublicIP(endpoints []string, wantIPv6 bool) string { cacheMu.Lock() if wantIPv6 && cachedIPv6 != "" { value := cachedIPv6 cacheMu.Unlock() return value } if !wantIPv6 && cachedIPv4 != "" { value := cachedIPv4 cacheMu.Unlock() return value } cacheMu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() value, err := fetchPublicIPFromEndpoints(ctx, publicIPClient, endpoints, wantIPv6) if err != nil { return "" } cacheMu.Lock() defer cacheMu.Unlock() if wantIPv6 { if cachedIPv6 == "" { cachedIPv6 = value } return cachedIPv6 } if cachedIPv4 == "" { cachedIPv4 = value } 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 { return "", err } address, err := netip.ParseAddr(value) if err != nil || address.Is6() != wantIPv6 { return "", errors.New("public IP address family does not match") } return address.String(), nil } func fetchPublicIP(client *http.Client, request *http.Request) (string, error) { if client == nil || request == nil || request.URL == nil || request.URL.Scheme != "https" { return "", errors.New("invalid public IP request") } response, err := client.Do(request) if err != nil { return "", err } defer response.Body.Close() if response.StatusCode != http.StatusOK { return "", errors.New("public IP service returned an unexpected status") } body, err := io.ReadAll(io.LimitReader(response.Body, maxIPResponseBytes+1)) if err != nil { return "", err } if len(body) > maxIPResponseBytes { return "", errors.New("public IP response is too large") } value := strings.TrimSpace(string(body)) parsed, err := netip.ParseAddr(value) if err != nil || !isPublicIP(parsed) { return "", errors.New("public IP response is invalid") } return parsed.String(), nil } func isPublicIP(value netip.Addr) bool { return value.IsValid() && value.IsGlobalUnicast() && !value.IsPrivate() && !value.IsLoopback() && !value.IsLinkLocalUnicast() && !value.IsLinkLocalMulticast() && !value.IsMulticast() && !value.IsUnspecified() && !net.IP(value.AsSlice()).IsInterfaceLocalMulticast() }