stunnel 背后的 OpenVPN:转发包装器,而不是 1194 端口
两个监听器、三个端口。检查转发落在哪里、包装器绑定在哪里、它连接到哪里、本地测试漏掉了什么,以及两个身份。
在 OpenVPN 前放一个 TLS 包装器,部署就会多出第二个监听器,而其中只有一个应当面对互联网。当隧道在同一台机器上可用、从外部却失败时,转发通常指向了两者中错误的那一个。必须对齐的是一条链:公开规则到达包装器,包装器自己的后端连接到达真正的 VPN 套接字。四个跳点承载这条链,其中任何一环都可能让排查提前结束。
两个套接字,两个所有者,其中一个是公开的
包装器为外层连接终止 TLS,并把解密后的字节流交给普通的 OpenVPN TCP 监听器。它们是 各自拥有独立配置文件的独立套接字,因此修改一个不会动到另一个,后者看起来仍然健康。这也意味着,只要改动其中一个而不动另一个,两者就可能逐渐脱节。
因此,从本机本身操作,几乎证明不了公开路径的任何情况。直连原生端口会跳过包装器,连接包装器的 LAN 地址会跳过转换,只有完整的客户端侧布置才真正演练外部用户所做的事。名称也要仔细读,因为 localhost 指的是运行该组件的主机,而不是你恰好正在输入的那台主机。把这个区别记在笔记里,因为一项没有记录所覆盖跳点的测试,会引来对两件不同事情的比较。
转发实际落在哪里
把这条链记成四个跳点,比记成三个端口号更容易,因为同一个数字在两端的含义可能不同。逐行读表:每一行都是一个必须先接受连接、再打开下一个连接点的套接字。当某一行指向了错误的对端,最后一列的症状就是用户报告的现象。
| Hop | Listens on | Must reach | Symptom when it points elsewhere |
|---|---|---|---|
| Client machine | A local plaintext port | The wrapper's public TLS address | Plain text offered to a TLS endpoint |
| Router forward | Public TCP 443 | The wrapper host on its translated port | Wrapper records no connection at all |
| Wrapper, server mode | Its accepted port, 8443 here | The native VPN socket on loopback | Handshake completes, then silence |
| Native VPN listener | Loopback TCP port | Its peer, over TCP only | A 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