Smail/server/utils/ip/ip.go
smail dd04bdb49e
Some checks are pending
Security Checks / security (push) Waiting to run
完成安全整改、配置重构与邮件认证优化
2026-08-28 14:33:13 +08:00

127 lines
2.8 KiB
Go

package ip
import (
"errors"
"io"
"net"
"net/http"
"net/netip"
"strings"
"sync"
"time"
)
const (
publicIPv4Endpoint = "https://api.ipify.org"
publicIPv6Endpoint = "https://api6.ipify.org"
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(publicIPv4Endpoint, false)
}
func GetIPv6() string {
return getPublicIP(publicIPv6Endpoint, true)
}
func getPublicIP(endpoint 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()
request, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return ""
}
value, err := fetchPublicIPForFamily(publicIPClient, request, wantIPv6)
if err != nil {
return ""
}
cacheMu.Lock()
defer cacheMu.Unlock()
if wantIPv6 {
if cachedIPv6 == "" {
cachedIPv6 = value
}
return cachedIPv6
}
if cachedIPv4 == "" {
cachedIPv4 = value
}
return cachedIPv4
}
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()
}