从机场节点全部 Timeout 到 iOS VPN 路由:一次 iPhone 热点故障排查
Windows 连接 iPhone 个人热点后,机场节点全部 Timeout,但部分网站和 IPv6 仍然正常。一次从 DNS、IPv4/IPv6 到 Shadowrocket VPN 路由的真实排障,最终定位到遗留的 Include All Networks 设置。
今天在外面用电脑时,我遇到了一个相当诡异的网络问题。
Windows 正常连接着 iPhone 的个人热点,B 站可以打开,机场订阅也能够正常更新,但代理客户端里的所有节点却突然全部变成了 Error 或 Timeout。
一开始我自然把注意力放在了代理本身:
是不是机场挂了?是不是当前移动网络把节点端口封了?是不是 DNS 有问题?还是 Windows 上代理客户端的 TUN 模式出了什么毛病?
折腾了一圈之后,我才发现真正的问题比这些都更底层:
电脑通过 iPhone 热点时,IPv6 是通的,但 IPv4 基本已经断了。
而最终的罪魁祸首,是我几个月前为了另一个问题打开、随后彻底忘记了的 Shadowrocket 设置:
Include All Networks关闭它以后,IPv4 立即恢复,Windows 上的机场节点也全部恢复正常。
这次排障比较有意思的地方,不是“Shadowrocket 有一个坑人的开关”,而是整个问题是怎么从一个非常模糊的 Timeout,一步步收敛到一个历史遗留设置上的。
最开始看到的是:
Windows
↓
iPhone Personal Hotspot
↓
代理客户端
所有节点 → Error / Timeout如果只看这一层,最容易产生的判断当然是:
机场是不是炸了?
但与此同时,又有两个很奇怪的现象。
第一,机场订阅可以正常更新。
第二,B 站之类的网站仍然可以正常访问。
也就是说,这台电脑并没有真正意义上的“断网”。
这其实是第一个非常重要的线索。
如果网络完全不通,那么问题空间反而很小;但现在的情况是:
一些连接 ✓
另一些连接 ✗问题就从:
“电脑有没有网?”
变成了:
“到底是哪一条网络路径还在工作,哪一条已经坏掉了?”
排障过程中,一个比较明显的异常首先出现在 DNS 上。
我尝试:
Test-NetConnection cn.bing.com -Port 443一度得到了域名解析失败。
于是继续查看:
Resolve-DnsName cn.bing.com
Get-DnsClientServerAddress
nslookup cn.bing.com后来 Resolve-DnsName 和 nslookup 又能够正常返回结果。
Windows 当时从 iPhone 热点获得的 DNS 是一个 IPv6 link-local 地址:
fe80::xxxx:xxxx:xxxx:xxxx所以当时一个很自然的假设是:
iPhone Hotspot
↓
IPv6 DNS
↓
Windows DNS 解析不稳定
↓
节点域名无法解析
↓
代理 Timeout这个方向并不荒谬。
DNS 的确出现了异常表现。
问题在于:异常现象不等于根因。
后来事实证明,DNS 更像是整个网络状态异常以后暴露出来的一个伴随症状。即使把 DNS 修好,也解释不了为什么所有 IPv4 网络连接本身都存在问题。
这是这次排障给我的第一个提醒:
当一个测试出现异常时,不要急着把它提升成“根因”。真正的根因应该能够解释尽可能多的现象。
继续排查之后,我发现了一个比 DNS 重要得多的现象:
IPv6 → 正常
IPv4 → 异常这一下,整个问题的性质发生了变化。
之前我面对的是一个非常模糊的:
“机场不能用”现在则变成了一个非常具体的:
“iPhone 热点下的 IPv4 路径有问题”这也能解释之前那些看起来互相矛盾的现象。
比如为什么 B 站还能打开、为什么部分域名还能解析、为什么订阅服务器仍然能够连接。
现代互联网并不是所有服务都只通过 IPv4 工作。一个网站可能同时拥有 IPv4 和 IPv6 地址,也可能挂在支持 IPv6 的 CDN 后面。
所以完全可能出现:
某网站
↓
IPv6
↓
成功 ✓而:
某机场节点
↓
IPv4
↓
失败 ✗这意味着“能打开网页”本身并不能证明 IPv4 正常。
同样,“订阅可以更新”也不能证明机场节点一定可达。
机场订阅服务器和真正的代理节点,本来就可能是完全不同的机器:
订阅:
subscription.example.com
节点:
node01.example.com:443前者可能拥有 IPv6、使用 CDN,后者却可能只有一个 IPv4 地址。
于是就会产生这种看似离谱、实际上完全可能成立的状态:
订阅服务器
↓ IPv6 / CDN
成功 ✓
代理节点
↓ IPv4
失败 ✗这也是这次故障里一个很容易被错误信息带偏的地方。
客户端显示:
Timeout很容易让人开始研究:
但假设一个节点实际上是:
node.example.com
↓
123.123.123.123:443在进行任何代理协议握手之前,Windows 至少要先能够把数据发送到:
123.123.123.123:443如果 IPv4 本身已经不通,那么整个过程可能在最前面的 IP/TCP 连接阶段就失败了。
可以粗略理解成:
Windows
↓
IPv4 数据包
↓
iPhone Hotspot
↓
× 无法正常转发这时 Trojan、VLESS 或 TLS 根本还没有真正发挥作用。
最终应用层能告诉我的只有一句:
Timeout所以:
应用显示出的错误,是故障最终传播到这一层以后的结果,不代表问题就发生在这一层。
这也是网络排障里很重要的一点。
真正把问题彻底收缩下来的,不是什么复杂的命令。
而是一个非常简单的操作:
关闭 iPhone 上的 Shadowrocket。
结果:
Shadowrocket ON
→ Windows IPv4 ✗
Shadowrocket OFF
→ Windows IPv4 ✓再打开:
Shadowrocket ON
→ IPv4 又出问题到这里,问题其实已经非常明确了。
因为只改变了一个变量:
Shadowrocket而故障会稳定地跟着这个变量出现和消失。
这比此前 DNS、MTU、运营商、节点协议之类的猜测都更有价值。
于是问题空间一下从:
Windows
DNS
机场
运营商
MTU
TUN
代理协议
iPhone收缩成:
iPhone
↓
Shadowrocket
↓
VPN / 路由配置后来回过头看,这是整次排障真正决定性的转折点。
真正有效的排障不一定是测更多参数,而是找到一个能够稳定控制故障出现和消失的变量。
理解后面的 Include All Networks 之前,需要先把网络拓扑理清楚。
不开 VPN 时,可以把 iPhone 热点粗略理解成:
Windows
↓ Wi-Fi
iPhone Personal Hotspot
↓
IP 转发 / 地址转换
↓
蜂窝网络
↓
InternetWindows 并没有直接接入移动运营商网络。
它实际上把 iPhone 当成了自己的上游网络设备。
因此开个人热点之后,iPhone 除了是一台手机,还临时承担了一部分“路由器”的职责:
Windows
│
▼
iPhone
│
▼
运营商网络具体到底涉及 IPv4 NAT、运营商 CGNAT、IPv6、NAT64 还是其他机制,会随着运营商和网络环境变化,所以这里没有必要把 iOS 内部实现武断地简化成某一种固定结构。
重点只是:
热点客户端的流量需要经过 iPhone 的网络栈才能继续前往互联网。
Shadowrocket 开启以后,iPhone 自己的网络结构又多了一层。
粗略表示:
iPhone App
↓
iOS 网络栈
↓
Network Extension / VPN Tunnel
↓
Shadowrocket
↓
代理节点
↓
InternetApple 的 Network Extension 框架允许 VPN 应用创建和配置网络隧道,从而改变网络流量的路由方式。
所以此时一台 iPhone 上实际上同时存在两件事:
iPhone 自己的流量
↓
Shadowrocket VPN
↓
Internet和:
Windows
↓
Personal Hotspot
↓
iPhone
↓
Internet也就是说:
┌─ VPN 客户端
iPhone ─┤
└─ 热点路由器两个功能同时工作。
真正的问题也正是在这里出现的。
Include All Networks在 Shadowrocket 的高级设置里,我最终发现:
Include All Networks = ON看到这个开关以后,我马上想起来了。
之前为了让 iPhone 上 Telegram 的推送更稳定,我曾经专门打开过它。
然后——忘记关了。
Apple 对 includeAllNetworks 的定义非常直接:这是一个决定系统是否将大部分网络流量发送到 VPN tunnel 的布尔属性。对于 Packet Tunnel,Apple 进一步说明,开启后网络流量会被导向隧道,同时存在一些系统规定或可配置的例外。
Apple 还专门提供了 excludeLocalNetworks、excludeCellularServices、excludeAPNs 等属性,用来在 includeAllNetworks 开启后排除某些类型的流量。
所以一个比较准确的理解是:
普通状态更接近:
iOS 网络流量
├── VPN 路径
└── 其他系统网络路径开启 Include All Networks 以后,则更接近:
iOS 网络流量
↓
更广泛的 VPN 路由控制
↓
VPN Tunnel不过这里需要非常明确地区分事实和推测。
这次我能够通过实验确认的是:
Include All Networks = ON
↓
在我的网络环境中
Windows 热点 IPv4 故障
Include All Networks = OFF
↓
Windows IPv4 恢复也就是说,这个设置和这次故障之间的因果关系在我的设备和当时的环境里得到了 A/B 验证。
但我不能仅凭这些现象就断言:
“iOS 内核具体在某一个 NAT 表里发生了某种错误。”
从现象来看,更合理的表述是:
Include All Networks扩大了 VPN 对系统网络流量的路由控制范围,而它与 Personal Hotspot 的 IPv4 转发路径在我的环境中产生了异常交互。
至于为什么偏偏 IPv4 出问题而 IPv6 仍然正常,以及问题究竟发生在 iOS 网络栈的哪一个具体环节,仅靠这次黑盒排障还不足以确定。
把:
Include All Networks关闭。
然后重新建立网络连接。
Windows 的 IPv4 恢复。
机场客户端原本:
Error
Timeout
Timeout
Timeout的节点,也重新出现正常延迟。
整个因果链终于闭环:
Windows 连接 iPhone 热点
↓
机场全部 Timeout
↓
发现并非完全断网
↓
DNS 出现异常,但解释不了全部现象
↓
发现 IPv6 ✓ / IPv4 ✗
↓
Shadowrocket OFF → IPv4 ✓
↓
问题锁定 Shadowrocket VPN 路由
↓
发现 Include All Networks = ON
↓
关闭
↓
IPv4 ✓
↓
机场节点 ✓到这里,前面一些看似重要的症状也终于有了统一解释。
如果把今天的试错重新整理一次:
| 排查 | 后来证明的价值 |
|---|---|
| 发现订阅仍能更新 | 有效:证明并非完全断网 |
| 发现 B 站仍能打开 | 有效:说明存在正常工作的网络路径 |
| 检查 DNS | 合理,但最终不是根因 |
| 尝试公共 DNS | 合理实验,但没有触及核心问题 |
| 怀疑 MTU | 当时合理,后续证据使其优先级下降 |
| 怀疑运营商封锁节点 | 合理,但无法解释 Shadowrocket 开关与 IPv4 的强相关 |
| 区分 IPv4 / IPv6 | 关键突破 |
| Shadowrocket ON/OFF | 决定性控制变量 |
| 检查 Shadowrocket 高级配置 | 找到根因 |
| 关闭 Include All Networks | 完成最终验证 |
这里其实没有必要把之前的 DNS 等方向称为“无效操作”。
真实的技术排障本来就不是:
第一步
→ 正确答案更常见的是不断建立假设,再利用新的证据降低或提高某个假设的概率。
问题只在于:
当出现了比旧假设解释力更强的证据以后,要及时改变方向,而不是继续沉没在原来的思路里。
比如发现:
Shadowrocket ON → IPv4 ✗
Shadowrocket OFF → IPv4 ✓之后,就不应该继续花大量时间随机修改 DNS、MTU 或节点参数了。
因为此时已经有了一个远比它们更强的关联变量。
如果把整个过程抽象一下,大概可以写成:
观察症状
↓
判断是完全故障还是部分故障
↓
寻找仍然能够工作的路径
↓
比较正常路径与异常路径
↓
寻找最小差异
↓
进行 A/B 控制变量实验
↓
锁定具体组件
↓
检查历史修改和非默认配置
↓
恢复设置
↓
验证故障是否随之消失对我来说,其中有三个经验尤其值得记住。
看到:
Timeout并不代表一定要去研究代理软件。
它可能是:
应用层
↑
TLS
↑
TCP
↑
IP
↑
网络接口任何更低层的问题最终冒到应用层以后形成的结果。
这次甚至很可能连代理协议真正的握手阶段都没有走到。
如果一台电脑彻底没网,排查反而简单。
真正值得研究的是:
A ✓
B ✗因为这两个路径之间的区别,本身就在告诉我问题在哪里。
这次:
IPv6 ✓
IPv4 ✗就是整个排障过程中信息量最大的一条线索之一。
DNS、MTU、TUN、端口、代理协议……
这些东西都可以测。
但最有价值的问题其实是:
“有没有什么操作,可以稳定地让故障出现和消失?”
当我发现答案是:
打开 Shadowrocket → 坏
关闭 Shadowrocket → 好问题就已经从整个互联网缩小到了 iPhone 上的一个应用。
这比再执行十条命令都有价值。
这次还有一个甚至比网络知识更容易记住的教训:
不用的高级设置,用完以后恢复默认。
Include All Networks 本身并不是一个“错误设置”。
我当时打开它,是为了处理 Telegram 推送,有一个非常具体的目的。
真正的问题是:
那个需求过去以后,我忘了自己曾经改过它。
于是一个几个月前留下的设置,在几个月后一个完全不同的场景里——iPhone 给 Windows 开热点——突然产生了副作用。
最麻烦的是,到了那个时候,我已经完全不会第一时间想到它。
从现在往回看:
几个月前:
为了问题 A 修改配置 X
↓
忘记配置 X
↓
今天:
出现问题 B
↓
排查问题 B 时
根本想不到问题 A这就是典型的“历史遗留配置”。
复杂系统尤其容易出现这种问题。
每保留一个没有必要的非默认设置,实际上就是给系统增加了一个新的状态变量。
非默认配置越来越多以后,未来遇到问题时,需要考虑的组合也越来越多。
所以默认配置的价值并不只是:
“官方觉得这样比较好。”
还有一个很实际的工程价值:
它减少了隐藏变量。
以后如果只是为了临时解决问题而修改一个高级设置,我至少应该做到其中之一:
说得简单一点就是:
不用的高级设置记得恢复默认,不然后面真的可能坑人(笑)。
这次最后既不是机场突然坏了,也不是 Windows 网络栈彻底炸了,更不是那个一度看起来非常可疑的 DNS 真正造成了全部问题。
而是过去的自己为了另一个需求留下的一个 VPN 设置,在新的网络拓扑下产生了意料之外的副作用。
所以除了 IPv4、IPv6、VPN 和 Personal Hotspot,我今天大概还重新学了一遍一个更加朴素的工程原则:
临时改过的东西,用完就恢复;必须长期保留的非默认配置,至少给未来的自己留个记录。
除另有说明,本文内容采用 CC BY-NC-SA 4.0 协议许可。转载或改编请署名、非商业使用,并以相同方式共享。