运营商在CGNAT地址空间(100.64.0.0/10)内分配私有IP地址给NAT网关后的客户,来防止与用户内部使用的私有IP地址发生冲突。Tailscale也使用CGNAT地址作为组网设备的IPv4地址。出于安全考虑,Tailscale在Linux上默认会建立一个iptables规则来丢弃所有来自100.64.0.0/10的数据包,防止攻击者假冒私有网络内的设备进行攻击(NVD - CVE-2019-14899,Tailscale issue #3104)。然而,运营商或者一些大型组织也可能使用CGNAT地址为客户提供内部服务,Tailscale添加的丢弃规则将使得运行Tailscale的设备无法访问这些资源。例如,阿里云为其弹性计算服务器(ECS)提供的APT软件包源和DNS服务器就使用100.100.0.0/16网段,ECS上启动Tailscale后就无法解析域名,也无法获取APT软件包。
问题复现
在100.64.0.0/24网段内准备两台Ubuntu 24.04设备:
- ubuntu-2
- IP:
100.64.0.2 - 网段:
100.64.0.0/24
- IP:
- ubuntu-3
- IP:
100.64.0.3 - 网段:
100.64.0.0/24
- IP:
此时,两台机器互相可以ping通。

Tailscale未安装时ubuntu-2可以ping通ubuntu-3
在ubuntu-2上安装Tailscale(此时安装的版本为1.90.4)并连接到已有的Tailscale网络:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
此时,在ubuntu-2上无法再ping通ubuntu-3。使用sudo iptables -L命令查看iptables规则发现,Tailscale添加的规则丢弃了所有来自100.64.0.0/10的数据包。

Tailscale连接后ubuntu-2无法ping通ubuntu-3

Tailscale添加的iptables链ts-input丢弃了所有来自100.64.0.0/10的数据包
断开Tailscale连接之后,在ubuntu-2上又重新可以ping通ubuntu-3。
尝试:手动调整iptables规则
直接使用iptables命令修改规则看似能解决这个问题,但只能临时缓解,不能彻底根除。
运行以下命令在iptables的INPUT链顶部插入对所需网段(此处以100.64.0.0/24为例)的ACCEPT命令:
sudo iptables -I INPUT 1 -s 100.64.0.0/24 -j ACCEPT
此时我们插入的ACCEPT规则会比Tailscale插入的DROP规则具有更高的优先级,ubuntu-2也能正常ping通ubuntu-3。但如果我们使用sudo tailscale down && sudo tailscale up命令断开并重新连接Tailscale网络,Tailscale就会将其自身iptables规则重新插入到列表顶端,我们新增的ACCEPT规则优先级低于Tailscale的DROP规则,从而ubuntu-2再次无法ping通ubuntu-3。

手动添加iptables规则后ubuntu-2能正常ping通ubuntu-3

Tailscale重新连接后iptables规则顺序改变,再次无法ping通ubuntu-3
Tailscale并不向外公开网络连接、断开的事件。当手动运行tailscale down时,tailscaled服务并不会停止,tailscale0网络接口也保持在UP状态,即使我们想要编写脚本来重新插入我们的iptables规则,也很难让脚本在需要的时机自动运行。
最后,运行以下命令删除刚才新增的iptables规则:
sudo iptables -D INPUT -s 100.64.0.0/24 -j ACCEPT
尝试:使用Tailscale的netfilter-mode=off参数
Tailscale提供了netfilter-mode=off参数来禁止Tailscale自动创建iptables规则。这个参数能够彻底防止Tailscale创建规则丢弃我们想要的数据包,但也同时会带来其他问题。
使用sudo tailscale up --netfilter-mode=off命令连接tailscale网络后,可以注意到iptables中没有了有关Tailscale的规则,也可以正常ping通ubuntu-3了。
但如果我们尝试将ubuntu-2设置为Tailscale子网路由,就会发现新的问题。在ubuntu-2上运行以下命令,将ubuntu-2设置为Tailscale子网路由,向Tailscale网络宣告100.64.0.0/24网段的路由:
sudo tailscale up --netfilter-mode=off --advertise-routes=100.64.0.0/24
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
在Tailscale网页端控制台上批准刚才宣告的子网路由,再准备一台位于不同网段(100.64.1.0/24)的Ubuntu 24.04设备:
- ubuntu-4
- IP:
10.0.2.2 - 网段:
10.0.2.0/24
- IP:
使用如下命令在ubuntu-4安装Tailscale并连接到已有的Tailscale网络,同时接受ubuntu-2上宣告的子网路由:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-routes
此时可以发现ubuntu-4能够ping通ubuntu-2,却无法ping通ubuntu-3。因此,使用netfilter-mode=off参数连接Tailscale网络时,子网路由功能不能正常工作。

使用netfilter-mode=off模式,ubuntu-4只能ping通子网路由ubuntu-2本身,但不能ping通ubuntu-3
尝试:使用Tailscale的netfilter-mode=nodivert参数
如果使用Tailscale的netfilter-mode=nodivert参数,Tailscale会创建必要的iptables规则链,但不会在原有的iptables链中增加调用新链的指令。这允许我们确保我们的iptables规则具有比Tailscale规则链更高的优先级,防止Tailscale自动将其自身iptables规则插入到列表顶端。但是,如果我们直接在INPUT链中调用ts-input链,Tailscale会认为该调用规则是其自身创建,因此在使用netfilter-mode=nodivert参数连接Tailscale网络时,Tailscale会删除该调用规则。
使用以下命令连接Tailscale网络并在iptables中新增必要规则,其中INPUT和FORWARD链中的第一条规则用于允许来自子网100.64.0.0/24的数据包,其他规则用于调用Tailscale创建的ts-*链:
sudo tailscale up --netfilter-mode=nodivert --advertise-routes=100.64.0.0/24
sudo iptables -A INPUT -s 100.64.0.0/24 -j ACCEPT
sudo iptables -A INPUT -j ts-input
sudo iptables -A FORWARD -s 100.64.0.0/24 -j ACCEPT
sudo iptables -A FORWARD -j ts-forward
sudo iptables -t nat -A POSTROUTING -j ts-postrouting
此时,ubuntu-4可以ping通ubuntu-2和ubuntu-3了。但是,如果使用sudo systemctl restart tailscaled命令重启tailscaled服务,就会发现刚才新增的iptables规则已经被自动删除。

tailscaled服务重新启动时会删除调用INPUT链中调用ts-input的规则
运行以下命令删除刚才新增的iptables规则:
sudo iptables -D INPUT -s 100.64.0.0/24 -j ACCEPT
sudo iptables -D FORWARD -s 100.64.0.0/24 -j ACCEPT
解决方案:使用Tailscale的netfilter-mode=nodivert参数,并添加间接iptables规则
在ubuntu-2上运行以下命令,设置netfilter-mode=nodivert参数防止Tailscale创建iptables规则随意丢弃数据包,同时将ubuntu-2设置为Tailscale子网路由,向Tailscale网络宣告100.64.0.0/24网段的路由:
sudo tailscale up --netfilter-mode=nodivert --advertise-routes=100.64.0.0/24
之后,不直接在INPUT、FORWARD链中调用ts-*链,而是添加间接iptables链,在这些链中调用ts-*链:
sudo iptables -N pre-ts-input
sudo iptables -A INPUT -j pre-ts-input
sudo iptables -A pre-ts-input -j ts-input
sudo iptables -N pre-ts-forward
sudo iptables -A FORWARD -j pre-ts-forward
sudo iptables -A pre-ts-forward -j ts-forward
sudo iptables -t nat -N pre-ts-postrouting
sudo iptables -t nat -A POSTROUTING -j pre-ts-postrouting
sudo iptables -t nat -A pre-ts-postrouting -j ts-postrouting
sudo ip6tables -N pre-ts-input
sudo ip6tables -A INPUT -j pre-ts-input
sudo ip6tables -A pre-ts-input -j ts-input
sudo ip6tables -N pre-ts-forward
sudo ip6tables -A FORWARD -j pre-ts-forward
sudo ip6tables -A pre-ts-forward -j ts-forward
sudo ip6tables -t nat -N pre-ts-postrouting
sudo ip6tables -t nat -A POSTROUTING -j pre-ts-postrouting
sudo ip6tables -t nat -A pre-ts-postrouting -j ts-postrouting
最后,在INPUT链和FORWARD链中根据需要新增规则,用于允许来自子网100.64.0.0/24的数据包。其中,设备若需要与100.64.0.0/10网段内设备通信,则应为所有相关的对端地址在INPUT链添加放行规则;若设备需要作为子网路由宣告位于100.64.0.0/10网段内的路由,则相关网段需要在FORWARD链内被放行。以当前实验为例,ubuntu-2需要与100.64.0.0/24网段内的ubuntu-3通信,则INPUT链需要放行该网段;ubuntu-2还需要作为路由器路由100.64.0.0/24网段,因此FORWARD链也需要放行该网段:
sudo iptables -I INPUT 1 -s 100.64.0.0/24 -j ACCEPT
sudo iptables -I FORWARD 1 -s 100.64.0.0/24 -j ACCEPT
此时进行测试,ubuntu-2可以正常ping通ubuntu-3,网络功能恢复正常;ubuntu-4也可以ping通ubuntu-2和ubuntu-3了,子网路由功能也正常运行。使用sudo systemctl restart tailscaled命令重启tailscaled服务后,相关功能仍然能正常使用。
为了使我们新增的iptables规则在重启时能够保留,需要安装iptables-persistent包并保存当前的规则(iptables-persistent包与ufw包存在功能冲突,若需要使用ufw包,则不能使用该方法保存iptables规则):
sudo apt install iptables-persistent
sudo netfilter-persistent save
之后,即使重启ubuntu-2,iptables规则也会保留,ubuntu-2能正常访问100.64.0.0/24网段,Tailscale子网路由功能也能正常运行。
总结
当安装了Tailscale的设备需要与CGNAT网段内设备通信,或是需要作为子网路由宣告位于CGNAT网段内的路由时,需要执行以下操作:
- 设置
netfilter-mode=nodivert参数防止Tailscale创建iptables规则丢弃必要的数据包; - 使用间接iptables链来调用Tailscale创建的规则,防止Tailscale自动删除位于
INPUT、FORWARD链中的调用ts-*链的iptables规则; - 手动在
INPUT链和FORWARD链中新增规则,用于允许来自所需子网的数据包; - 使用iptables-persistent包持久化iptables规则。