最快最稳的方式是封装http.handler,在servehttp入口提取并校验ip:优先x-real-ip, fallback x-forwarded-for最后一个非私有ip,最后remoteaddr截取;黑名单用map[string]struct{} o(1)查询,cidr校验用预解析的net.ipnet.contains,决策顺序为黑名单→白名单→默认拒绝。
直接在 go 的 net/http 协议层做恶意 ip 拉黑,最快、最稳的方式是封装 http.handler,在 servehttp 入口处提取并校验 ip——不是改连接、不碰内核、不依赖第三方中间件,5 行逻辑就能生效。
怎么在 HTTP Handler 入口安全提取真实客户端 IP
你拿到的 r.RemoteAddr 几乎总是反向代理(Nginx/ALB)或本地回环地址,不能直接用。必须按可信链路逐级降级取值:
- 优先读
r.Header.Get("X-Real-IP"),Nginx 默认透传,且只设一次,伪造成本高 - fallback 到
r.Header.Get("X-Forwarded-For"):取最后一个非私有 IP(需用net.ParseIP解析后调IsPrivate()校验),跳过127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、fd00::/8 - 最后才用
r.RemoteAddr,仅限直连调试场景;注意它含端口(如"192.168.1.100:54321"),得用strings.Split(r.RemoteAddr, ":")[0]截取 - 所有 IP 字符串必须经
net.ParseIP()解析再转回字符串(ip.String()),否则"::1"和"127.0.0.1"无法等价比对
用 map[string]struct{} 实现 O(1) 黑名单查询
万级以下黑名单,别折腾布隆过滤器或 trie——它引入误判、增加部署复杂度,而 map[string]struct{} 查找快、内存省、热更新简单:
- 初始化:
blacklist := make(map[string]struct{}) - 加载时:
blacklist["192.168.1.100"] = struct{}{}或从配置文件/DB 批量注入 - 判断:
_, blocked := blacklist[ipStr],无锁、无 GC 压力、无哈希冲突退化 - 别用
sync.Map:除非你每秒增删上千次,否则它比普通 map +sync.RWMutex更慢、更占内存 - 高频攻击下,日志要采样:同一 IP 1 分钟内首次拦截记
INFO,后续只WARN并带计数,避免磁盘打满
为什么不能在 net.Listener 层或 XDP 做“硬拉黑”
想在连接建立前就丢包?Go 本身做不到,强行上会踩三个硬坑:
-
net.Listener.Accept()后立刻查黑名单,会把 Nginx/ALB 后的真实用户 IP 当成代理地址误杀——因为此时还没解析 HTTP 头,拿不到X-Real-IP - 短连接洪泛(健康检查、HTTP/2 Ping)高频触发查表,哪怕用
map,也比指针比较慢一个数量级;实测 10Gbps 流量下 CPU 占用可飙到 15%+ - 真要用 XDP 硬拦截,Go 只能当“遥控器”:C 写 eBPF 程序跑在网卡驱动层,Go 通过
libbpfgo加载.o文件并往BPF_MAP_TYPE_HASH插 IP;传 IP 前必须用binary.BigEndian.PutUint32()转网络字节序,否则127.0.0.1变成0x0100007f查不到
网段黑名单必须用 net.IPNet.Contains 而非字符串匹配
如果黑名单含 CIDR(如 "192.168.1.0/24" 或 "2001:db8::/32"),绝不能用 strings.HasPrefix 或正则——IPv6 下极易误判:
- 预解析所有 CIDR:用
net.ParseCIDR("192.168.1.0/24")得到*net.IPNet,缓存为[]*net.IPNet切片,别每次请求都解析 - 客户端 IP 必须先
net.ParseIP(),再传给ipNet.Contains(clientIP);net.ParseIP自动处理::ffff:192.168.1.1这类映射地址 - 决策顺序必须是:先查黑名单(命中即拒),再查白名单(仅黑名单未命中时生效),最后默认拒绝——写成
if inWhitelist && !inBlacklist是典型逻辑漏洞 - IPv6 场景下,
/64网段和/128单 IP 的判断行为一致,Contains内部已处理对齐与掩码
真正难的不是写几行 if _, ok := blacklist[ip]; ok { http.Error(...) },而是搞清你的流量路径里到底经过了几层代理、哪些头可信、哪些 IP 段该被跳过校验——这些细节漏掉一个,整套拉黑就形同虚设。











