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
2
3
4
[  5] local 192.168.x.2 port 60660 connected to 100.x.x.x port 5201
[ ID] Interval Transfer Bitrate Total Datagrams
[ 5] 0.00-1.00 sec 1.14 MBytes 9.1 Mbits/sec 760
[ 5] 1.00-2.00 sec 573.00 KBytes 4.6 Mbits/sec 382

速度就很正常,但是切到TCP就惨不忍睹了

1
2
3
4
5
6
7
perf3-windows-x86_64.exe client 100.x.x.x --reverse
Connecting to host 100.x.x.x, port 5201
[ 5] local 192.168.x.2 port 11215 connected to 100.x.x.x port 5201
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-1.01 sec 28.52 KBytes 0.2 Mbits/sec
[ 5] 1.01-2.02 sec 19.84 KBytes 0.2 Mbits/sec
[ 5] 2.02-3.03 sec 19.84 KBytes 0.2 Mbits/sec

但是如果在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特有的处理环节。

走过的弯路

排错过程中试了一大堆东西,全部无效,列一下给后来人避坑:

  1. ISP QoS限速 — 路由器自身下载20.9 Mbps,不是运营商的锅(运营商:???背了大半年黑锅)
  2. IPv6地址选择 — 试了preferred_lft 0弃用br-lan地址,Tailscale不吃这套
  3. MTU/PMTUD — tailscale0 MTU 1280,客户端降到1280也没用,fw3已经配好了MSS钳制
  4. 强制DERP中继TS_DEBUG_DISABLE_DIRECT=true,走中继也一样卡
  5. conntracktcp_be_liberal=1已经设了,nf_conntrack_checksum=0也关了,conntrack表也没满
  6. Qualcomm ECM硬件加速rmmod ecm卸了也没用
  7. 关闭MASQUERADE — 这个最离谱,关了masq以后TCP/UDP直接完全不通了。Tailscale的packet filter会在SNAT之前检查源IP,192.168.x.x不是Tailscale地址直接给你丢了。所以masq是必须的,没得选
  8. 关闭tailscale0的GRO/GSO/TSOethtool -K tailscale0 gso off gro off tso off,没用,内核转发路径内部还是会做GSO聚合
  9. iptables NOTRACK — QWRT的iptables v1.8.3 legacy版连NOTRACK target都没有,CT --notrack直接把连接搞断了

基本上能想到的都试了个遍,每次都满怀希望然后失望。

关键突破:br-lan抓包

最后在br-lan上抓包的时候发现了端倪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2480字节的段 → 客户端没收到
15:33:33.76921 seq 112:2592 length 2480 ← 丢了
15:33:33.770045 seq 2592:5072 length 2480 ← 丢了
15:33:33.770122 seq 5072:7552 length 2480 ← 丢了
15:33:33.770192 seq 7552:10032 length 2480 ← 丢了

# 1240字节的段 → 客户端正常收到
15:33:33.793373 seq 14992:16232 length 1240 ← 收到了!

# 客户端SACK:我只收到了14992:16232这一个段,前面的全没收到
15:33:33.793511 ack 112, sack {14992:16232}

# 重传变成1240字节 → 收到了
15:33:33.800682 seq 112:1352 length 1240 ← 收到了!

看到这个抓包结果的时候人都傻了。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
2
3
for iface in br-lan $(ls /sys/class/net/br-lan/brif/ 2>/dev/null); do
ethtool -K "$iface" gso off tso off gro off 2>/dev/null
done

关闭br-lan及其底层物理端口的GSO/TSO/GRO,强制内核在发送前完成软件分段。

持久化写到/etc/rc.localexit 0之前就行:

1
2
3
4
# Fix: 关闭br-lan GSO/TSO 防止Tailscale转发路径产生超大TCP帧
for iface in br-lan $(ls /sys/class/net/br-lan/brif/ 2>/dev/null); do
ethtool -K "$iface" gso off tso off gro off 2>/dev/null
done

关闭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真的是排查网络问题的神器,早该跑的。