IPv6 邻居发现欺骗防御:RA Guard 配置实战

IPv6 邻居发现欺骗是 IPv6 内网最常见的一类二层攻击:攻击者伪造路由器通告(RA)抢占默认网关,或伪造邻居通告(NA)把同网段流量引到自己网卡上。与 ARP 欺骗不同,IPv6 的邻居发现协议天生不带认证,一条伪造报文就足以让整段网络断线或全量流量经过攻击者。下面按接入层、主机内核、防火墙、代理与监控五层给出可执行的配置。

适用场景

  • 办公网或 IDC 已经开始分配 IPv6 地址,终端通过 SLAAC 自动获取地址与默认路由;
  • 云主机或虚拟化环境启用了 IPv6,同一二层域内有不受控的设备;
  • 出现过「某个网段间歇性断网,交换机日志无异常」的情况,需要排除 RA 抢占;
  • 等保或安全基线要求对 ICMPv6 做类型级管控。

纯 IPv4 环境不需要做这些配置,但 ip6tables 默认放行所有 ICMPv6 是另一个隐患,建议顺手把 IPv6 栈按需关闭或收紧。

前置条件

  • 能登录接入交换机,且设备支持 RA Guard(Cisco IOS 15.x 以上、华为、H3C 主流型号均支持);
  • 服务器已规划静态 IPv6 地址与静态默认网关,不依赖 SLAAC;
  • 主机安装 ip6tables、tcpdump,测试环境可安装 thc-ipv6 验证拦截效果;
  • 记录各网段合法的 RA 源地址,通常是网关的链路本地地址。

原理说明

NDP 由五类 ICMPv6 报文组成:133 路由器请求(RS)、134 路由器通告(RA)、135 邻居请求(NS)、136 邻居通告(NA)、137 重定向。终端收到 RA 后自动配置前缀、默认路由和 MTU;收到 NA 后更新邻居缓存表。协议本身没有任何认证字段,接收方无条件信任,这就是攻击面所在。

防御的关键不是「加密 NDP」,而是限制谁能发、发多少。接入交换机侧的 RA Guard 直接在端口上丢弃主机发来的 RA,从源头掐断;主机侧关闭 RA 接收,避免被伪造路由带偏;防火墙侧对 135/136 限速,防止邻居缓存被刷爆造成拒绝服务。三层叠加后,即使有一层被绕过,攻击面也被压到很小。

操作步骤

第一步:建立邻居与 RA 基线

先看清楚现状,后续才能判断配置有没有生效。记录每个网段实际收到 RA 的源地址,正常情况下应该只有网关一个。

ip -6 addr show scope global
ip -6 route show default
ip -6 neigh show dev eth0

# 抓 60 秒 RA,观察源地址是否唯一
sudo timeout 60 tcpdump -i eth0 -nn -vv 'icmp6 and ip6[40] == 134'

# 主动询问网关通告内容
sudo rdisc6 -1 eth0

第二步:接入交换机开启 RA Guard

以 Cisco IOS 为例,把接终端的端口标记为 host 角色,收到 RA 直接丢弃。上行口必须保持 router 角色,否则会把自己网段的合法 RA 也拦掉。

ipv6 nd raguard policy HOST_POLICY
 device-role host
!
interface range GigabitEthernet1/0/1 - 24
 ipv6 nd raguard attach-policy HOST_POLICY
!
show ipv6 nd raguard policy
show ipv6 nd raguard statistics interface GigabitEthernet1/0/1

交换机不支持 RA Guard 时,退而用端口 ACL 过滤 134 类型,效果接近,但要逐个端口维护。

ipv6 access-list BLOCK_RA
 deny icmp any any router-advertisement
 permit ipv6 any any
!
interface GigabitEthernet1/0/5
 ipv6 traffic-filter BLOCK_RA in

第三步:主机内核参数固化

服务器不应该参与 SLAAC,也不应该接受重定向。参数写到 /etc/sysctl.d/ 下才能重启保留,直接 sysctl -w 只在当前会话有效。

sudo tee /etc/sysctl.d/60-ipv6-ndp.conf <<'EOF'
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.all.accept_ra_defrtr = 0
net.ipv6.conf.all.accept_ra_pinfo = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.all.autoconf = 0
net.ipv6.conf.all.proxy_ndp = 0
net.ipv6.conf.default.accept_ra = 0
net.ipv6.conf.default.accept_redirects = 0
EOF

sudo sysctl --system
sysctl net.ipv6.conf.all.accept_ra net.ipv6.conf.all.accept_redirects

第四步:ip6tables 按类型限速

这一步最容易出事:ICMPv6 承担了 IPv6 的正常运转,1 到 4 类型的差错报文和 128/129 的 ping 必须放行,否则会出现「能解析不能访问」的怪现象。顺序上先放行必要类型,再对 134 直接丢弃,最后对 135/136 限速。

sudo ip6tables -F INPUT
sudo ip6tables -A INPUT -i lo -j ACCEPT
sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# 必要的差错报文与 ping
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 1 -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 2 -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 3 -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 4 -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 128 -m limit --limit 20/second -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 129 -j ACCEPT

