记一次无法获取IPv6 PD的反直觉经历

几个月之前,我把我的主路由重新刷成了Debian 12,装完各种软件之后....就再也没有正常获取到IPv6 PD,只能用NAT6先凑合着用

直到最近亲自抓了抓包,才发现问题何在——倒不如说,借此机会好好学习了一下IPv6 PD的获取流程

前言

玩过IPv6的朋友都知道,一般来说,相对『正常的』IPv6获取流程是:

  1. PPPoE,以及一堆乱七八糟的ICMPv6通告,拿到单播IPv6地址(国内ISP比较喜欢称呼为『互联地址』),配置在WAN口上
  2. 接着是DHCPv6,发RS(路由器请求)请求PD,对端发RA(路由器通告)把PD给你(俗称的『业务地址』),路由器拿到之后配在LAN口上,并通过SLAAC等方式向内网广播

如果是详细一些的资料,可能还会告诉你,请求PD的时候,路由器会通过WAN,用WAN口的fe80地址的546端口,向ff02::1:2这个组播地址的567端口发送RS,然后对端就会通过567端口向你的546端口发送IPv6 PD等信息,然后走流程就行

问题来了,对端是怎么回复的?

故障实例

这个软路由用的是wide-dhcpv6-client,其核心就是dhcp6c。运行之后,本地dhcp6c向远端请求前缀,但是反复重试均未能获得前缀:

root@router:~# dhcp6c -dDfi ppp0

Feb/06/2025 16:14:06: get_duid: extracted an existing DUID from /var/lib/dhcpv6/dhcp6c_duid: 00:01:00:01:*:*:*:*:*:*:*:*:*:*
Feb/06/2025 16:14:06: dhcp6_reset_timer: reset a timer on ppp0, state=INIT, timeo=0, retrans=720
Feb/06/2025 16:14:07: client6_send: a new XID (5****a) is generated
Feb/06/2025 16:14:07: copy_option: set client ID (len 14)
Feb/06/2025 16:14:07: copy_option: set elapsed time (len 2)
Feb/06/2025 16:14:07: client6_send: send information request to ff02::1:2%ppp0
Feb/06/2025 16:14:07: dhcp6_reset_timer: reset a timer on ppp0, state=INFOREQ, timeo=0, retrans=987

# 之后都是不断的重试

通过tcpdump抓包,可以发现,实际上远端已经有回复,但是不知何故dhcp6c并没有收到:

root@router:~# tcpdump -ni ppp0 port \(546 or 547\)

tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ppp0, link-type LINUX_SLL (Linux cooked v1), snapshot length 262144 bytes
23:36:23.657373 IP6 fe80::1060:9335:2d8:eba0.546 > ff02::1:2.547: dhcp6 solicit
23:36:23.659679 IP6 fe80::a67b:2cff:fe1f:a1f.547 > fe80::1060:9335:2d8:eba0.546: dhcp6 advertise
23:36:24.703343 IP6 fe80::1060:9335:2d8:eba0.546 > ff02::1:2.547: dhcp6 solicit
23:36:24.705625 IP6 fe80::a67b:2cff:fe1f:a1f.547 > fe80::1060:9335:2d8:eba0.546: dhcp6 advertise

很显然,回包被某个东西拦截住了。于是检查防火墙的配置,发现了这么一条命令:

ip6tables -A INPUT -i ppp0 -p udp --dport 1:1024 -j REJECT

此命令拦截了UDP端口1到1024的入站请求,这其中自然也就包括了UDP 546的入站。因此在这条规则前面插入一条放行546的规则

ip6tables -I INPUT -i ppp0 -s fe80::/10 -p udp --dport 546 -j ACCEPT

此时就可以正常获取到前缀了

为什么说他『反直觉』

首先先来看,我们浏览网页时,网络层面发生的部分流程:

  1. 路由器拿到内网请求,不管你NAT也好什么都好,反正TCP的源端口是可以确定的,这里假设是12345。源地址也是可以确定的,这里假设是1.1.1.1
  2. HTTP服务器2.2.2.2的端口是80
  3. 于是,产生的数据包就是1.1.1.1:12345 => 2.2.2.2:80
  4. 对端收到之后,处理请求,然后回包。回包路径要反过来,那么就是2.2.2.2:80 => 1.1.1.1:12345

现在请思考这么一个问题:方才设置的防火墙规则,不仅仅有UDP,还有TCP,也就是说,入站的TCP80和TCP443同样会被阻断,但是,为什么内网用户依然可以正常访问网页,而不会出现回包被阻断而无法看到网页的情况?

答案很简单,这得益于防火墙的CONNTRACK(连接跟踪)机制。在第三步的时候,防火墙看到,从内网发出来一个1.1.1.1:12345 => 2.2.2.2:80的包,就会把他的信息记录下来。对端回包的时候,防火墙检查存储的信息,发现确实有一条这样的请求被发出去过,于是自然而然会放行

现在我们回到DHCPv6请求PD的场景,尝试套用上面的模板:

  1. 路由器构建请求,假设是[fe80::1]:546
  2. 请求的目标地址是[ff02::1:2]:567
  3. 于是,产生的数据包就是[fe80::1]:546 => [ff02::1:2]:567的包过去(此时防火墙记住了这条请求)
  4. 对端收到之后,处理请求,然后回包。

问题就出在第四步。IPv6中,ff02::1:2是一个组播地址,组播地址不能用作源地址,因此对端路由器回包的时候,源地址实际上是对端的本地链路地址,假设是fe80::2,那么此时回包就变成了[fe80::2]:567 => [fe80::1]:546。你或许已经发现问题所在了:当这个包回到防火墙的时候,因为地址已经改变,防火墙无法找到对应的跟踪条目,只能按照其他规则处理——我们设定的规则正是『阻断端口1到1024的UDP入站』,于是这个包就被防火墙丢弃了,只剩下dhcp6c一脸懵逼

多提一嘴,有人可能会说,为什么我OpenWRT啥都不配置就能直接用啊

道理很简单,OpenWRT已经帮你擦屁股了(详见Ticket #10381):

$ cat /etc/config/firewall

····
config 'rule'
  option 'target' 'ACCEPT'
  option '_name' 'DHCPv6 reply'
  option 'src' 'wan'
  option 'proto' 'udp'
  option 'dest_port' '546'
  option 'family' 'ipv6'
  option 'src_ip' 'fe80::/10'
  option 'src_port' '547'
  option 'dest_ip' 'fe80::/10'
····

写在最后

所以,老实说,使用普通的Linux发行版作为路由器上完全没有问题的,但是....就非常考验你对于Linux和网络技术的了解程度,不然你遇到疑难杂症的时候真的很难去知道问题在哪

以及,再说一遍:善于利用日志,以及抓包工具!

Troubleshooting any problem without the error log is like driving with your eyes closed

在没有错误日志的情况下诊断问题,无异于闭眼开车

(完)


木头箱子脆脆,但是这样正好

如无特殊声明,本站内容均遵循 CC BY-NC-SA 4.0 协议

转载请注明出处并保留作者信息,谢谢!

none
最后修改于:2025年02月10日 21:23

添加新评论

提醒:站长手头紧,没有配备『评论回复邮件提醒』功能
评论后,劳烦您隔一段时间回到本页面查看站长回复(一般都会回)