隧道从家宽连上,到了酒店或办公室网络就卡住,而把服务器换到 443 端口并没有改变任何结果。这个结果常见到有固定形状:端口是容易的部分,剩下的判断属于客户端与服务器之间的一切。两个问题能解决大多数情况,而且都与密码套件无关。网络是否接受发往该地址的 TLS 握手,以及隧道起来之后客户端是否还保留通往自己封装层的路由?

握手透露的比端口号更多

TCP 443 上的原生 VPN 监听器从第一个包起就在说自己的协议。ClientHello 有版本字段、随机值和扩展列表,设备只要读几百字节就能分辨差异,完全不必解密。端口号很容易满足,过滤器真正检查的是协议外形。规则通常也按协议分别书写,所以检查 TLS 的策略对 UDP 一无所知。

坚持要有效 TLS 会话的透明代理会在建立阶段丢弃连接,往往是在 TCP 握手完成之后。这个细节后面有用,因为它意味着客户端日志在套接字建立之后就停住了。

封装流会带来两个故障点

像 stunnel 这样的 TLS 封装恢复了预期外形:客户端连到本地监听器,封装层向服务器在 443 上打开真正的 TLS 会话,服务器上的第二层封装把解密后的字节转给 VPN 守护进程。外层会话现在看起来和任何 HTTPS 连接一样。服务器上的 VPN 守护进程应该只监听 loopback,因为封装之外没有任何东西需要够到它。

这个设计有三件事必须对齐。客户端配置指向本地监听器,服务器证书与客户端校验的名字一致,服务器侧的转发指向守护进程真正使用的端口。任何一项出错,连接在到达 VPN 层之前就会死掉。证书不匹配于是表现为封装错误而不是 VPN 错误,这正是两份日志值得并排读的原因。

通往封装的路由才是陷阱

隧道一起来,默认路由就指进隧道,而外层 TLS 会话需要一条不进入它所承载隧道的路径。普通 VPN 客户端保护自己拨号的地址;被封装客户端拨的是自己的 loopback,所以封装的目的地址对那层保护不可见。

sudo ip route add 198.51.100.20 via 192.168.0.1
sudo openvpn --config client.ovpn
sudo ip route del 198.51.100.20

精确地址由你填写,而路由必须在连接整个生命周期内存在,不只是启动时。一个在客户端之前加路由、之后删掉的小封装脚本,比记住一条命令更可靠。

在 Android 上,这条例外的对应物是逐应用设置:把封装应用排除在隧道之外,让它自己的流量走移动连接出去。不排除的话,封装会被路由进它正要建立的隧道里,重连循环看起来就像服务器出了问题。

改动之前先看时间特征

每次都在同一时间间隔后死掉的会话是策略指纹而不是密码故障,长时间流限制正好产生这种模式。握手之后立刻被重置的连接,提示代理不喜欢这条证书链。

从两个网络分别比较普通 HTTPS 站点的证书签发者。如果受限网络上签发者不同,有东西正在中间终止 TLS,任何封装都无法原样穿过这种拦截。同样的比较还能告诉你,封锁针对的是你的服务器名,还是该地址上的每一个 TLS 会话。

把观察到的行为写下来:会话持续了多久、套接字有没有打开、封装有没有记录握手。每一项都能缩小下一次测试的范围。

一次只测一个变量

先测 UDP,因为有些网络会把它整个丢掉,而 UDP 失败并不能说明 443 的情况。然后试不带封装的 TCP 443,再试 443 上的封装。每一步都读客户端日志,行为一变就停下来。

每次只改一个要素。新端口、新封装和新密码放在同一轮里,你就说不清三者中哪个起了作用。

相关文章