c.clientip()线上失效因未配置可信代理,导致取到内网地址而非真实ip;须设settrustedproxies并优先读x-real-ip或校验x-forwarded-for最左有效ip。

为什么直接用 c.ClientIP() 拦截扫描器会失效
线上环境几乎必然返回 127.0.0.1 或内网地址(如 10.0.0.3),因为真实出口 IP 被压在 X-Forwarded-For 或 X-Real-IP 头里。Gin 的 c.ClientIP() 默认只信任回环网段,不配置可信代理链就调用,等于拿反向代理的地址当用户地址——扫描器从 203.208.60.12 发请求,你却在拦 127.0.0.1。
必须显式设置:engine.SetTrustedProxies([]string{"192.168.0.0/16", "203.208.60.0/24"}),否则解析 X-Forwarded-For 时会把攻击者伪造的头当真。
- 若 Nginx 已配
proxy_set_header X-Real-IP $remote_addr,优先读c.Request.Header.Get("X-Real-IP") - 若走 CDN → Nginx → Go 多层代理,取
X-Forwarded-For中最左的、且不在SetTrustedProxies列表里的地址,不是最右 - 漏掉
SetTrustedProxies,黑白名单逻辑直接被绕过
如何用 CIDR 支持的 IPSet 实现热更新黑名单
用 map[string]bool 存单个 IP 看似简单,但无法匹配网段(如 192.168.1.0/24),并发读写要加锁,更致命的是规则更新时直接替换 map,正在处理的请求可能看到新旧规则混杂状态。
正确做法是用支持 CIDR 的结构(如 netip.Prefix + atomic.Value)封装规则:
- 初始化时构建
netip.Prefix切片,按掩码长度倒序排列(保证/32优先于/24) - 用
atomic.Value包装该切片,更新时Store整个新切片,无锁、原子、无中间态 - 查询时遍历切片,对每个
Prefix.Contains(ip)做判断,命中即拦截 - 豁免健康检查路径(如
/healthz、/metrics),避免监控误报
拦截后必须 c.Abort() + return,否则等于没拦
常见错误是写了 c.JSON(403, gin.H{"msg": "forbidden"}) 却没跟 c.Abort() 和 return,导致后续中间件甚至业务 handler 继续执行——数据库可能已被改、日志已落盘、通知已发出。
-
c.Abort()只阻止后续中间件执行,不终止当前函数体 - 漏掉
return,c.Next()仍会被调用,请求继续向下流转 - 静态资源路径(如
/public/js/app.js)也得走同一套过滤逻辑,否则扫描器可通过加载 JS 文件试探接口路径
如何识别并限流高频自动化扫描行为
单纯靠 IP 黑白名单只能封已知恶意源,对新 IP 扫描无效。需叠加行为特征识别:
- 统计单位时间(如 60 秒)内同一 IP 的请求数,超阈值(如 100 次)即临时加入内存黑名单(TTL 5 分钟)
- 检查
User-Agent是否含sqlmap、nuclei、gobuster等典型扫描器标识,匹配即拦截 - 对无 Referer、Accept 头异常、或 Accept: */* 但 Host 不匹配的请求提高可疑等级
- 注意:所有计数器必须用
sync.Map或带 TTL 的 LRU cache(如github.com/hashicorp/golang-lru),别用全局 map 加互斥锁,高并发下成瓶颈
真正难的不是写规则,而是确保每层代理都正确透传真实 IP 并参与校验——少配一个 SetTrustedProxies,整个过滤链就断在第一环。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











