Files
2026-10-27 16:44:55 +08:00

439 lines
9.5 KiB
Go

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()
}