diff --git a/api/entrance.go b/api/entrance.go index fd03032..4371224 100644 --- a/api/entrance.go +++ b/api/entrance.go @@ -53,6 +53,7 @@ type APIGroup struct { WafFirewallIPBlockApi WafPluginApi WafLogFileWriteApi + WafIPLocationApi } var APIGroupAPP = new(APIGroup) diff --git a/api/waf_iplocation.go b/api/waf_iplocation.go new file mode 100644 index 0000000..e681af1 --- /dev/null +++ b/api/waf_iplocation.go @@ -0,0 +1,271 @@ +package api + +import ( + "SamWaf/global" + "SamWaf/iplocation" + "SamWaf/model/common/response" + "SamWaf/utils" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/gin-gonic/gin" +) + +type WafIPLocationApi struct { +} + +// GetIPDBStatusApi 获取 IP 数据库状态 +func (w *WafIPLocationApi) GetIPDBStatusApi(c *gin.Context) { + if global.GIPLOCATION_MANAGER == nil { + response.FailWithMessage("IP 数据库管理器未初始化", c) + return + } + + status := global.GIPLOCATION_MANAGER.GetStatus() + + // 获取文件的实际创建时间 + dataDir := filepath.Join(utils.GetCurrentDir(), "data") + + // 获取 IPv4 文件创建时间 + var ipv4FilePath string + if status.IPv4Source == "ip2region" { + ipv4FilePath = filepath.Join(dataDir, "ip2region.xdb") + } else if status.IPv4Source == "geolite2" { + ipv4FilePath = filepath.Join(dataDir, "GeoLite2-Country.mmdb") + } + if ipv4FilePath != "" { + if fileInfo, err := os.Stat(ipv4FilePath); err == nil { + status.IPv4CreateTime = fileInfo.ModTime().Format("2006-01-02 15:04:05") + } + } + + // 获取 IPv6 文件创建时间 + var ipv6FilePath string + if status.IPv6Source == "ip2region" { + ipv6FilePath = filepath.Join(dataDir, "ip2region_v6.xdb") + } else if status.IPv6Source == "geolite2" { + ipv6FilePath = filepath.Join(dataDir, "GeoLite2-Country.mmdb") + } + if ipv6FilePath != "" { + if fileInfo, err := os.Stat(ipv6FilePath); err == nil { + status.IPv6CreateTime = fileInfo.ModTime().Format("2006-01-02 15:04:05") + } + } + + response.OkWithDetailed(status, "获取成功", c) +} + +// UploadIPDBFileApi 上传 IP 数据库文件 +func (w *WafIPLocationApi) UploadIPDBFileApi(c *gin.Context) { + // 获取文件类型 (ipv4/ipv6) + ipType := c.PostForm("type") + if ipType != "ipv4" && ipType != "ipv6" { + response.FailWithMessage("无效的类型参数,必须是 ipv4 或 ipv6", c) + return + } + + // 获取上传的文件 + file, err := c.FormFile("file") + if err != nil { + response.FailWithMessage("文件上传失败: "+err.Error(), c) + return + } + + // 检查文件扩展名 + ext := filepath.Ext(file.Filename) + if ext != ".xdb" && ext != ".mmdb" { + response.FailWithMessage("不支持的文件类型,仅支持 .xdb 和 .mmdb 文件", c) + return + } + + // 确定保存路径 + var finalPath string + dataDir := filepath.Join(utils.GetCurrentDir(), "data") + + // 确保 data 目录存在 + if _, err := os.Stat(dataDir); os.IsNotExist(err) { + err = os.MkdirAll(dataDir, 0755) + if err != nil { + response.FailWithMessage("创建数据目录失败: "+err.Error(), c) + return + } + } + + if ipType == "ipv4" { + if ext == ".xdb" { + finalPath = filepath.Join(dataDir, "ip2region.xdb") + } else { + finalPath = filepath.Join(dataDir, "GeoLite2-Country.mmdb") + } + } else { + if ext == ".xdb" { + finalPath = filepath.Join(dataDir, "ip2region_v6.xdb") + } else { + finalPath = filepath.Join(dataDir, "GeoLite2-Country.mmdb") + } + } + + // 先保存到临时文件 + tempPath := finalPath + ".tmp" + err = c.SaveUploadedFile(file, tempPath) + if err != nil { + response.FailWithMessage("保存临时文件失败: "+err.Error(), c) + return + } + + // 读取临时文件内容 + fileData, err := ioutil.ReadFile(tempPath) + if err != nil { + os.Remove(tempPath) // 清理临时文件 + response.FailWithMessage("读取文件失败: "+err.Error(), c) + return + } + + // 先热加载到内存,验证文件有效性 + if global.GIPLOCATION_MANAGER != nil { + var reloadErr error + + if ipType == "ipv4" { + if ext == ".xdb" { + reloadErr = global.GIPLOCATION_MANAGER.LoadV4Ip2Region(fileData, iplocation.DBFormat(global.GCONFIG_IP_V4_FORMAT)) + if reloadErr == nil { + global.GCONFIG_IP_V4_SOURCE = "ip2region" + } + } else { + reloadErr = global.GIPLOCATION_MANAGER.LoadV4GeoLite2(fileData) + if reloadErr == nil { + global.GCONFIG_IP_V4_SOURCE = "geolite2" + } + } + } else { + if ext == ".xdb" { + reloadErr = global.GIPLOCATION_MANAGER.LoadV6Ip2Region(fileData, iplocation.DBFormat(global.GCONFIG_IP_V6_FORMAT)) + if reloadErr == nil { + global.GCONFIG_IP_V6_SOURCE = "ip2region" + } + } else { + reloadErr = global.GIPLOCATION_MANAGER.LoadV6GeoLite2(fileData) + if reloadErr == nil { + global.GCONFIG_IP_V6_SOURCE = "geolite2" + } + } + } + + if reloadErr != nil { + os.Remove(tempPath) // 清理临时文件 + response.FailWithMessage("加载数据库失败: "+reloadErr.Error(), c) + return + } + } + + // 热加载成功后,原子替换正式文件 + err = os.Rename(tempPath, finalPath) + if err != nil { + os.Remove(tempPath) // 清理临时文件 + response.FailWithMessage("替换文件失败: "+err.Error(), c) + return + } + + response.OkWithMessage("文件上传成功并已重新加载", c) +} + +// ReloadIPDBApi 重新加载 IP 数据库 +func (w *WafIPLocationApi) ReloadIPDBApi(c *gin.Context) { + if global.GIPLOCATION_MANAGER == nil { + response.FailWithMessage("IP 数据库管理器未初始化", c) + return + } + + dataDir := filepath.Join(utils.GetCurrentDir(), "data") + + // 重新加载 IPv4 + if global.GCONFIG_IP_V4_SOURCE == "ip2region" { + ipv4Path := filepath.Join(dataDir, "ip2region.xdb") + if _, err := os.Stat(ipv4Path); err == nil { + data, err := ioutil.ReadFile(ipv4Path) + if err == nil { + err = global.GIPLOCATION_MANAGER.LoadV4Ip2Region(data, iplocation.DBFormat(global.GCONFIG_IP_V4_FORMAT)) + if err != nil { + response.FailWithMessage("重新加载 IPv4 数据库失败: "+err.Error(), c) + return + } + } + } + } else if global.GCONFIG_IP_V4_SOURCE == "geolite2" { + ipv4Path := filepath.Join(dataDir, "GeoLite2-Country.mmdb") + if _, err := os.Stat(ipv4Path); err == nil { + data, err := ioutil.ReadFile(ipv4Path) + if err == nil { + err = global.GIPLOCATION_MANAGER.LoadV4GeoLite2(data) + if err != nil { + response.FailWithMessage("重新加载 IPv4 数据库失败: "+err.Error(), c) + return + } + } + } + } + + // 重新加载 IPv6 + if global.GCONFIG_IP_V6_SOURCE == "ip2region" { + ipv6Path := filepath.Join(dataDir, "ip2region_v6.xdb") + if _, err := os.Stat(ipv6Path); err == nil { + data, err := ioutil.ReadFile(ipv6Path) + if err == nil { + err = global.GIPLOCATION_MANAGER.LoadV6Ip2Region(data, iplocation.DBFormat(global.GCONFIG_IP_V6_FORMAT)) + if err != nil { + response.FailWithMessage("重新加载 IPv6 数据库失败: "+err.Error(), c) + return + } + } + } + } else if global.GCONFIG_IP_V6_SOURCE == "geolite2" { + ipv6Path := filepath.Join(dataDir, "GeoLite2-Country.mmdb") + if _, err := os.Stat(ipv6Path); err == nil { + data, err := ioutil.ReadFile(ipv6Path) + if err == nil { + err = global.GIPLOCATION_MANAGER.LoadV6GeoLite2(data) + if err != nil { + response.FailWithMessage("重新加载 IPv6 数据库失败: "+err.Error(), c) + return + } + } + } + } + + response.OkWithMessage("数据库重新加载成功", c) +} + +// TestIPLookupApi 测试 IP 查询 +func (w *WafIPLocationApi) TestIPLookupApi(c *gin.Context) { + var req struct { + IP string `json:"ip" binding:"required"` + } + + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage("参数解析失败", c) + return + } + + if global.GIPLOCATION_MANAGER == nil { + response.FailWithMessage("IP 数据库管理器未初始化", c) + return + } + + result := global.GIPLOCATION_MANAGER.Lookup(req.IP) + + resp := map[string]interface{}{ + "ip": req.IP, + "country": result.Country, + "province": result.Province, + "city": result.City, + "isp": result.ISP, + "region": result.Region, + "district": result.District, + "raw": fmt.Sprintf("%v", result.ToSlice()), + } + + response.OkWithDetailed(resp, "查询成功", c) +} diff --git a/cmd/samwaf/main.go b/cmd/samwaf/main.go index cb522cf..b86b9fe 100644 --- a/cmd/samwaf/main.go +++ b/cmd/samwaf/main.go @@ -7,6 +7,7 @@ import ( "SamWaf/enums" "SamWaf/global" "SamWaf/globalobj" + "SamWaf/iplocation" "SamWaf/model" "SamWaf/model/wafenginmodel" "SamWaf/plugin" @@ -149,38 +150,83 @@ func (m *wafSystenService) run() { zlog.Info("执行位置:", executablePath) global.GWAF_RUNTIME_CURRENT_EXEPATH = executablePath //初始化步骤[加载ip数据库] - // 从嵌入的文件中读取内容 + // 创建 IP Location Manager + global.GIPLOCATION_MANAGER = iplocation.NewManager() - // 拼接文件路径 + // 加载 IPv4 数据库 ip2RegionFilePath := filepath.Join(utils.GetCurrentDir(), "data", "ip2region.xdb") - // 检查文件是否存在 + var ipv4Data []byte if _, err := os.Stat(ip2RegionFilePath); os.IsNotExist(err) { - global.GCACHE_IP_CBUFF = Ip2regionBytes + // 使用内置数据 + ipv4Data = Ip2regionBytes + zlog.Info("Using embedded IPv4 database, size: ", len(ipv4Data)) } else { - // 读取文件内容 + // 读取外部文件 fileBytes, err := ioutil.ReadFile(ip2RegionFilePath) if err != nil { log.Fatalf("Failed to read IP database file ip2region.xdb: %v", err) } - global.GCACHE_IP_CBUFF = fileBytes - // 检查是否成功读取 - zlog.Info("IP database ip2region.xdb loaded into cache, size: ", len(global.GCACHE_IP_CBUFF), ip2RegionFilePath) + ipv4Data = fileBytes + zlog.Info("IPv4 database ip2region.xdb loaded from file, size: ", len(ipv4Data), ip2RegionFilePath) } - //检测是否存在IPV6得数据包 - ipv6RegionFilePath := filepath.Join(utils.GetCurrentDir(), "data", "GeoLite2-Country.mmdb") - // 检查文件是否存在 - if _, err := os.Stat(ipv6RegionFilePath); os.IsNotExist(err) { - global.GCACHE_IP_V6_COUNTRY_CBUFF = Ipv6CountryBytes - } else { - // 读取文件内容 - fileBytes, err := ioutil.ReadFile(ipv6RegionFilePath) + // 根据配置加载 IPv4 数据库 + if global.GCONFIG_IP_V4_SOURCE == "ip2region" { + err := global.GIPLOCATION_MANAGER.LoadV4Ip2Region(ipv4Data, iplocation.DBFormat(global.GCONFIG_IP_V4_FORMAT)) + if err != nil { + log.Fatalf("Failed to load IPv4 ip2region database: %v", err) + } + zlog.Info("IPv4 ip2region database loaded successfully") + } else if global.GCONFIG_IP_V4_SOURCE == "geolite2" { + err := global.GIPLOCATION_MANAGER.LoadV4GeoLite2(ipv4Data) + if err != nil { + log.Fatalf("Failed to load IPv4 GeoLite2 database: %v", err) + } + zlog.Info("IPv4 GeoLite2 database loaded successfully") + } + + // 加载 IPv6 数据库 + if global.GCONFIG_IP_V6_SOURCE == "ip2region" { + // IPv6 ip2region 需要单独的文件 + ipv6Ip2RegionPath := filepath.Join(utils.GetCurrentDir(), "data", "ip2region_v6.xdb") + if _, err := os.Stat(ipv6Ip2RegionPath); err == nil { + fileBytes, err := ioutil.ReadFile(ipv6Ip2RegionPath) + if err != nil { + zlog.Warn("Failed to read IPv6 ip2region database file: ", err) + } else { + err = global.GIPLOCATION_MANAGER.LoadV6Ip2Region(fileBytes, iplocation.DBFormat(global.GCONFIG_IP_V6_FORMAT)) + if err != nil { + zlog.Warn("Failed to load IPv6 ip2region database: ", err) + } else { + zlog.Info("IPv6 ip2region database loaded successfully, size: ", len(fileBytes)) + } + } + } else { + zlog.Warn("IPv6 ip2region database file not found, please upload ip2region_v6.xdb") + } + } else if global.GCONFIG_IP_V6_SOURCE == "geolite2" { + // IPv6 GeoLite2 + ipv6GeoLitePath := filepath.Join(utils.GetCurrentDir(), "data", "GeoLite2-Country.mmdb") + var ipv6Data []byte + if _, err := os.Stat(ipv6GeoLitePath); os.IsNotExist(err) { + // 使用内置数据 + ipv6Data = Ipv6CountryBytes + zlog.Info("Using embedded IPv6 GeoLite2 database, size: ", len(ipv6Data)) + } else { + // 读取外部文件 + fileBytes, err := ioutil.ReadFile(ipv6GeoLitePath) + if err != nil { + log.Fatalf("Failed to read IPv6 GeoLite2 database file: %v", err) + } + ipv6Data = fileBytes + zlog.Info("IPv6 GeoLite2 database loaded from file, size: ", len(ipv6Data), ipv6GeoLitePath) + } + + err := global.GIPLOCATION_MANAGER.LoadV6GeoLite2(ipv6Data) if err != nil { - log.Fatalf("Failed to read IPv6 database file GeoLite2-Country.mmdb: %v", err) + log.Fatalf("Failed to load IPv6 GeoLite2 database: %v", err) } - global.GCACHE_IP_V6_COUNTRY_CBUFF = fileBytes - // 检查是否成功读取 - zlog.Info("IPv6 database file GeoLite2-Country.mmdb loaded into cache, size: ", len(global.GCACHE_IP_V6_COUNTRY_CBUFF), ipv6RegionFilePath) + zlog.Info("IPv6 GeoLite2 database loaded successfully") } global.GWAF_DLP_CONFIG = ldpConfig global.GWAF_REG_PUBLIC_KEY = publicKey diff --git a/global/global.go b/global/global.go index 8a9fd99..c19a002 100644 --- a/global/global.go +++ b/global/global.go @@ -4,6 +4,7 @@ import ( "SamWaf/cache" "SamWaf/common/gwebsocket" "SamWaf/common/queue" + "SamWaf/iplocation" "SamWaf/model" "SamWaf/model/spec" "SamWaf/wafnotify" @@ -119,10 +120,19 @@ var ( GWAF_HTTP_SENSITIVE_REPLACE_STRING = "**" //HTTP 敏感内容替换成 /*********IP相关**************/ - GCACHE_IP_CBUFF []byte // IP相关缓存 - GCACHE_IP_V6_COUNTRY_CBUFF []byte // IPv6国家相关缓存 - GCACHE_IPV4_SEARCHER *xdb.Searcher //IPV4得查询器 - GCACHE_IPV6_SEARCHER *geoip2.Reader // IPV6得查询器 + GCACHE_IP_CBUFF []byte // IP相关缓存 (已废弃,由 GIPLOCATION_MANAGER 管理) + GCACHE_IP_V6_COUNTRY_CBUFF []byte // IPv6国家相关缓存 (已废弃,由 GIPLOCATION_MANAGER 管理) + GCACHE_IPV4_SEARCHER *xdb.Searcher //IPV4得查询器 (已废弃,由 GIPLOCATION_MANAGER 管理) + GCACHE_IPV6_SEARCHER *geoip2.Reader // IPV6得查询器 (已废弃,由 GIPLOCATION_MANAGER 管理) + + // IP 数据库配置 + GCONFIG_IP_V4_SOURCE string = "ip2region" // IPv4 数据来源: ip2region / geolite2 + GCONFIG_IP_V6_SOURCE string = "geolite2" // IPv6 数据来源: ip2region / geolite2 + GCONFIG_IP_V4_FORMAT string = "legacy" // IPv4 xdb 字段格式 + GCONFIG_IP_V6_FORMAT string = "legacy" // IPv6 xdb 字段格式(仅 ip2region 时有效) + + // IP Location Manager 全局实例 + GIPLOCATION_MANAGER *iplocation.Manager GDATA_DELETE_INTERVAL int64 = 180 // 删除180天前的数据 diff --git a/go.mod b/go.mod index 742e870..0295821 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/hashicorp/go-plugin v1.7.0 github.com/hyperjumptech/grule-rule-engine v1.15.0 github.com/kardianos/service v1.2.2 - github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20220907060842-b2ba5d58e48d + github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260202110255-d427b4dcd879 github.com/oschwald/geoip2-golang v1.11.0 github.com/pires/go-proxyproto v0.8.1 github.com/pquerna/otp v1.5.0 @@ -111,6 +111,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.69 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect diff --git a/go.sum b/go.sum index 4ff7c3b..e2e2b1a 100644 --- a/go.sum +++ b/go.sum @@ -268,6 +268,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20220907060842-b2ba5d58e48d h1:E7Qr3vakQIk9KTpn6m4yhsEhOdo4g51EqE4cSMy2LCw= github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20220907060842-b2ba5d58e48d/go.mod h1:bChUKvbKVC3zL/lLLIcu6alhQaL8uWD/DA+jRdyggdI= +github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260202110255-d427b4dcd879 h1:ouxvoYN6WL482nK1zS1IplyniAut3G4D+0N5JS0YTuw= +github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260202110255-d427b4dcd879/go.mod h1:+mNMTBuDMdEGhWzoQgc6kBdqeaQpWh5ba8zqmp2MxCU= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magefile/mage v1.15.1-0.20231118170541-2385abb49a1f h1:iiLWLoibjCL0XND6inF7bs2nc20lU/FYkiR//VIOLUc= @@ -286,6 +288,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/iplocation/format.go b/iplocation/format.go new file mode 100644 index 0000000..b738d61 --- /dev/null +++ b/iplocation/format.go @@ -0,0 +1,84 @@ +package iplocation + +import "strings" + +// ParseRegion 根据 DBFormat 将 "|" 分隔的 region 字符串解析为统一的 IPLocationResult +func ParseRegion(region string, format DBFormat) *IPLocationResult { + if region == "" { + return &IPLocationResult{} + } + + fields := strings.Split(region, "|") + + // 辅助函数:安全获取字段,处理 "0" 和空值 + getField := func(index int) string { + if index >= len(fields) { + return "" + } + val := strings.TrimSpace(fields[index]) + if val == "0" || val == "" { + return "" + } + return val + } + + result := &IPLocationResult{} + + switch format { + case FormatLegacy: // 国家|区域|省份|城市|ISP + result.Country = getField(0) + result.Region = getField(1) + result.Province = getField(2) + result.City = getField(3) + result.ISP = getField(4) + + case FormatOpenSource: // 国家|省份|城市|网络运营商 + result.Country = getField(0) + result.Province = getField(1) + result.City = getField(2) + result.ISP = getField(3) + + case FormatFull: // 大洲|国家|省份|城市|区县|网络运营商|其他 + result.Region = getField(0) + result.Country = getField(1) + result.Province = getField(2) + result.City = getField(3) + result.District = getField(4) + result.ISP = getField(5) + + case FormatStandard: // 国家|省份|城市|区县|网络运营商|其他 + result.Country = getField(0) + result.Province = getField(1) + result.City = getField(2) + result.District = getField(3) + result.ISP = getField(4) + + case FormatCompact: // 国家|省份|城市|网络运营商|其他 + result.Country = getField(0) + result.Province = getField(1) + result.City = getField(2) + result.ISP = getField(3) + + default: + // 默认按 legacy 格式处理 + result.Country = getField(0) + result.Region = getField(1) + result.Province = getField(2) + result.City = getField(3) + result.ISP = getField(4) + } + + // 处理特殊标记 "内网IP" + if len(fields) > 0 { + lastField := fields[len(fields)-1] + if strings.Contains(lastField, "内网IP") { + result.Country = "内网" + result.Province = "内网" + result.City = "内网" + result.Region = "" + result.ISP = "" + } + } + + return result +} diff --git a/iplocation/manager.go b/iplocation/manager.go new file mode 100644 index 0000000..24a6283 --- /dev/null +++ b/iplocation/manager.go @@ -0,0 +1,329 @@ +package iplocation + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/lionsoul2014/ip2region/binding/golang/xdb" + "github.com/oschwald/geoip2-golang" +) + +// Manager IP 地理位置查询管理器 +type Manager struct { + mu sync.RWMutex + + // IPv4 后端 + v4Source DBSource + v4Format DBFormat + v4Searcher *xdb.Searcher // ip2region IPv4 (buffer 模式并发安全) + v4GeoReader *geoip2.Reader // GeoLite2 (备选) + v4LoadTime time.Time + v4FileSize int64 + v4CreateTime time.Time // 文件创建时间 + + // IPv6 后端 + v6Source DBSource + v6Format DBFormat + v6Searcher *xdb.Searcher // ip2region IPv6 + v6GeoReader *geoip2.Reader // GeoLite2-Country (默认) + v6LoadTime time.Time + v6FileSize int64 + v6CreateTime time.Time // 文件创建时间 +} + +// NewManager 创建新的 IP 地理位置查询管理器 +func NewManager() *Manager { + return &Manager{ + v4Source: SourceIp2Region, + v4Format: FormatLegacy, + v6Source: SourceGeoLite2, + v6Format: FormatLegacy, + } +} + +// Lookup 查询 IP 地理位置信息,自动判断 IPv4/IPv6 +func (m *Manager) Lookup(ipStr string) *IPLocationResult { + m.mu.RLock() + defer m.mu.RUnlock() + + // 判断是 IPv4 还是 IPv6 + ip := net.ParseIP(ipStr) + if ip == nil { + return &IPLocationResult{Country: "无效IP"} + } + + // 判断 IP 类型 + if ip.To4() != nil { + // IPv4 + return m.lookupV4(ipStr) + } else { + // IPv6 + return m.lookupV6(ipStr) + } +} + +// lookupV4 查询 IPv4 地址 +func (m *Manager) lookupV4(ipStr string) *IPLocationResult { + if m.v4Source == SourceIp2Region && m.v4Searcher != nil { + region, err := m.v4Searcher.SearchByStr(ipStr) + if err != nil { + return &IPLocationResult{Country: "查询失败"} + } + if region == "" { + return &IPLocationResult{Country: "未知"} + } + return ParseRegion(region, m.v4Format) + } else if m.v4Source == SourceGeoLite2 && m.v4GeoReader != nil { + ip := net.ParseIP(ipStr) + record, err := m.v4GeoReader.Country(ip) + if err != nil { + return &IPLocationResult{Country: "查询失败"} + } + countryName := record.Country.Names["zh-CN"] + if countryName == "" { + countryName = record.Country.Names["en"] + } + return &IPLocationResult{Country: countryName} + } + + return &IPLocationResult{Country: "未配置"} +} + +// lookupV6 查询 IPv6 地址 +func (m *Manager) lookupV6(ipStr string) *IPLocationResult { + if m.v6Source == SourceIp2Region && m.v6Searcher != nil { + region, err := m.v6Searcher.SearchByStr(ipStr) + if err != nil { + return &IPLocationResult{Country: "查询失败"} + } + if region == "" { + return &IPLocationResult{Country: "未知"} + } + return ParseRegion(region, m.v6Format) + } else if m.v6Source == SourceGeoLite2 && m.v6GeoReader != nil { + ip := net.ParseIP(ipStr) + record, err := m.v6GeoReader.Country(ip) + if err != nil { + return &IPLocationResult{Country: "查询失败"} + } + countryName := record.Country.Names["zh-CN"] + if countryName == "" { + countryName = record.Country.Names["en"] + } + if countryName == "" { + countryName = "内网" + } + return &IPLocationResult{Country: countryName} + } + + return &IPLocationResult{Country: "未配置"} +} + +// LoadV4Ip2Region 加载 IPv4 ip2region 数据库 +func (m *Manager) LoadV4Ip2Region(data []byte, format DBFormat) error { + m.mu.Lock() + defer m.mu.Unlock() + + // 关闭旧的 searcher + if m.v4Searcher != nil { + m.v4Searcher.Close() + m.v4Searcher = nil + } + + // 创建新的 searcher (buffer 模式并发安全) + searcher, err := xdb.NewWithBuffer(xdb.IPv4, data) + if err != nil { + return fmt.Errorf("创建 IPv4 searcher 失败: %w", err) + } + + m.v4Searcher = searcher + m.v4Source = SourceIp2Region + m.v4Format = format + m.v4LoadTime = time.Now() + m.v4FileSize = int64(len(data)) + m.v4CreateTime = time.Now() // 记录创建时间 + + return nil +} + +// LoadV4GeoLite2 加载 IPv4 GeoLite2 数据库 +func (m *Manager) LoadV4GeoLite2(data []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + + // 关闭旧的 reader + if m.v4GeoReader != nil { + m.v4GeoReader.Close() + m.v4GeoReader = nil + } + + // 创建新的 reader + reader, err := geoip2.FromBytes(data) + if err != nil { + return fmt.Errorf("创建 IPv4 GeoLite2 reader 失败: %w", err) + } + + m.v4GeoReader = reader + m.v4Source = SourceGeoLite2 + m.v4LoadTime = time.Now() + m.v4FileSize = int64(len(data)) + m.v4CreateTime = time.Now() // 记录创建时间 + + return nil +} + +// LoadV6Ip2Region 加载 IPv6 ip2region 数据库 +func (m *Manager) LoadV6Ip2Region(data []byte, format DBFormat) error { + m.mu.Lock() + defer m.mu.Unlock() + + // 关闭旧的 searcher + if m.v6Searcher != nil { + m.v6Searcher.Close() + m.v6Searcher = nil + } + + // 创建新的 searcher (buffer 模式并发安全) + searcher, err := xdb.NewWithBuffer(xdb.IPv6, data) + if err != nil { + return fmt.Errorf("创建 IPv6 searcher 失败: %w", err) + } + + m.v6Searcher = searcher + m.v6Source = SourceIp2Region + m.v6Format = format + m.v6LoadTime = time.Now() + m.v6FileSize = int64(len(data)) + m.v6CreateTime = time.Now() // 记录创建时间 + + return nil +} + +// LoadV6GeoLite2 加载 IPv6 GeoLite2 数据库 +func (m *Manager) LoadV6GeoLite2(data []byte) error { + m.mu.Lock() + defer m.mu.Unlock() + + // 关闭旧的 reader + if m.v6GeoReader != nil { + m.v6GeoReader.Close() + m.v6GeoReader = nil + } + + // 创建新的 reader + reader, err := geoip2.FromBytes(data) + if err != nil { + return fmt.Errorf("创建 IPv6 GeoLite2 reader 失败: %w", err) + } + + m.v6GeoReader = reader + m.v6Source = SourceGeoLite2 + m.v6LoadTime = time.Now() + m.v6FileSize = int64(len(data)) + m.v6CreateTime = time.Now() // 记录创建时间 + + return nil +} + +// ReloadV4 热加载 IPv4 数据库 +func (m *Manager) ReloadV4(data []byte, source DBSource, format DBFormat) error { + if source == SourceIp2Region { + return m.LoadV4Ip2Region(data, format) + } else if source == SourceGeoLite2 { + return m.LoadV4GeoLite2(data) + } + return fmt.Errorf("未知的数据源: %s", source) +} + +// ReloadV6 热加载 IPv6 数据库 +func (m *Manager) ReloadV6(data []byte, source DBSource, format DBFormat) error { + if source == SourceIp2Region { + return m.LoadV6Ip2Region(data, format) + } else if source == SourceGeoLite2 { + return m.LoadV6GeoLite2(data) + } + return fmt.Errorf("未知的数据源: %s", source) +} + +// GetStatus 获取当前数据库状态 +func (m *Manager) GetStatus() *DBStatus { + m.mu.RLock() + defer m.mu.RUnlock() + + status := &DBStatus{ + IPv4Source: string(m.v4Source), + IPv4Format: string(m.v4Format), + IPv4FileSize: m.v4FileSize, + IPv6Source: string(m.v6Source), + IPv6Format: string(m.v6Format), + IPv6FileSize: m.v6FileSize, + } + + if !m.v4LoadTime.IsZero() { + status.IPv4LoadTime = m.v4LoadTime.Format("2006-01-02 15:04:05") + } + if !m.v6LoadTime.IsZero() { + status.IPv6LoadTime = m.v6LoadTime.Format("2006-01-02 15:04:05") + } + if !m.v4CreateTime.IsZero() { + status.IPv4CreateTime = m.v4CreateTime.Format("2006-01-02 15:04:05") + } + if !m.v6CreateTime.IsZero() { + status.IPv6CreateTime = m.v6CreateTime.Format("2006-01-02 15:04:05") + } + + return status +} + +// Close 关闭所有数据库连接 +func (m *Manager) Close() { + m.mu.Lock() + defer m.mu.Unlock() + + if m.v4Searcher != nil { + m.v4Searcher.Close() + m.v4Searcher = nil + } + if m.v4GeoReader != nil { + m.v4GeoReader.Close() + m.v4GeoReader = nil + } + if m.v6Searcher != nil { + m.v6Searcher.Close() + m.v6Searcher = nil + } + if m.v6GeoReader != nil { + m.v6GeoReader.Close() + m.v6GeoReader = nil + } +} + +// SetV4Source 设置 IPv4 数据源 +func (m *Manager) SetV4Source(source DBSource) { + m.mu.Lock() + defer m.mu.Unlock() + m.v4Source = source +} + +// SetV6Source 设置 IPv6 数据源 +func (m *Manager) SetV6Source(source DBSource) { + m.mu.Lock() + defer m.mu.Unlock() + m.v6Source = source +} + +// SetV4Format 设置 IPv4 数据格式 +func (m *Manager) SetV4Format(format DBFormat) { + m.mu.Lock() + defer m.mu.Unlock() + m.v4Format = format +} + +// SetV6Format 设置 IPv6 数据格式 +func (m *Manager) SetV6Format(format DBFormat) { + m.mu.Lock() + defer m.mu.Unlock() + m.v6Format = format +} diff --git a/iplocation/types.go b/iplocation/types.go new file mode 100644 index 0000000..92a3a45 --- /dev/null +++ b/iplocation/types.go @@ -0,0 +1,50 @@ +package iplocation + +// IPLocationResult 统一的 IP 地理位置查询结果 +type IPLocationResult struct { + Country string // 国家 + Province string // 省份 + City string // 城市 + ISP string // 运营商 + Region string // 区域/大洲 + District string // 区县 +} + +// ToSlice 返回兼容老格式的 []string: [国家, 区域, 省份, 城市, ISP] +func (r *IPLocationResult) ToSlice() []string { + return []string{r.Country, r.Region, r.Province, r.City, r.ISP} +} + +// DBFormat IP 数据库字段格式 +type DBFormat string + +const ( + FormatLegacy DBFormat = "legacy" // 国家|区域|省份|城市|ISP (老版本内置) + FormatOpenSource DBFormat = "opensource" // 国家|省份|城市|网络运营商 + FormatFull DBFormat = "full" // 大洲|国家|省份|城市|区县|网络运营商|其他 + FormatStandard DBFormat = "standard" // 国家|省份|城市|区县|网络运营商|其他 + FormatCompact DBFormat = "compact" // 国家|省份|城市|网络运营商|其他 +) + +// DBSource IP 数据库来源 +type DBSource string + +const ( + SourceIp2Region DBSource = "ip2region" + SourceGeoLite2 DBSource = "geolite2" +) + +// DBStatus 数据库状态信息 +type DBStatus struct { + IPv4Source string `json:"ipv4_source"` + IPv4Format string `json:"ipv4_format"` + IPv4FileSize int64 `json:"ipv4_file_size"` + IPv4LoadTime string `json:"ipv4_load_time"` + IPv4CreateTime string `json:"ipv4_create_time"` + + IPv6Source string `json:"ipv6_source"` + IPv6Format string `json:"ipv6_format"` + IPv6FileSize int64 `json:"ipv6_file_size"` + IPv6LoadTime string `json:"ipv6_load_time"` + IPv6CreateTime string `json:"ipv6_create_time"` +} diff --git a/router/entrance.go b/router/entrance.go index 23927c2..087af66 100644 --- a/router/entrance.go +++ b/router/entrance.go @@ -51,6 +51,7 @@ type ApiGroup struct { FirewallIPBlockRouter PluginRouter LogFileWriteRouter + IPLocationRouter } type PublicApiGroup struct { LoginRouter diff --git a/router/waf_iplocation.go b/router/waf_iplocation.go new file mode 100644 index 0000000..381e726 --- /dev/null +++ b/router/waf_iplocation.go @@ -0,0 +1,20 @@ +package router + +import ( + "SamWaf/api" + "github.com/gin-gonic/gin" +) + +type IPLocationRouter struct { +} + +func (receiver *IPLocationRouter) InitIPLocationRouter(group *gin.RouterGroup) { + apiInstance := api.APIGroupAPP.WafIPLocationApi + router := group.Group("/api/v1/iplocation") + { + router.GET("/status", apiInstance.GetIPDBStatusApi) + router.POST("/upload", apiInstance.UploadIPDBFileApi) + router.POST("/reload", apiInstance.ReloadIPDBApi) + router.POST("/test", apiInstance.TestIPLookupApi) + } +} diff --git a/utils/common.go b/utils/common.go index acbe666..18c9d60 100644 --- a/utils/common.go +++ b/utils/common.go @@ -16,9 +16,6 @@ import ( "strconv" "strings" "time" - - "github.com/lionsoul2014/ip2region/binding/golang/xdb" - "github.com/oschwald/geoip2-golang" ) func GetExternalIp() string { @@ -149,73 +146,17 @@ func GetPublicIP() string { } func GetCountry(ip string) []string { - if IsValidIPv6(ip) { - a := "ipv6|ipv6|ipv6|ipv6|ipv6" - if global.GCACHE_IPV6_SEARCHER == nil { - db, err := geoip2.FromBytes(global.GCACHE_IP_V6_COUNTRY_CBUFF) - if err != nil { - zlog.Error("Failed to open GeoLite2-Country.mmdb:", err) - return strings.Split(a, "|") - } - global.GCACHE_IPV6_SEARCHER = db - } - ipv6 := net.ParseIP(ip) - record, err := global.GCACHE_IPV6_SEARCHER.Country(ipv6) - if err != nil { - zlog.Error("Failed to Search GeoLite2-Country.mmdb:", err) - return strings.Split(a, "|") - } - if record.Country.Names == nil { - a = "内网" + "||||" - } else { - a = record.Country.Names["zh-CN"] + "||||" - } - - return strings.Split(a, "|") - } - - //IPV4得查询逻辑 - if global.GCACHE_IPV4_SEARCHER == nil { - // 2、用全局的 cBuff 创建完全基于内存的查询对象。 - searcher, err := xdb.NewWithBuffer(global.GCACHE_IP_CBUFF) - if err != nil { - fmt.Printf("failed to create searcher with content: %s\n", err) - - } - global.GCACHE_IPV4_SEARCHER = searcher - } - - // do the search - var tStart = time.Now() - - // 备注:并发使用,每个 goroutine 需要创建一个独立的 searcher 对象。 - region, err := global.GCACHE_IPV4_SEARCHER.SearchByStr(ip) - if err != nil { - fmt.Printf("failed to SearchIP(%s): %s\n", ip, err) - return []string{"无", "无"} + if global.GIPLOCATION_MANAGER != nil { + result := global.GIPLOCATION_MANAGER.Lookup(ip) + return result.ToSlice() // [国家, 区域, 省份, 城市, ISP] } - - zlog.Debug("{region: %s, took: %s}\n", region, time.Since(tStart)) - regions := strings.Split(region, "|") - //如果是内网IP情况下显示内网的内容 - if regions[4] == "内网IP" { - regions[0] = "内网" - regions[1] = "内网" - regions[2] = "内网" - } - return regions + return []string{"未知", "", "", "", ""} } // CloseIPDatabase 关闭IP数据库 func CloseIPDatabase() { - if global.GCACHE_IPV4_SEARCHER != nil { - global.GCACHE_IPV4_SEARCHER.Close() - } - if global.GCACHE_IPV6_SEARCHER != nil { - err := global.GCACHE_IPV6_SEARCHER.Close() - if err != nil { - return - } + if global.GIPLOCATION_MANAGER != nil { + global.GIPLOCATION_MANAGER.Close() } } diff --git a/wafmangeweb/localserver.go b/wafmangeweb/localserver.go index 75725d4..a9a026e 100644 --- a/wafmangeweb/localserver.go +++ b/wafmangeweb/localserver.go @@ -92,6 +92,7 @@ func (web *WafWebManager) initRouter(r *gin.Engine) { router.ApiGroupApp.InitNotifyLogRouter(RouterGroup) router.ApiGroupApp.InitFirewallIPBlockRouter(RouterGroup) router.ApiGroupApp.InitLogFileWriteRouter(RouterGroup) + router.ApiGroupApp.InitIPLocationRouter(RouterGroup) } if global.GWAF_RELEASE == "true" { diff --git a/waftask/task_config.go b/waftask/task_config.go index 811c179..4b284f8 100644 --- a/waftask/task_config.go +++ b/waftask/task_config.go @@ -3,6 +3,7 @@ package waftask import ( "SamWaf/common/zlog" "SamWaf/global" + "SamWaf/iplocation" "SamWaf/model" "SamWaf/model/request" "SamWaf/wafipban" @@ -243,6 +244,34 @@ func setConfigStringValue(name string, value string, change int) { case "log_file_write_custom_tpl": global.GCONFIG_LOG_FILE_WRITE_CUSTOM_TPL = value syncLogFileWriterConfig() + case "ip_v4_source": + global.GCONFIG_IP_V4_SOURCE = value + if change == 1 { + zlog.Info("IPv4 数据源配置已更改为: ", value) + } + case "ip_v6_source": + global.GCONFIG_IP_V6_SOURCE = value + if change == 1 { + zlog.Info("IPv6 数据源配置已更改为: ", value) + } + case "ip_v4_format": + global.GCONFIG_IP_V4_FORMAT = value + if change == 1 { + // 重新加载 IPv4 数据库 + if global.GIPLOCATION_MANAGER != nil { + global.GIPLOCATION_MANAGER.SetV4Format(iplocation.DBFormat(value)) + zlog.Info("IPv4 数据格式配置已更改为: ", value) + } + } + case "ip_v6_format": + global.GCONFIG_IP_V6_FORMAT = value + if change == 1 { + // 重新加载 IPv6 数据库 + if global.GIPLOCATION_MANAGER != nil { + global.GIPLOCATION_MANAGER.SetV6Format(iplocation.DBFormat(value)) + zlog.Info("IPv6 数据格式配置已更改为: ", value) + } + } default: zlog.Warn("Unknown config item:", name) } @@ -380,6 +409,20 @@ func TaskLoadSetting(initLoad bool) { updateConfigStringItem(initLoad, "ssl", "zerossl_eab_kid", global.GCONFIG_ZEROSSL_EAB_KID, "zerossl eab_kid", "string", "", configMap) updateConfigStringItem(initLoad, "ssl", "zerossl_eab_hmac_key", global.GCONFIG_ZEROSSL_EAB_HMAC_KEY, "zerossl eab_hmac_key", "string", "", configMap) + // IP数据库配置 + updateConfigStringItem(initLoad, "ip_database", "ip_v4_source", + global.GCONFIG_IP_V4_SOURCE, "IPv4数据库来源", + "options", "ip2region|ip2region,geolite2|GeoLite2", configMap) + updateConfigStringItem(initLoad, "ip_database", "ip_v6_source", + global.GCONFIG_IP_V6_SOURCE, "IPv6数据库来源", + "options", "ip2region|ip2region,geolite2|GeoLite2", configMap) + updateConfigStringItem(initLoad, "ip_database", "ip_v4_format", + global.GCONFIG_IP_V4_FORMAT, "IPv4 xdb字段格式", + "options", "legacy|老版本,opensource|开源版,full|满载版,standard|标准版,compact|精简版", configMap) + updateConfigStringItem(initLoad, "ip_database", "ip_v6_format", + global.GCONFIG_IP_V6_FORMAT, "IPv6 xdb字段格式(仅ip2region时有效)", + "options", "legacy|老版本,opensource|开源版,full|满载版,standard|标准版,compact|精简版", configMap) + // 日志文件写入相关配置 updateConfigIntItem(initLoad, "logfile", "log_file_write_enable", global.GCONFIG_LOG_FILE_WRITE_ENABLE, "日志文件写入开关(0关闭 1开启)", "options", "0|关闭,1|开启", configMap) updateConfigStringItem(initLoad, "logfile", "log_file_write_path", global.GCONFIG_LOG_FILE_WRITE_PATH, "日志文件输出路径", "string", "", configMap)