IPv6 CC攻击怎么防?按/64聚合限流怎么做

开了 IPv6 后单 IP 限流几乎失效,因为地址几乎是无限的。本文用 10 个问答讲清 IPv6 CC攻击的特点、按 /64 聚合限流怎么落地、Nginx 与内核防火墙各自能做什么,附日志聚合脚本。

Q:开了 IPv6,为什么原来的单 IP 限流不管用了?

  • IPv6 的一个用户通常拿到一整段 /64,地址数量是天文数字,换地址的成本几乎为零。
  • 原来的 limit_req_zone $binary_remote_addr 只锁一个地址,现在每个请求换一个新地址,每个键只出现一次,永远碰不到阈值。
  • 还有一个容易忽略的副作用:IPv6 的键是 16 字节,同样大小的 zone 能装下的状态数大约只剩 IPv4 的一半,高并发下更早触发内存告警。
  • 结论:IPv6 下限流的单位必须从「地址」换成「前缀」,否则限流形同虚设。
  • 更麻烦的是,很多自建封禁脚本是按 IPv4 写的,遇到带冒号的地址直接跳过,攻击在自己的工具链里变成了隐形流量。

Q:IPv6 下的 CC 攻击有哪些不一样的表现?

  • Top IP 排行榜失效:所有攻击地址的请求数都只有个位数,按 IP 排序看不出任何异常。
  • 聚合后才现形:把同一个 /64 的请求加起来,一小时能到几千上万次。
  • UA、URL、Referer 高度一致:地址在变,请求指纹不变,这是 IPv6 攻击最好抓的特征。
  • 监控指标得跟着改:从「单 IP QPS」改成「单 /64 每分钟请求数」,否则看板上永远风平浪静。
  • 攻击经常是双栈混合的,IPv4 那侧被限住了,压力会整体挪到 IPv6 上。

Q:那直接封 /48 或者整段是不是更快?

  • /64 通常是分配给终端用户的最小单位,封一个 /64 大致等于封一户家宽或一台云主机,误伤面可控。
  • 再往上就是 /48、/32,那可能是一整个运营商或一个机房,一封就是一片正常用户,代价远大于收益。
  • 移动网络和部分云厂商的分配策略不一样,封之前要确认这个 /64 下的地址是否只服务一个客户。
  • 原则:只聚合到 /64,不往更粗走;宁可多封几条,也不要一刀切一大段。

Q:Nginx 能直接按 /64 限流吗?

  • 不能原生做。limit_req_zone 的键只能是一个变量的值,没有「取前缀」这种指令。
  • 继续用 $binary_remote_addr,就是每个地址独立计数,等于没有聚合。
  • 用 map 加正则截取前四段,只对完整展开写法有效,遇到压缩写法(带双冒号那种)就漏,同一个前缀会被算成两个键,攻击者稍微变形就绕过去了。
  • 所以聚合位置只有三个可选:CDN/WAF 侧、OpenResty 用 Lua 算、源站内核防火墙。前两个能做实时限流,最后一个只能做事后封禁。

Q:OpenResty 里怎么算出 /64 前缀当限流键?

  • 思路:取客户端地址的二进制形式,前 8 字节就是 /64 前缀,直接当限流键用。
  • 必须用 set_by_lua_block,它在 rewrite 阶段执行,早于 limit_req 所在的 preaccess 阶段;写成 access_by_lua_block 就来不及了,限流键取到的还是旧值。
  • IPv4 的二进制是 4 字节,原样返回,同一套配置双栈通吃。
# http 块
limit_req_zone $cc_key zone=cc6:20m rate=10r/s;

# server 或 location 块
set $cc_key "";
set_by_lua_block $cc_key {
    local bin = ngx.var.binary_remote_addr
    if bin and #bin == 16 then
        return string.sub(bin, 1, 8)   -- 前 8 字节 = /64 前缀
    end
    return bin or ""
}
limit_req zone=cc6 burst=20 nodelay;
limit_req_status 429;
  • rate=10r/s 是「单个 /64 每秒 10 个请求」的意思,按你站点的正常人均访问量定,不要照抄。
  • 改成前缀键之后,一个 /64 只占一个状态,zone 反而更省,20m 装几万个前缀够用,不必按 IP 数量去估算。
  • 同样是 429 而不是 503,503 会被 CDN 缓存成故障页,误伤整站。

