家里的智能音箱语音功能失灵了:唤醒有反应,指令说完之后一直转圈,最后报错。同一个网络下手机、电脑、电视全都正常。
排查花了两轮才定案。中间推翻过一次结论,最后的根因和最初的怀疑方向完全无关。
最反常的线索是「没有线索」
代理软件的日志一片干净。error 计数 0,timeout 计数 0,和音箱那个 IP 相关的异常记录一条都没有。
但音箱确实在不停重试——把日志级别临时提到 info 之后,能看到语音上传的域名在 30 秒内被请求了 7 次,另一个上报域名 4 秒内 3 次。典型的应用层超时重试循环。
这里有个容易忽略的事实:代理软件只管连接能不能建立。 连上了就算成功,之后数据传不传得动它不关心。所以「连接成功、数据卡死」这种故障,在它的日志里就是一片祥和。
日志干净不等于没问题。有时候恰恰相反——它把范围缩小了:问题不在连接建立阶段,而在数据传输阶段。
换个模式就好了,这条线索价值最大
第一轮我怀疑域名嗅探和 DNS 规则,补了一堆过滤规则,还修了三处配置里的引号错误。配置确实修对了,但音箱依旧报错。
真正有用的线索是试出来的:把代理从 TUN 模式换成混合模式,一切正常。
这条线索的价值在于它能整类排除:
域名嗅探、fake-ip 过滤、DNS 处理这些逻辑,在两种模式下行为完全一致。既然换个模式就好了,那它们就都解释不了这个现象,可以整体划掉——不用再一条条试。
两种模式唯一的区别是 TCP 的数据通路:
| 模式 | TCP 路径 | UDP 路径 |
|---|---|---|
| TUN 模式 | 走 utun 虚拟网卡 | 走 utun |
| 混合模式 | 走 NAT 重定向,不碰 TUN | 走 utun |
再补一个观察:音箱的 UDP 在两种模式下都正常。UDP 都走 utun 却没事,TCP 走 utun 才出事——范围一下子收窄到「TCP 经过 utun 时发生了什么」。
根因:9000 的 MTU
看一眼网卡:
utun: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 9000
eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
utun 的 MTU 是 9000,而真实链路只有 1500。
在 stack: system 模式下,内核在 utun 上终结客户端的 TCP 连接。握手时它按 utun 的 MTU 通告 MSS——9000 - 40 = 8960。
音箱收到「对端能接收 8960 字节的段」,就照着发接近 9000 字节的大包。这些包走到真实的 Wi-Fi 链路上,1500 的 MTU 根本装不下,被静默丢弃。
于是所有症状都对上了:
- 控制面请求正常——查账号、拉配置,报文都在 1500 以内,畅通无阻
- 语音上传卡死——payload 大,一超过 1500 就没了
- 日志干净——连接建立成功过,代理软件认为一切正常
一个设备只有大包传不过去,小包没事。这种「半通不通」比完全不通难查得多。
修复:MSS clamp
既然问题出在通告了过大的 MSS,那就在 SYN 包经过时把它改小:
TUN_DEV="utun"; TUN_MSS="1400"
for CH in FORWARD OUTPUT; do
iptables -t mangle -D "$CH" -o "$TUN_DEV" -p tcp \
--tcp-flags SYN,RST SYN -j TCPMSS --set-mss "$TUN_MSS" 2>/dev/null
iptables -t mangle -A "$CH" -o "$TUN_DEV" -p tcp \
--tcp-flags SYN,RST SYN -j TCPMSS --set-mss "$TUN_MSS" 2>/dev/null
done
两个细节值得说:
先 -D 再 -A 保证幂等。 删除不存在的规则会报错,用 2>/dev/null 吞掉即可。这样脚本重复执行不会堆叠出一串重复规则。
这里不能用 --clamp-mss-to-pmtu。 这是最反直觉的一点——那个参数是「按路径 MTU 自动调整」,听起来正是我们要的。但 utun 这条路由的 PMTU 本身就是 9000,自动 clamp 出来还是 8960,等于没改。必须用 --set-mss 显式指定数值。
1400 是留了余量的保守值:1500 减去 IP 头 20、TCP 头 20 得 1460,再往下留 60 字节给可能存在的隧道封装。
装上之后,语音上传域名的请求频率从「每分钟 2 到 5 次」降到「一次交互一次连接」,重试循环消失,音箱恢复正常。
为什么默认是 9000,以及为什么只有音箱出事
TUN 不是真实链路,它是内核与代理进程之间的交接口。数据每跨越一次就是一次系统调用加一次内存拷贝。MTU 越大,每传输一兆数据所需的系统调用次数越少,吞吐更高。所以主流代理内核照搬巨型帧的思路,把默认 MTU 定在 9000。
在设计预期的部署方式里,这没有任何问题:TUN 跑在客户端自己身上,代理终结 TCP 之后另开一条出站连接,MSS 在真实网卡上重新协商。9000 这个数字从来不会出现在网络上。
旁路由拓扑打破了这个前提。 客户端是另一台机器,和 TUN 之间隔着一跳真实的 1500 链路,却拿到了源自 9000 的 MSS 通告。
那么这算不算代理软件的 bug?严格讲不算。
按 TCP 的规矩,发送方应该取 min(对端通告的 MSS, 本机 MTU - 40)。一个规矩的协议栈拿到 8960,看看自己的网卡只有 1500,最终也只会发 1460 字节的段。
家里其他设备全都没事,正说明这一点。 手机、电脑、电视的 TCP 栈都按规矩办事,只有这台音箱把通告值当真了——嵌入式设备的网络栈裁剪得比较狠,这种简化并不罕见。
准确的说法是:9000 这个默认值在旁路由拓扑下,暴露了某些设备协议栈的不严谨,而 MSS clamp 把这个暴露面堵上了。
责任在谁其实不重要。重要的是:你没法要求全世界的智能设备都把 TCP 栈写对,但你可以在自己的路由器上加一条规则。
几条可复用的经验
日志干净是一种信息,不是没有信息。 它排除了「连接建立失败」这一整类原因,把范围推向了数据传输阶段。
能整类排除的线索,比能定位具体问题的线索更值钱。 「换个模式就好」一句话划掉了三分之二的可能性,比逐条试配置快得多。
MTU 问题的典型特征是「小包通、大包不通」。 如果某个服务的轻量请求正常、重载请求卡死,先去量 MTU,别在应用层绕。
在虚拟网卡上,--clamp-mss-to-pmtu 可能是无效的。 它依赖路由的 PMTU,而虚拟网卡的 PMTU 就是那个过大的值本身。
排查时先问「两种情况下什么不一样」,而不是「哪里配错了」。 后者容易陷入逐条排查配置的泥潭——我第一轮就是这么浪费掉的,而且中途还因为「配置确实有错」被误导,以为找到了原因。
配置有错,和这个错是不是当前故障的原因,是两回事。