# 服务器不做路由,丢弃一切 RA 与重定向
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 134 -j DROP
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 137 -j DROP

# 邻居请求与通告限速,并丢弃跳数不为 255 的伪造报文
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 135 -m limit --limit 100/second -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 136 -m limit --limit 100/second -j ACCEPT
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 135 -j DROP
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 136 -m hl ! --hl-eq 255 -j DROP
sudo ip6tables -A INPUT -p icmpv6 -j DROP
sudo ip6tables -A INPUT -j ACCEPT

合法 NDP 报文的跳数固定为 255,这是协议规定的防跨网段伪造机制,用 -m hl 校验能过滤掉绝大多数外部注入的伪造包。

第五步:ndppd 代理与邻居缓存保护

虚拟化或前缀委派场景常需要代答 NDP,配置不当会把整段邻居关系暴露给攻击者。ndppd 只允许代理指定前缀,且必须显式 static,避免默认转发所有人。

sudo yum install -y ndppd
sudo tee /etc/ndppd.conf <<'EOF'
route-ttl 30000

proxy eth0 {
    router yes
    timeout 500
    ttl 30000
    rule 2001:db8:1::/64 {
        static
    }
}
EOF

sudo systemctl enable --now ndppd
sudo systemctl status ndppd --no-pager

第六步:邻居变更监控

配置只能降低概率,发现要靠监控。把邻居表快照与基线比对,出现 lladdr 变化或同 MAC 多地址就立刻告警。

#!/usr/bin/env python3
import json, subprocess, time, sys

BASE = "/etc/ndp_baseline.json"
def snap():
    out = subprocess.run(["ip", "-6", "neigh", "show"], capture_output=True, text=True).stdout
    d = {}
    for line in out.splitlines():
        f = line.split()
        if "lladdr" in f and "dev" in f:
            ip = f[0]
            mac = f[f.index("lladdr") + 1]
            d[ip] = mac
    return d

try:
    base = json.load(open(BASE))
except FileNotFoundError:
    json.dump(snap(), open(BASE, "w"), indent=1)
    sys.exit("baseline created")

cur = snap()
for ip, mac in cur.items():
    if ip in base and base[ip] != mac:
        print(f"[ALERT] {ip} lladdr {base[ip]} -> {mac} at {time.strftime('%F %T')}")
for ip in set(base) - set(cur):
    print(f"[WARN] {ip} disappeared")
json.dump(cur, open(BASE, "w"), indent=1)

放进 crontab 每两分钟跑一次,输出接告警通道:*/2 * * * * /usr/local/bin/ndp_watch.py >> /var/log/ndp_watch.log 2>&1。

配置验证

  • 再抓一次 RA:sudo timeout 30 tcpdump -i eth0 -nn 'icmp6 and ip6[40] == 134' 应无输出;
  • rdisc6 -1 eth0 返回空,说明主机已不接受 RA;
  • ip6tables -vnL INPUT | grep -E "134|136" 能看到 DROP 规则命中计数增长;
  • 在测试网用 sudo fake_router6 eth0 2001:db8:1::/64 伪造 RA,终端默认路由不应改变;
  • 正常业务回归:IPv6 站点可访问、DNS 解析正常、ping6 通,确认没有误伤必要 ICMPv6。

常见问题

Q1:关掉 accept_ra 之后,服务器 IPv6 地址和默认路由都没了怎么办?

这是预期行为,服务器本来就不该用 SLAAC。改成静态配置即可:ip -6 addr add 2001:db8:1::10/64 dev eth0、ip -6 route add default via 2001:db8:1::1,然后写进 NetworkManager 或 systemd-networkd 的配置文件持久化。云主机还要在控制台确认 IPv6 网关地址,部分厂商的网关不在 ::1。

Q2:接入交换机太老不支持 RA Guard,只做主机侧够吗?

不够但能挡住大部分。主机侧只能保护自己,同一网段的其他终端照样会被伪造 RA 带偏,进而形成中间人。补救办法是在能管控的端口上做 ACL 限制 134 出向,或在汇聚层部署 ND 检测设备;终端数量少的场景,直接用静态地址加静态路由,彻底不依赖 NDP 自动发现。

Q3:限制 135/136 之后,部分终端互相访问变慢或时通时断?

限速值设得太低。邻居表规模大的网段(例如上千终端的办公网),100/second 可能不够,建议按实际抓包峰值乘以 2 设定,并观察 ip6tables -vnL 的 DROP 计数是否持续增长。另外 135/136 的 ACCEPT 规则必须排在 DROP 之前,顺序错了会直接断网。

总结

IPv6 邻居发现欺骗的防御没有单点解,必须接入层与主机层同时收口:交换机用 RA Guard 拦住伪造通告,主机用 sysctl 关闭 RA 与重定向接收,防火墙按 ICMPv6 类型放行必要报文并对邻居请求限速,代理场景用 ndppd 限定前缀,最后用邻居表比对做持续发现。上线顺序建议先在测试网做一遍回归,确认 IPv6 业务不中断,再按网段灰度推送,避免一次性改完导致大面积断网。