- 新增 internal/security/ipinfo:基于 projectdiscovery/cdncheck v1.3.0 (离线指纹、零外部请求)识别来源 IP 归属,识别失败折叠为展示标签, 不中断告警链路 - autoban 告警回调注入归属识别;告警邮件新增「网络归属」行, 命中 CDN/代理网络时提示「封禁可能无法拦截真实来源」 - 新增邮件模板渲染回归测试 internal/mail/mail_alert_test.go - dev/s3stub:webp(cgo) 本地类型检查桩,替换 s3.go 做全模块编译验证, 生产 Linux 构建不受影响;dev/overlay.json 指向该桩 - go mod tidy 顺带升级共享依赖(lipgloss/xid/testify 等), 全模块类型检查与相关包测试通过
85 lines
2.5 KiB
Go
85 lines
2.5 KiB
Go
// Package ipinfo 识别 IP 的网络归属(CDN / 云厂商 / WAF)。
|
||
// 基于 projectdiscovery/cdncheck 的离线签名(编译期嵌入,上游每周更新指纹),
|
||
// 判定过程零外部网络请求,适合 1C1G 小型服务器与告警链路使用。
|
||
// 当前用于安全告警邮件:展示扫描来源的网络归属,帮助管理员判断告警严重性。
|
||
package ipinfo
|
||
|
||
import (
|
||
"fmt"
|
||
"net"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/projectdiscovery/cdncheck"
|
||
)
|
||
|
||
var (
|
||
clientOnce sync.Once
|
||
client *cdncheck.Client
|
||
)
|
||
|
||
// getClient 懒加载判定客户端(签名数据编译期嵌入,New 无错误返回)。
|
||
func getClient() *cdncheck.Client {
|
||
clientOnce.Do(func() {
|
||
client = cdncheck.New()
|
||
})
|
||
return client
|
||
}
|
||
|
||
// Lookup 判定 IP 的网络归属。
|
||
// 返回:
|
||
// - label:展示用标签,如 "Cloudflare(CDN)"、"亚马逊(云厂商)";
|
||
// 未命中返回 "未识别(普通网络)",输入非法或判定出错返回 "识别失败"。
|
||
// - proxied:IP 是否属于 CDN/WAF/代理网络(用于提示"封禁可能无法拦截真实来源")。
|
||
//
|
||
// 任何内部错误都被折叠为展示标签,不向调用方传播——告警链路不应因归属识别失败而中断。
|
||
func Lookup(ipStr string) (label string, proxied bool) {
|
||
ip := net.ParseIP(strings.TrimSpace(ipStr))
|
||
if ip == nil {
|
||
return "识别失败(IP 格式异常)", false
|
||
}
|
||
|
||
matched, value, itemType, err := getClient().Check(ip)
|
||
if err != nil {
|
||
return "识别失败", false
|
||
}
|
||
if !matched || value == "" {
|
||
return "未识别(普通网络)", false
|
||
}
|
||
|
||
return fmt.Sprintf("%s(%s)", title(value), typeLabel(itemType)), true
|
||
}
|
||
|
||
// typeLabel 将 cdncheck 的 itemType 翻译为中文展示。
|
||
func typeLabel(itemType string) string {
|
||
switch itemType {
|
||
case "cdn":
|
||
return "CDN"
|
||
case "waf":
|
||
return "WAF"
|
||
case "cloud":
|
||
return "云厂商"
|
||
default:
|
||
return "未知类型"
|
||
}
|
||
}
|
||
|
||
// acronymProviders 需要保持全大写的提供商缩写(cdncheck 返回全小写,如 aws → AWS)。
|
||
var acronymProviders = map[string]bool{
|
||
"aws": true, "gcp": true, "oci": true, "gds": true, "ovh": true,
|
||
}
|
||
|
||
// title 将提供商名转为展示形式:缩写全大写(aws → AWS),其余首字母大写
|
||
// (cloudflare → Cloudflare)。
|
||
func title(s string) string {
|
||
if s == "" {
|
||
return s
|
||
}
|
||
lower := strings.ToLower(s)
|
||
if acronymProviders[lower] {
|
||
return strings.ToUpper(lower)
|
||
}
|
||
r := []rune(lower)
|
||
return strings.ToUpper(string(r[0])) + string(r[1:])
|
||
}
|