QWRT tailscale subnet路由填坑笔记
被tailscale subnet路由性能卡的要死的情况折腾了大半年,实在忍不了了而且春节也是闲的没事就尝试解决一下。
首先这感觉就是被运营商QoS到死,又因为自有国情在此,这两年运营商大搞网间限速,我甚至到昨天晚上找到rootcause是subnet路由之前都以为是运营商QoS限速导致的。而且这感觉上也是,我连另一个子网的路由,卡的连个html都要加载好几分钟。以至于在我心里运营商背了大半年的黑锅。最后发现其实是因为去年9月份从asuswrt的大螃蟹换到了qwrt的地表最有性价比的小米BE10000以后没仔细调过tailscale的转发,只是本着能跑就行的态度,给自己挖了个大坑。
直到最近tailscale实现了peer relay,我在测试的时候发现,强行relay到peer也卡,但是我pc上开就不卡,一过本地qwrt路由就完蛋。跟AI聊了一下AI一开始也猜是因为不同ipv6 地址策略不一样。结果一步一步排错下来,AI给了个意见说让perf测一下,我觉得有道理,结果一测,草,问题一目了然了。
太久没配网连这最基本的怎么排查问题的测试都忘了做了,实在是草
node A(perf服务端) node B(QWRT + tailscale) node C(PC没跑tailscale)
在C上跑rperf3-windows-x86_64.exe client 100.x.x.x –reverse -u
1 | [ 5] local 192.168.x.2 port 60660 connected to 100.x.x.x port 5201 |
速度就很正常,但是切到TCP就惨不忍睹了
1 | perf3-windows-x86_64.exe client 100.x.x.x --reverse |
但是如果在node B上面跑tcp和udp测试就都正常。
好了 rootcause缩小到TCP子网转发问题。
前方AI生成预警,懒得写了,主要是留个笔记,别人踩坑的时候可以参考一下。
排错
问题精确到:路由器上 tailscale0 → FORWARD → br-lan 路径,仅影响TCP,不影响UDP。
| 测试路径 | 方向 | 协议 | 速度 |
|---|---|---|---|
| 路由器(100.x) → 目标 | upload | TCP | 60.8 Mbps ✅ |
| 目标 → 路由器(100.x) | download | TCP | 20.9 Mbps ✅ |
| 子网设备(192.168.x.2) → 目标 | upload | TCP | 54.8 Mbps ✅ |
| 目标 → 子网设备(192.168.x.2) | download | TCP | 0.2 Mbps ❌ |
| 目标 → 子网设备(192.168.x.2) | download | UDP | 正常 ✅ |
路由器自己收TCP没问题,子网设备发TCP也没问题,唯独子网设备收TCP就完蛋。UDP完全正常。这说明转发路径本身没有带宽瓶颈,问题出在某个TCP特有的处理环节。
走过的弯路
排错过程中试了一大堆东西,全部无效,列一下给后来人避坑:
- ISP QoS限速 — 路由器自身下载20.9 Mbps,不是运营商的锅(运营商:???背了大半年黑锅)
- IPv6地址选择 — 试了
preferred_lft 0弃用br-lan地址,Tailscale不吃这套 - MTU/PMTUD — tailscale0 MTU 1280,客户端降到1280也没用,fw3已经配好了MSS钳制
- 强制DERP中继 —
TS_DEBUG_DISABLE_DIRECT=true,走中继也一样卡 - conntrack —
tcp_be_liberal=1已经设了,nf_conntrack_checksum=0也关了,conntrack表也没满 - Qualcomm ECM硬件加速 —
rmmod ecm卸了也没用 - 关闭MASQUERADE — 这个最离谱,关了masq以后TCP/UDP直接完全不通了。Tailscale的packet filter会在SNAT之前检查源IP,192.168.x.x不是Tailscale地址直接给你丢了。所以masq是必须的,没得选
- 关闭tailscale0的GRO/GSO/TSO —
ethtool -K tailscale0 gso off gro off tso off,没用,内核转发路径内部还是会做GSO聚合 - iptables NOTRACK — QWRT的iptables v1.8.3 legacy版连NOTRACK target都没有,
CT --notrack直接把连接搞断了
基本上能想到的都试了个遍,每次都满怀希望然后失望。
关键突破:br-lan抓包
最后在br-lan上抓包的时候发现了端倪:
1 | # 2480字节的段 → 客户端没收到 |
看到这个抓包结果的时候人都傻了。2480字节的段全丢,1240字节的段全收到。2480 = 2 × 1240,正好是两个MSS。重传的时候TCP会退回到单MSS发送,所以重传包是1240字节,就能收到。
这就是为什么速度只有0.2 Mbps——只有重传包能到达客户端,正常发送的数据全被丢了。
Root Cause
tailscale0的MTU是1280,减去IP+TCP头40字节,MSS是1240。数据从tailscale0进来的时候是一个个1240字节的TCP段,但是内核的转发路径会通过GSO(Generic Segmentation Offload)把多个段聚合成一个大段来提高效率。两个1240聚合成2480,加上40字节头就是2520字节的IP包。
问题来了:br-lan的MTU是1500。2520 > 1500。
正常情况下内核应该在发送前把这个GSO大段重新切分成小段,但是br-lan是个软件桥接口,它底下的物理端口没有正确执行这个分段。结果就是2520字节的超大以太网帧直接被扔到了线路上,客户端网卡一看这帧超过MTU了,静默丢弃,连个错误都不报。
为什么只影响TCP下行转发?
- TCP download:GSO聚合TCP段为超大帧 → 客户端丢弃 ❌
- UDP download:UDP包不会被GSO聚合,每个包独立且≤MTU ✅
- TCP upload:客户端发送的段本来就是正确大小,不需要分段 ✅
- TCP直连路由器:走INPUT路径,本地协议栈内部处理GSO,不经过物理线路 ✅
完美解释了所有现象。
修复
一行命令:
1 | for iface in br-lan $(ls /sys/class/net/br-lan/brif/ 2>/dev/null); do |
关闭br-lan及其底层物理端口的GSO/TSO/GRO,强制内核在发送前完成软件分段。
持久化写到/etc/rc.local的exit 0之前就行:
1 | # Fix: 关闭br-lan GSO/TSO 防止Tailscale转发路径产生超大TCP帧 |
关闭GSO/TSO对路由器性能基本没影响。br-lan本身就是软件桥接,没有硬件TSO。关了以后只是把分段时机提前到内核协议栈内,CPU开销可以忽略。如果开了硬件flow offloading,大部分WAN↔LAN流量直接绕过内核协议栈,更不受影响。
总结
折腾了大半年的tailscale subnet路由TCP龟速问题,根因居然是br-lan的GSO没有正确分段,导致超大TCP帧被客户端静默丢弃。UDP不受影响是因为UDP包不会被GSO聚合。上行不受影响是因为客户端发出的包本来就是正确大小。路由器自身不受影响是因为本地协议栈不走物理线路。
这个问题的坑在于:tcpdump在br-lan上能抓到这些2480字节的包(因为tcpdump在GSO分段之前捕获),所以看起来包是发出去了,但实际上到了线路上就是超大帧,客户端直接丢了。如果不仔细对比包大小和客户端的SACK响应,根本发现不了。
教训:换路由器以后一定要跑一遍完整的网络测试,不要本着”能跑就行”的态度。还有就是iperf3真的是排查网络问题的神器,早该跑的。