只允许通过代理出网的网络,正是原本可用的配置文件突然失效的地方。代理地址是那次故障中最不值得关注的变量。决定一切的是代理要转发的流量形态,因为这些代理转发的是字节流,而 UDP 隧道并不是字节流。于是只剩两种架构,而它们失效的位置并不相同。

一个约束决定设计

SOCKS5 确实规定了 UDP 关联模式。但 OpenVPN 客户端的代理支持仍然围绕流来构建:socks-proxy 仅记录适用于 TCP 配置文件,而 HTTP CONNECT 代理根本没有数据报通路。在碰配置文件之前,一行文档就已经把架构定下来了。

因此问题不是选哪个代理。而是隧道究竟变成一条字节流,还是保持数据报形态、让别的东西来承担代理。

方案 A:把隧道变成流

这需要一台带 TCP 监听器的服务器,以及指定代理的客户端配置。两部分缺一不可,而人们常忘的正是服务器一侧:

proto tcp-client
remote vpn.example.net 443
socks-proxy 127.0.0.1 1080

把最后一行换成 http-proxy proxy.internal 8080,当代理说 HTTP 时,并在需要时追加凭据文件。代理必须愿意对你 VPN 端口执行 CONNECT,服务器必须在 TCP 上应答;只监听 UDP 的服务器会忽略这一尝试。选 443 是务实之举,因为许多受限网络只放行它和很少别的东西。

代价是 TCP 叠在 TCP 上。外层一个分段丢失,就会卡住共享该连接的所有内层流,于是本应独立的恢复变成了共享恢复。建立也更慢,而抖动外层路径会变成抖动的隧道,而不是慢速隧道。

方案 B:不动隧道

当隧道必须留在 UDP 上时,要配置的地方并不在隧道里。而是把应用发出的 TCP 重定向到本地 SOCKS 监听器,应用始终不知道代理存在。在 Linux 上通常是 redsocks 加上 nat 表中的 NAT REDIRECT 规则。

依然只支持 TCP。像 privoxy 这样只支持 HTTP 的代理无法转发任意协议,而这一设计也没有为应用增加数据报支持。

在软件支持的地方,直接告诉每个应用代理的存在通常更好。它更容易审计,而且失效点是你真的能看见的。

两种方案各自失效之处

方案 A 会在服务器缺少 TCP 监听器、代理拒绝 CONNECT 目标,或外层流被限速到低于隧道所需时失效。前两者会在代理日志里留下记录;第三者留下的是一条能建立、随后感觉不对的连接。在改配置之前先读那份日志,因为它能把权限问题和容量问题分开。CONNECT 成功完成,说明调查该往隧道内部走。

方案 B 对所有需要数据报的东西、代理不理解的协议,以及在重定向规则存在但路由把流量送偏的主机上,都会静默失效。

两种方案都不改变设备要求。内核转发之前,数据包仍然需要 TUN 或 TAP 节点,所以进程仍然需要 /dev/net/tun 的权限。包装器改变的是节点打开之后发生什么,而不是你是否需要它。

证明你搭的是哪条路

看套接字,而不是托盘图标。VPN 进程应当保持一条到代理地址的已建立连接,代理则应当记录下被要求到达的目标:

ss -tnp | grep openvpn

然后访问只有隧道才能提供的目标,以及 只有隧道解析器才能回答的名字。代理连接成功而隧道已死,通常说明对端从未提供你配置里写明的传输方式。

把传输方式和接受连接的代理记在可靠的地方。当同一份配置在另一个网络失效时,这条记录能在几秒内排除或确认代理。

相关文章