156 lines
3.8 KiB
Go
156 lines
3.8 KiB
Go
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()
|
|
}
|