package ipaddr import ( "fmt" "log" "net" "os" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/lionsoul2014/ip2region/binding/golang/xdb" ) // XDBDetector 基于 xdb 的海外IP检测器 type XDBDetector struct { searcher *xdb.Searcher cache sync.Map cacheTTL time.Duration } // RegionInfo 区域信息 type RegionInfo struct { Country string `json:"country"` Region string `json:"region"` Province string `json:"province"` City string `json:"city"` ISP string `json:"isp"` IsOverseas bool `json:"is_overseas"` IsChinaMainland bool `json:"is_china_mainland"` } type IPVersion string const ( IPv4 IPVersion = "IPv4" IPv6 IPVersion = "IPv6" InvalidIP IPVersion = "Invalid" ) // GetIPVersion 获取IP版本 func GetIPVersion(ip string) IPVersion { parsedIP := net.ParseIP(ip) if parsedIP == nil { return InvalidIP } if parsedIP.To4() != nil { return IPv4 } return IPv6 } // IsIPv4 判断是否为IPv4 func IsIPv4(ip string) bool { return GetIPVersion(ip) == IPv4 } // NewXDBDetector 创建 xdb 检测器 func NewXDBDetectorV6(dbPath string) (*XDBDetector, error) { // 自动查找数据库文件 if dbPath == "" { dbPath = findXDBFile() if dbPath == "" { return nil, fmt.Errorf("未找到 ip2region.xdb 数据库文件") } } // 检查文件是否存在 if _, err := os.Stat(dbPath); os.IsNotExist(err) { return nil, fmt.Errorf("数据库文件不存在: %s", dbPath) } // 读取整个 xdb 文件到内存 cBuff, err := xdb.LoadContentFromFile(dbPath) if err != nil { return nil, fmt.Errorf("加载 xdb 内容失败: %v", err) } // 创建搜索器 searcher, err := xdb.NewWithBuffer(xdb.IPv6, cBuff) if err != nil { return nil, fmt.Errorf("创建搜索器失败: %v", err) } detector := &XDBDetector{ searcher: searcher, cacheTTL: 24 * time.Hour, } log.Printf("XDBDetector 初始化成功,数据库: %s", dbPath) return detector, nil } // NewXDBDetector 创建 xdb 检测器 func NewXDBDetectorV4(dbPath string) (*XDBDetector, error) { // 自动查找数据库文件 if dbPath == "" { dbPath = findXDBFile() if dbPath == "" { return nil, fmt.Errorf("未找到 ip2region.xdb 数据库文件") } } // 检查文件是否存在 if _, err := os.Stat(dbPath); os.IsNotExist(err) { return nil, fmt.Errorf("数据库文件不存在: %s", dbPath) } // 读取整个 xdb 文件到内存 cBuff, err := xdb.LoadContentFromFile(dbPath) if err != nil { return nil, fmt.Errorf("加载 xdb 内容失败: %v", err) } // 创建搜索器 searcher, err := xdb.NewWithBuffer(xdb.IPv4, cBuff) if err != nil { return nil, fmt.Errorf("创建搜索器失败: %v", err) } detector := &XDBDetector{ searcher: searcher, cacheTTL: 24 * time.Hour, } log.Printf("XDBDetector 初始化成功,数据库: %s", dbPath) return detector, nil } // Search 搜索IP信息 func (d *XDBDetector) Search(ip string) (string, error) { if d.searcher == nil { return "", fmt.Errorf("搜索器未初始化") } // 验证IP格式 if net.ParseIP(ip) == nil { return "", fmt.Errorf("无效的IP地址: %s", ip) } return d.searcher.SearchByStr(ip) } // SearchRegion 搜索并解析区域信息 func (d *XDBDetector) SearchRegion(ip string) (*RegionInfo, error) { regionStr, err := d.Search(ip) if err != nil { return nil, err } return d.parseRegionString(regionStr), nil } // parseRegionString 解析区域字符串 // 格式: 国家|区域|省份|城市|ISP func (d *XDBDetector) parseRegionString(regionStr string) *RegionInfo { parts := strings.Split(regionStr, "|") // 确保有足够的字段 for len(parts) < 5 { parts = append(parts, "") } country := strings.TrimSpace(parts[0]) region := strings.TrimSpace(parts[1]) province := strings.TrimSpace(parts[2]) city := strings.TrimSpace(parts[3]) isp := strings.TrimSpace(parts[4]) isChinaMainland := d.isChinaMainland(country) isOverseas := !isChinaMainland // 特别行政区处理 if !isChinaMainland && d.isChinaSpecialRegion(country) { // 可以根据业务需求调整特别行政区是否视为海外 isOverseas = false } return &RegionInfo{ Country: country, Region: region, Province: province, City: city, ISP: isp, IsOverseas: isOverseas, IsChinaMainland: isChinaMainland, } } // IsOverseasIP 判断是否为海外IP func (d *XDBDetector) IsOverseasIP(ip string) (bool, error) { // 检查缓存 if cached, ok := d.getFromCache(ip); ok { return cached, nil } // 内网IP默认为国内 if d.isPrivateIP(ip) { d.setCache(ip, false) return false, nil } regionInfo, err := d.SearchRegion(ip) if err != nil { return false, err } d.setCache(ip, regionInfo.IsOverseas) return regionInfo.IsOverseas, nil } // isChinaMainland 判断是否为中国大陆 func (d *XDBDetector) isChinaMainland(country string) bool { if country == "" { return false } chinaKeywords := []string{"中国", "China", "CN"} for _, keyword := range chinaKeywords { if country == keyword { return true } } lowerCountry := strings.ToLower(country) for _, keyword := range []string{"china", "cn", "中国"} { if strings.Contains(lowerCountry, keyword) { return true } } return false } // isChinaSpecialRegion 判断是否为中国特别行政区 func (d *XDBDetector) isChinaSpecialRegion(region string) bool { specialRegions := []string{ "台湾", "台湾省", "Taiwan", "TW", "香港", "Hong Kong", "HK", "澳门", "Macao", "Macau", "MO", } for _, keyword := range specialRegions { if region == keyword { return true } } return false } // BatchIsOverseas 批量判断海外IP func (d *XDBDetector) BatchIsOverseas(ips []string) (map[string]bool, error) { results := make(map[string]bool) for _, ip := range ips { isOverseas, err := d.IsOverseasIP(ip) if err != nil { // 失败时默认为海外(安全策略) results[ip] = true continue } results[ip] = isOverseas } return results, nil } // isPrivateIP 判断内网IP func (d *XDBDetector) isPrivateIP(ip string) bool { parsedIP := net.ParseIP(ip) if parsedIP == nil { return false } privateBlocks := []string{ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10", } for _, block := range privateBlocks { _, ipNet, err := net.ParseCIDR(block) if err == nil && ipNet.Contains(parsedIP) { return true } } return false } // 缓存管理 func (d *XDBDetector) getFromCache(ip string) (bool, bool) { if item, ok := d.cache.Load(ip); ok { if cacheItem, ok := item.(*cacheResult); ok { if time.Since(cacheItem.timestamp) < d.cacheTTL { return cacheItem.isOverseas, true } d.cache.Delete(ip) } } return false, false } func (d *XDBDetector) setCache(ip string, isOverseas bool) { d.cache.Store(ip, &cacheResult{ isOverseas: isOverseas, timestamp: time.Now(), }) } type cacheResult struct { isOverseas bool timestamp time.Time } // Close 关闭检测器 func (d *XDBDetector) Close() { if d.searcher != nil { d.searcher.Close() } } // findXDBFile 查找 xdb 文件 func findXDBFile() string { paths := []string{ "ip2region.xdb", "data/ip2region.xdb", "./ip2region.xdb", "../ip2region.xdb", "/usr/local/share/ip2region/ip2region.xdb", } for _, path := range paths { if _, err := os.Stat(path); err == nil { return path } } return "" } // ResolveClientIP 解析客户端 IP,用于 m3u8 限流、token 签发与 ipstat 统计。 func ResolveClientIP(c *gin.Context) string { if c == nil { return "" } remoteIP := normalizeIPString(c.RemoteIP()) clientIP := normalizeIPString(c.ClientIP()) if shouldTrustForwardedHeaders(remoteIP, clientIP) { if ip := firstPublicIPFromHeader(c.GetHeader("X-Forwarded-For")); ip != "" { return ip } if ip := normalizeIPString(c.GetHeader("X-Real-IP")); ip != "" && !isPrivateOrLoopbackIP(ip) { return ip } } if clientIP != "" && !isPrivateOrLoopbackIP(clientIP) { return clientIP } if clientIP != "" { return clientIP } return remoteIP } func shouldTrustForwardedHeaders(remoteIP, clientIP string) bool { if isPrivateOrLoopbackIP(remoteIP) || isLocalMachineIP(remoteIP) { return true } return clientIP != "" && remoteIP != "" && clientIP != remoteIP } // isLocalMachineIP 判断对端是否本机网卡地址。 // H5 与 1043 一样用本机公网 IP 回源 app,此时对端不是 127.0.0.1,但仍应信任 X-Real-IP。 func isLocalMachineIP(ipStr string) bool { ip := net.ParseIP(ipStr) if ip == nil { return false } addrs, err := net.InterfaceAddrs() if err != nil { return false } for _, addr := range addrs { var candidate net.IP switch v := addr.(type) { case *net.IPNet: candidate = v.IP case *net.IPAddr: candidate = v.IP } if candidate != nil && candidate.Equal(ip) { return true } } return false } func normalizeIPString(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } ip := net.ParseIP(raw) if ip == nil { return raw } if v4 := ip.To4(); v4 != nil { return v4.String() } return ip.String() } func firstPublicIPFromHeader(xff string) string { xff = strings.TrimSpace(xff) if xff == "" { return "" } for _, part := range strings.Split(xff, ",") { ip := normalizeIPString(strings.TrimSpace(part)) if ip != "" && !isPrivateOrLoopbackIP(ip) { return ip } } return "" } func isPrivateOrLoopbackIP(ipStr string) bool { ip := net.ParseIP(ipStr) if ip == nil { return false } return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() }