Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
413 lines
8.9 KiB
Go
413 lines
8.9 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) {
|
|
return true
|
|
}
|
|
return clientIP != "" && remoteIP != "" && clientIP != remoteIP
|
|
}
|
|
|
|
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()
|
|
}
|