两个服务可以同时在 443 端口上应答而从不冲突,因为监听器由传输层、地址和端口三者共同决定,而不只是端口号。把 Web 服务器和 OpenVPN 放在同一台机器上,最该先回答的问题就是每个进程向内核申请哪个套接字。动手改配置之前先把这份清单写下来,大多数「地址已被占用」的报错就会自己解释清楚。清单之后的一切,都是在 UDP 监听器、共享 TCP 套接字和按协议分流的前端之间做选择。

先看每个进程申请哪个套接字

bind 的成败取决于这个三元组,所以两个守护进程只有三者全部重合时才会冲突。这正是 Web 站点能在 TCP 443 上与 UDP 443 上的 OpenVPN 监听器无限期共存的原因,也是把其中之一挪到同一台主机的第二个本地地址就能结束争论的原因。要看真正在运行的监听器,而不是那些本应创建它的配置文件,因为过期的 include 或被遗忘的 unit 文件,往往比你以为做过的那次修改活得更久。

Line in the listener listWhat it establishesWhat to do next
One udp row and one tcp row, each on 443Separate transport sockets, nothing contestedPoint the VPN profile at udp and stop editing
A single tcp address and port listed by two processesA real bind collision between two daemonsMove one service, or put a dispatcher in front of both
A proxy process owns the socket, no vpn processThe proxy accepts and has to hand off correctlyCheck that its backend speaks the protocol it forwards

以 root 运行 ss -lntup,把每一行提到 443 的输出都读一遍,包括 IPv6 条目。通配的 v6 监听器通常也接受 v4 流量,被遗忘的监听器就是这样在你改完自认为负责的那个文件之后仍然占着端口。如果某个进程名让你意外,先用 ss -lntup 'sport = :443' 缩小输出,再动手。

网络允许时把隧道切到 UDP

UDP 监听器完全绕开了冲突,通常也是最省事的修法,因为既不共享套接字,也没有多余的产品插进链路。但它并非没有代价。有些客户端网络会直接丢弃 UDP 443 而不回 reset,隧道于是停在「建立不起来」而不是「绑定失败」,用户只会报告 VPN 挂了,却没有任何错误可读。UDP 监听器还能挺过 Web 层的重启,这对站点经常重新部署的机器很重要。要从用户所在的网络去测,因为服务器上的 shell 只能证明服务器能到自己。

在服务端配置和客户端配置里同时改传输方式。仍然要求 TCP 的配置会对着只有 UDP 的监听器反复重连,看起来像网络故障,直到你把两个文件放在一起对比。

让 VPN 进程拥有 TCP 443,其余转发出去

Community OpenVPN 的 TCP 服务器模式会读取入站连接的前几个字节,把不属于自身协议的流量转发给 HTTPS 后端,port-share 指令就是干这个的。后端是启用了 TLS 的普通 Web 服务器,在这条指令里用主机和端口指定,而不是用 URL。

port-share 10.0.0.5 8080

在站点持有第二个私有地址的机器上,这一行会把非 VPN 流量送到 10.0.0.5:8080,因此 HTTPS 虚拟主机必须绑定在那里,防火墙也要放行这次本地跳转。Web 服务器必须自己终结 TLS,因为到达的字节就是浏览器的原始握手,中间没有任何环节解密过它。把指令指向一个普通 HTTP 监听器,浏览器就会收到「方法无效」的报错,读起来像站点坏了,而不像后端放错了位置。这条指令只在 TCP 服务器模式下存在,UDP 监听器没有可检查的字节流。

由于 VPN 进程接受每一条连接,Web 应用看到的转发请求来自主机自身的某个地址,而不是来自访客。任何需要真实客户端地址的东西,都依赖后端的 PROXY 协议支持,或者依赖在前面加这一层的前端。把后端挪到另一个本地地址并不会把访客地址找回来,只是改变了站点记录的是主机哪个地址。趁分析报表还没提出这个问题,先把这条限制写进部署说明。

反向代理是错的工具,四层路由器不是

HTTP 反向代理会终结 TLS 才能读请求,这意味着它无法原样放行 OpenVPN 的字节流。按 SNI 或首包特征转发的四层路由器可以挡在两者前面,前提是它不终结 VPN 所依赖的会话。两个设备在图上长得一样,在套接字层面的行为完全不同。

商业版 Access Server 自带端口共享设置,那些设置并不能与社区版指令一一对应。在套用为另一个版本写的指南之前,先确认产品和版本。被静默忽略的照搬配置比不配置更糟,因为事后看监听器列表依然像模像样。

把网站和隧道当成两条独立路径来测

打开网站并连接隧道,然后再读一遍监听器列表,看现在是谁占着 443。页面能打开不能证明 VPN 正常,隧道能连上也不能证明网站正常,哪怕两者用的是同一个端口号。下面三条命令依次检查共享套接字、Web 路径和 VPN 服务。

ss -lntup 'sport = :443'
curl -sI https://vpn.example.com/ | head -n 1
journalctl -u openvpn-server@server -n 20

把后端访问日志里的客户端地址与同一时刻 VPN 日志里的地址对照。如果站点把每个访客都记成主机自己,说明转发是通的,而地址处理没通。如果两边日志都没有记录,说明前端根本没有把连接交出去。无论如何,监听器列表决定了谁在接受套接字,这就是改下一个文件之前要先确认的事实。

把这张监听器地图放在配置旁边,让下一个碰 443 的人知道是哪个进程在接受、另一个协议去了哪里。改完之后要确认归属,而不是相信这次修改本来想做什么。

相关文章