feat:ip location

#628
This commit is contained in:
samwaf
2026-02-11 09:42:50 +08:00
parent 96a8e799c3
commit a19bde6412
14 changed files with 892 additions and 90 deletions
+1
View File
@@ -53,6 +53,7 @@ type APIGroup struct {
WafFirewallIPBlockApi
WafPluginApi
WafLogFileWriteApi
WafIPLocationApi
}
var APIGroupAPP = new(APIGroup)
+271
View File
@@ -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)
}
+66 -20
View File
@@ -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 read IPv6 database file GeoLite2-Country.mmdb: %v", err)
log.Fatalf("Failed to load IPv4 ip2region 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("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 load IPv6 GeoLite2 database: %v", err)
}
zlog.Info("IPv6 GeoLite2 database loaded successfully")
}
global.GWAF_DLP_CONFIG = ldpConfig
global.GWAF_REG_PUBLIC_KEY = publicKey
+14 -4
View File
@@ -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天前的数据
+2 -1
View File
@@ -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
+4
View File
@@ -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=
+84
View File
@@ -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
}
+329
View File
@@ -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
}
+50
View File
@@ -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"`
}
+1
View File
@@ -51,6 +51,7 @@ type ApiGroup struct {
FirewallIPBlockRouter
PluginRouter
LogFileWriteRouter
IPLocationRouter
}
type PublicApiGroup struct {
LoginRouter
+20
View File
@@ -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)
}
}
+6 -65
View File
@@ -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, "|")
if global.GIPLOCATION_MANAGER != nil {
result := global.GIPLOCATION_MANAGER.Lookup(ip)
return result.ToSlice() // [国家, 区域, 省份, 城市, ISP]
}
//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{"无", "无"}
}
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()
}
}
+1
View File
@@ -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" {
+43
View File
@@ -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)