在 OpenVPN 前放一个 TLS 包装器,部署就会多出第二个监听器,而其中只有一个应当面对互联网。当隧道在同一台机器上可用、从外部却失败时,转发通常指向了两者中错误的那一个。必须对齐的是一条链:公开规则到达包装器,包装器自己的后端连接到达真正的 VPN 套接字。四个跳点承载这条链,其中任何一环都可能让排查提前结束。

两个套接字,两个所有者,其中一个是公开的

包装器为外层连接终止 TLS,并把解密后的字节流交给普通的 OpenVPN TCP 监听器。它们是 各自拥有独立配置文件的独立套接字,因此修改一个不会动到另一个,后者看起来仍然健康。这也意味着,只要改动其中一个而不动另一个,两者就可能逐渐脱节。

因此,从本机本身操作,几乎证明不了公开路径的任何情况。直连原生端口会跳过包装器,连接包装器的 LAN 地址会跳过转换,只有完整的客户端侧布置才真正演练外部用户所做的事。名称也要仔细读,因为 localhost 指的是运行该组件的主机,而不是你恰好正在输入的那台主机。把这个区别记在笔记里,因为一项没有记录所覆盖跳点的测试,会引来对两件不同事情的比较。

转发实际落在哪里

把这条链记成四个跳点,比记成三个端口号更容易,因为同一个数字在两端的含义可能不同。逐行读表:每一行都是一个必须先接受连接、再打开下一个连接点的套接字。当某一行指向了错误的对端,最后一列的症状就是用户报告的现象。

HopListens onMust reachSymptom when it points elsewhere
Client machineA local plaintext portThe wrapper's public TLS addressPlain text offered to a TLS endpoint
Router forwardPublic TCP 443The wrapper host on its translated portWrapper records no connection at all
Wrapper, server modeIts accepted port, 8443 hereThe native VPN socket on loopbackHandshake completes, then silence
Native VPN listenerLoopback TCP portIts peer, over TCP onlyA UDP socket behind a byte-stream relay

把目标指向原生 VPN 端口而不是包装器的规则会完全跳过 TLS 终止,这也是这个错误最常见的形态:客户端向一个期待 OpenVPN 分帧的监听器发起握手,于是连接可能建立后停住。请对照包装器的配置文件核对翻译后的端口,而不是凭记忆,因为下一个编辑该文件的人可能选了另一个数字。下面两条命令同时展示了这个问题的两面。

grep -nE '^[[:space:]]*(accept|connect)' /etc/stunnel/stunnel.conf
ss -lntp | grep -E ':443|:8443|:1194'

拒绝你的绑定地址

绑定到 127.0.0.1 的 accept 行只应答主机自身,因此到达 LAN 地址的转发无法连接,客户端看到的拒绝听起来像数据包过滤问题。先看套接字,再动防火墙,因为套接字是更省事的答案。要么把包装器绑定到转发所指向的接口,要么让它留在 loopback 上,并让转发通过真正可行的方式到达 loopback。这是两种设计,各自都需要自己的规则。

把两者混在一起,就是正在工作的包装器从外部变得不可达、而每个文件看起来都正确的原因。下面的检查只有一条命令,它回答的是审查防火墙规则回答不了的问题。如果输出显示 loopback 地址,而转发指向 LAN 地址,那么修正属于 accept 行,而不是数据包过滤器。

ss -lntp '( sport = :8443 )'

本地测试到底证明了什么

没有记录所覆盖跳点的成功,并不能作为关于其他跳点的证据。记下测试用了哪个地址、由哪个进程应答。然后从网络外部重试,而不是在同一 Wi-Fi 的手机上,因为发夹行为因路由器而异,从未离开 LAN 的连接也能让客户端满意,而转发根本没有参与。只有第二次尝试才走外部用户真正走的路。

当尝试失败时,沿两个日志跟踪同一条连接:包装器的 accept 与 connect 行,然后是 VPN 守护进程自己的会话日志。第一个什么都没记录的边界,就是下一次改动该去的地方。一旦 OpenVPN 开始比较对端选项,就要按该实现的规则判断这些消息。link-mtu 不一致警告并不自动致命,把 link-mtu 固定为 1500 并不能证明分片已经停止,而密码协商属于内层 OpenVPN 配置,不属于路由器的转发。

nc -vz 127.0.0.1 1194
journalctl -u stunnel -n 50 --no-pager

这些探针覆盖后端套接字和包装器日志,从而把被拒绝的后端与被拒绝的握手区分开。要在单次尝试进行时运行它们,而不是之后。同一次尝试的时间戳,才让比较可信。

两个身份,两种失败顺序

外层证书属于包装器,内层身份属于 OpenVPN,二者可能来自不同的机构。为了让路径看起来健康而关闭任一检查,是用表面结果换掉真实保护。两者都要保留,并按顺序调试,因为消息顺序会点明层次:包装器证书问题出现在任何 VPN 选项协商之前,而内层身份失败只在流建立之后才出现。

stunnel 手册 精确定义了 accept、connect 和客户端模式,这些章节回答套接字问题比猜测更快。当仍说不清是哪一层应答时,用下面的命令读取公开端口出示的证书。输出中出现包装器证书,说明转发到达了包装器;沉默或协议错误则说明没有到达。就这一处区分,比这里其他任何检查都更省时间。

openssl s_client -connect vpn.example:443 -servername vpn.example </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer

相关文章