两个 DNS 名称、一个 OpenVPN 端口:真正起选择作用的是什么
指向同一地址的两个主机名,终究还是到达同一个监听者。只有客户端发送的字段才能选择后端,而原生 OpenVPN 不发送任何字段。
两条 A 记录、一个地址、一个守护进程:无论用户输入哪个名字,到达的报文都一模一样,绑定在端口 1194 上的进程也无从区分二者。这不是有待修复的配置错误,而是名称解析所能表达的边界。任何要在两个 OpenVPN 实例之间做选择的东西,都必须是客户端放进连接、并且前端能够读回的字段;而标准 OpenVPN 客户端不会把这样的字段明文放进去。下文说明这会把你留在什么位置,以及哪些方案才真的把流量分开。
从解析器返回的内容说起
客户端查询 vpn-a.example,得到一个地址;这个地址加一个端口,就是它拨号的全部内容。名字在查询过程中于本地被消耗掉,从不成为线路上的字段,因此另一端的监听者看不到用过哪个标签。把 vpn-a.example 和 vpn-b.example 都指向同一台主机,两次查询返回同一个目的地,于是守护进程把两条连接并入同一个实例,而不记录任何差别。基于地址的配置与基于名称的配置表现完全相同,道理就在这里。
这也堵死了一类诱人的变通做法:额外的别名、在同一个套接字上按名称挑选证书,或者按所查名字生效的防火墙规则。它们都无法作用于在第一个报文离开客户端之前就已经被丢弃的信息。如果两台服务器必须保持分离,分离就必须随连接本身一起走,也就是地址、端口,或协议定义的载荷字段。
唯一算数的选择器走在报文内部
HTTP 靠 Host 头解决这件事,TLS 靠 ClientHello 里的服务器名称扩展解决,于是一个地址就能在一个监听者背后服务许多站点。两者都成立,是因为线路上的协议带着针对某个具体服务的请求。OpenVPN 的相关事实是:中间设备所检查的那一层上,这两个字段都不存在。
在标准部署里,保护控制通道的 TLS 握手发生在外层会话建立之后,并被承载在该会话内部,因此没有终止该会话的监听者只能看到一段不透明的流。它仍然能判断这是 OpenVPN,因为开头字节里的操作码和分帧可以识别,但服务器名称如果存在,是坐在加密部分里的。指望从外层报文里读到虚拟主机,读到的是协议从不写在那里的字段。这源于隧道是如何搭建的,而不是某个更聪明的模式匹配能补上的缺口。
协议分流器止步的地方
sslh 项目所记录的那类多路复用器在一个端口上监听、检查开头字节,并把连接交给说它识别出的协议的那个后端。当一个端口必须同时服务 SSH、HTTPS 和 OpenVPN 时,这是真实的价值。
sslh 为每种协议保留独立的探测,它的 TLS 探测能够从真正的 ClientHello 里取出服务器名称或 ALPN 值。识别原生 OpenVPN 与读取 TLS 扩展是两种不同的分类,二者并不叠加,因为从不向监听套接字发送 ClientHello 的客户端,让探测无内容可读。所以匹配 OpenVPN 的规则只会回答“这是一个 OpenVPN 客户端”,不会再往前走。该规则背后的两个 OpenVPN 后端无法区分,前端没有做选择的依据,每个会话都落在同一个地方。多路复用是把一个端口在不同服务之间共享,它并不在一个服务内部做虚拟主机。
真正把两台服务器分开的方案
最便宜的诚实做法是第二个公网地址,每个实例一个;现有配置已经带着所需的东西,因为它们指向不同的地址。其次是同一地址上的不同端口,代价不过是每个配置里的一个数字。TCP 与 UDP 各维护独立的端口表,所以 1194/tcp 与 1194/udp 是互不相关的套接字,各取一个可以共存于同一台主机;这属于传输层分离,而不是按名称路由,常被混为一谈。两种方案都经得起防火墙审查,因为规则匹配的是报文里真实存在的东西。
如果你需要的边界是在用户群之间,而不是在两种实现之间,那么一个实例配上不同的客户端配置、自己的证书处理以及明确的访问策略,就可以覆盖它。这买到的是服务器内部的策略,而不是服务器之间的隔离,二者是不同的要求。必须在被攻破时仍然成立的隔离,仍然需要地址或防火墙边界。
把 TLS 终止器放在 OpenVPN 前面,可以向有能力的后端暴露真实的服务器名称,但前提是客户端一侧也针对这一额外层做了配置。现有配置不会仅仅因为指向同一地址就获得这种行为。
先证明选择器存在,再动手搭
先给将要标识后端的字段命名,再确认客户端确实会发送它;如果答案是地址,方案就成立;如果它是握手内部、永远到不了监听者的服务器名称,那么再多 DNS 工作也救不回来。三条命令很快就能把监听这一侧弄清楚。
ss -lntup | grep -E ':443|:1194'
openssl s_client -connect vpn-a.example:443 -servername vpn-a.example </dev/null | head -n 20
journalctl -u openvpn-server@server-a -n 20 --no-pager
openssl 那一行只在该地址上有什么东西终止 TLS 的地方才给出证书;面对原生 OpenVPN 监听者,它会卡住或失败,而这本身就是关于外层暴露了什么的答案。journal 那一行在每个后端上读取,显示是哪个实例接受了会话。前端的日志记录的是被分类的结果,而从不记录原本的意图,因此用户到底落在哪里,只有后端说了算。
最后用两个本应到达不同服务器的配置收尾,逐个连接,并记下各自的日志。如果两个会话都出现在同一份 journal 里,说明选择器根本不存在,而不是配置错了,解法是客户端能够瞄准的地址或端口。改路由规则没有用,因为规则从来没有拿到过可供路由的东西。一旦分离是真的,就把它写在配置旁边,免得下一个人再加一个别名,还指望它把活干完。