Q:源站层面怎么按 /64 直接封?

  • Nginx 限流只是「减速」,真要止血还得在内核层丢包,成本最低。
  • 用 nftables 建一个带自动过期的集合,封禁 30 分钟后自动失效,避免永久误伤。
  • ip6tables 是老写法,能用但不推荐,规则多了性能差、维护也乱。
# 建表、建带 30 分钟过期的集合、挂到 input 链
nft add table inet ccguard
nft add set inet ccguard block6 '{ type ipv6_addr; flags interval; timeout 30m; }'
nft add chain inet ccguard input '{ type filter hook input priority -10; policy accept; }'
nft add rule inet ccguard input ip6 saddr @block6 counter drop

# 封一个 /64(只封这一段,别扩到 /48)
nft add element inet ccguard block6 '{ 2001:db8:abcd:12::/64 }'

# 老写法,效果一样
ip6tables -I INPUT -s 2001:db8:abcd:12::/64 -j DROP
  • 封禁清单要能一键回滚,写进脚本执行,别在命令行里手敲,敲错一位就是一个网段。
  • 封禁时长从 30 分钟起步,同一前缀反复出现再翻倍,不要一上来就封一天。

Q:日志里怎么找出正在打的 /64?

  • 按 IP 排序的做法在 IPv6 下完全失效,必须先把地址归到前缀再统计。
  • 脚本只处理带冒号的地址,IPv4 请求不参与,避免把正常流量混进来。
  • 输出结果直接能转成 nftables 命令,省掉手抄环节。
import ipaddress, collections
c = collections.Counter()
for line in open('/var/log/nginx/access.log', errors='ignore'):
    p = line.split()
    if not p:
        continue
    ip = p[0]
    if ':' not in ip:
        continue                      # 只看 IPv6
    try:
        net = ipaddress.ip_network(ip + '/64', strict=False)
    except ValueError:
        continue
    c[str(net)] += 1
for net, n in c.most_common(10):
    print(n, net)
  • 如果前面套了 CDN,日志第一列是节点地址,要先取 X-Forwarded-For 里的第一个地址再聚合。
  • 统计窗口建议 5 到 10 分钟,太长会把正常爬虫也算成攻击。

Q:CDN 后面的 real_ip 对 IPv6 有什么坑?

  • 不配 set_real_ip_from 的 IPv6 回源段,$binary_remote_addr 全是 CDN 节点地址,所有用户合并成一个键,限流直接把全站限死。
  • 这是「一开限流全站 429」最常见的原因,先查这一步再怀疑规则。
  • 回源段要写全,CDN 的 IPv6 节点段经常比 IPv4 段多,漏写一个段就有一批用户被合并计数。
set_real_ip_from 2001:db8:aaaa::/48;
set_real_ip_from 2001:db8:bbbb::/48;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
  • 如果回源头里的 IPv6 被加上方括号,realip 有可能解不出来,表现是日志里清一色节点地址,用 remote_addr 和 X-Forwarded-For 对照一看就知道。
  • realip 在 post-read 阶段生效,早于限流,顺序本身没问题;但限流键如果走 Lua,仍要确认 Lua 执行阶段在限流之前。

Q:按 /64 聚合会不会误伤正常用户?

  • 一个 /64 通常就是一户,误伤面比按 /32 小得多;但企业专线、酒店、学校可能一个 /64 覆盖很多人。
  • 先观察后封禁:先生成候选清单,人工确认再分批下发,别让脚本自动封。
  • 白名单优先:搜索引擎、监控探针、合作方回源段先进白名单,再谈封禁。
  • 封禁粒度配合时长:短封加复查,比长封加遗忘安全得多。
  • 封之前先数一下这个前缀下有多少个不同地址在访问,几百个地址同时打,基本能确认是攻击而不是共享出口。

Q:还有什么补充建议?

  • 双栈一起看。攻击者常用 IPv4 试探、IPv6 主力,只封一侧等于留了个后门。
  • 监控里加一条「单 /64 每分钟请求数」,比单 IP QPS 有用得多,这是 IPv6 时代的核心指标。
  • 带宽计费不分协议栈,IPv6 打进来一样算钱,别以为换了协议就不用管流量账单。
  • 最后一句:IPv6 下防 CC,关键不是封得快,而是聚合的单位选对,单位错了,规则写得再狠也白费。