Linux中多个IPv6地址带来的问题

前景提要

电信提供了公网IPv6地址, 一般情况下无需改动光猫配置. 有时需要通过管理员密码(找宽带安装师傅要)登录光猫后台启用IPv6功能.

根据观测, 大部分家庭宽带默认开启了较高等级的防火墙, 会禁止外部发起IPv6链接. 需要通过管理员密码登录光猫后台找到防火墙配置, 改为更低等级的防护或关闭防火墙.

注意, 对于家庭网络中光猫-路由器-其他设备的场景, 路由器IPv6配置方式可能需要改为 Native 才能使局域网设备也获得公网IPv6. 某些路由器(比如华硕)的 Stateless 配置似乎存在Bug导致只有路由器本身能获取到v6而下级设备无法正确获取地址.

另外根据观测, 不同ISP(例如联通和电信)之间的公网IPv6地址无法实现互通.

问题概述

两个小区之间都接入了电信的家庭宽带, 其中一端(下称A)开启了允许公网访问. 由另一端(下称B)向开启公网访问的这一端发起WireGuard连接.

电信提供的公网IPv6地址似乎在一段时间之后会失效, 但是在失效前一段时间, 会同时并存多个IPv6公网地址, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ip a
...
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
altname enp0s18
inet 192.168.xx.xx/24 brd 192.168.xx.255 scope global ens18
valid_lft forever preferred_lft forever
inet6 240e:xx:xx:xx::xx/128 scope global dynamic noprefixroute
valid_lft 25044sec preferred_lft 25044sec
inet6 240e:xx:321a:xx:xx:xx:xx:xx/64 scope global dynamic mngtmpaddr noprefixroute
valid_lft 177778sec preferred_lft 91378sec
inet6 240e:xx:3216:xx:xx:xx:xx:xx/64 scope global dynamic mngtmpaddr noprefixroute <--- 注意这个地址
valid_lft 91395sec preferred_lft 4995sec
inet6 fe80::xx:xx:xx:xx/64 scope link
valid_lft forever preferred_lft forever
...

大部分情况下这种情况不会持续太久, 但是有时候老的地址会在 valid_lft 之前实际上失效, 即无法通过该IPv6地址收到来自外部的回包, 但仍然能够通过该地址发送IPv6数据包.

由于WireGuard并不能设置ListenAddress这样的东西, 导致从tcpdump看到远端A能够正常收到来自 240e:xx:3216:xx:xx:xx:xx:xx 的握手包且发送了回包, 但是本端(B)无法正常收到回包导致握手失败.

进一步排查, Linux允许同一个接口有多个IPv6地址, 并且会根据 RFC 6724 来选择出网的IPv6地址的. 可以看到其中并不会根据 preferred_lftvalid_lft 进行选择. 而对比到目标地址 240e:xx:3211:... 的最长前缀, 可以发现两个地址到目标地址的最长前缀都是一样的 (0x3211=11001000010001, 0x3216=11001000010110, 0x321a=11001000011010) 因此似乎Linux内核的选择并没有问题, 但实际上会导致通讯一直中断, 直到 240e:xx:3216:xx:xx:xx:xx:xx 这条地址所指定的 valid_lft 归零才会恢复.

解决办法

解决办法有两种:

  1. 手动(或者通过定时任务)删除 valid_lft 更小的IPv6地址
1
sudo ip addr del 240e:xx:3216:xx:xx:xx:xx:xx/64 dev ens18
  1. 强制该接口重新配置IPv6, 这会立刻删除掉该接口的所有IPv6地址, 内核会重新创建 link-local address, 并完成SLAAC配置.
1
2
sudo sysctl -w net.ipv6.conf.ens18.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.ens18.disable_ipv6=0

上述命令会使Linux内核创建 fe80::... 这样的 link-local 地址.

如果没有自动获得公网IPv6地址, 对于使用 systemd-networkd 的系统, 可通过 networkctl 触发配置:

1
2
3
4
5
6
7
$ sudo networkctl
IDX LINK TYPE OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 ens18 ether routable configured

$ sudo networkctl reload
$ sudo networkctl reconfigure ens18

完成后接口将获取到新的IPv6地址. (不一定和之前的不一样, 但只会有一个)

除了使用 networkctl, 还可以使用 rdisc6 (sudo apt install ndisc6) 命令触发 SLAAC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ rdisc6 -1 ens18
Soliciting ff02::2 (ff02::2) on ens18...

Hop limit : 255 ( 0xff)
Stateful address conf. : No
Stateful other conf. : Yes
Mobile home agent : No
Router preference : medium
Neighbor discovery proxy : Yes
Router lifetime : 1800 (0x00000708) seconds
Reachable time : unspecified (0x00000000)
Retransmit time : unspecified (0x00000000)
Prefix : 240e:xx:xx:xx::/64
On-link : Yes
Autonomous address conf.: Yes
Valid time : 2993 (0x00000bb1) seconds
Pref. time : 2993 (0x00000bb1) seconds
Recursive DNS server : 240e:xx:xx:xx:xx:xx:xx:xx
DNS server lifetime : infinite (0xffffffff)
MTU : 1500 bytes (valid)
Source link-layer address: xx:xx:xx:xx:xx:xx
from fe80::f22f:xx:xx:xx