Tailscale 因 DHCPv6 地址导致无法直连排查记录
在手机上通过 tailscale 访问 Armbian 设备,始终无法建立 Direct Connection。最终定位到根因是 DHCPv6 分配的 /128 地址被内核选为默认源地址,导致 IPv6 回包无法送达。在 Netplan 中添加 ethernets 段并指定 ipv6-address-generation: stable-privacy 后解决。
问题
- 设备:Armbian ARM 盒子(ab01)
- 网络管理:Netplan + NetworkManager
- 问题:从手机访问 ab01 时,Tailscale 连接始终走 DERP 中继(欧洲节点,~450ms),
tailscale netcheck显示IPv6: no, unavailable in OS - 对比:同局域网的 Linux x86桌面电脑(w01)却能正常直连
排查过程
确认 IPv6 地址和路由都正常
$ ip -6 addr show eth0 | grep inet6 inet6 2408:xxxx:xxxx:xxxx::1001/128 scope global dynamic noprefixroute inet6 2408:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/64 scope global dynamic noprefixroute inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 scope link noprefixroute
$ ip -6 route show defaultdefault via fe80::xxxx:xxxx:xxxx:xxxx dev eth0 proto ra metric 100 pref medium设备同时拿到 /128(DHCPv6)和 /64(SLAAC)两个公网地址。路由和防火墙均无异常。
定位到“能发不能收”
先用 nc -6 -u 测试 UDP 发送:
$ nc -6 -u -z -w 3 2400:3200::1 53 && echo "UDP OK" || echo "UDP FAIL"Connection to 2400:3200::1 53 port [udp/domain] succeeded!UDP OK但这只能证明“包发出去了”,不能证明“能收到回包”。UDP 是无连接协议,-z 只是尝试发送,没有握手确认。
于是用一个 Python 脚本做更完整的测试:向 IPv6 的公共 DNS 服务器(2400:3200::1)发送一个 DNS 查询包,然后等待接收服务器返回的结果。如果收到回应,说明 IPv6 收发都正常;如果超时,说明回包进不来。
$ python3 -c "import sockets = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s.settimeout(5)query = b'\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05baidu\x03com\x00\x00\x01\x00\x01'try: s.sendto(query, ('2400:3200::1', 53)) data, addr = s.recvfrom(512) print('收到回应,长度:', len(data))except Exception as e: print('未收到回应:', e)finally: s.close()"未收到回应: timed out结果是超时,定位到问题是“能发不能收”。
找到根因:源地址选错
$ python3 -c "import sockets = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s.connect(('2400:3200::1', 53))print('本地地址:', s.getsockname())s.close()"本地地址: ('2408:xxxx:xxxx:xxxx::1001', 49906, 0, 0)内核默认选了 /128 的 DHCPv6 地址作为源地址。该地址没有对应的 on-link 路由,回包到达光猫后无法送回设备。
强制绑定 SLAAC 的 /64 地址发送同样的 DNS 查询,立刻收到回应,确认根因。
$ python3 -c "import sockets = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)s.settimeout(5)s.bind(('2408:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx', 0))query = b'\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x05baidu\x03com\x00\x00\x01\x00\x01'try: s.sendto(query, ('2400:3200::1', 53)) data, addr = s.recvfrom(512) print('收到回应,长度:', len(data))except Exception as e: print('未收到回应:', e)finally: s.close()"收到回应,长度: 91解决方案
确认系统用的是 Netplan:
$ ls /etc/netplan/00-default-use-network-manager.yaml原始文件只有:
network: version: 2 renderer: NetworkManager在文件中添加 ethernets 段,为 eth0 指定 IPv6 配置:
network: version: 2 renderer: NetworkManager ethernets: eth0: dhcp4: true dhcp6: false accept-ra: true ipv6-address-generation: stable-privacy应用配置:
netplan applyipv6-address-generation: stable-privacy 明确告诉 NetworkManager 使用 RFC7217 方式生成 SLAAC 地址的接口标识符。这个参数能被 Netplan 正确识别并翻译给 NetworkManager,从而完整触发 SLAAC 流程,绕过了 accept-ra 翻译丢失的问题。
验证
ip -6 addr show eth0 | grep inet6 # 确认 IPv6 地址存在tailscale netcheck # 确认 IPv6: yestailscale ping ab01 # 确认直连建立重启设备,再确认一次。
发现
手机流量可以走 IPv6 直连
排查中发现:用手机流量连接 ab01 时,有时能建立 IPv6 直连,有时却走中继。原因是:
- 手机流量通常有公网 IPv6:4G/5G 网络普遍下发 IPv6 地址,且没有 NAT 转换。如果 ab01 也有可用的公网 IPv6,双方直接通过 IPv6 互通,不需要打洞。
- ab01 缺少公网 IPv6 时:手机只能靠 IPv4 打洞。但手机流量的 IPv4 通常处于严格的 CGNAT 后面,打洞成功率低,所以回退到中继。
- 同局域网内设备表现不同:w01 能直连而 ab01 不能,正是因为 w01 有公网 IPv6,而 ab01 当时没有。
不对称直连是正常现象
Tailscale 的每条连接都是双向独立建立的。两端各自尝试用自己能用的最佳路径去连对方,最终可能形成一条“不对称”的连接路径(一端走 IPv4,另一端走 IPv6)。只要至少一个方向能直连,整体连接就不会走中继。
总结
Tailscale 的 ipv6os=false 不一定代表系统没有 IPv6,也可能是源地址选择错误导致 UDP 回包收不到。排查时重点验证 UDP 的接收能力,而不只是发送。
在 Armbian + Netplan + NetworkManager 环境下,accept-ra: true 可能无法正确触发 SLAAC。在 Netplan 中添加 ethernets 段并指定 ipv6-address-generation: stable-privacy,可以显式引导 NetworkManager 走 SLAAC 流程,是持久有效的解决方案。