SOCKS5 上的 OpenVPN:匹配版本、传输与 DNS
当代理版本、传输方式或 DNS 路径不一致时,SOCKS 链路就会失败。匹配 SOCKS5、承载 TCP,并把链路限制在一个命名空间内。
从这台工作站出去的一条路由是 SOCKS 代理,而它后面的 OpenVPN 报连接超时,却不附带任何有用的信息。同一台机器上的浏览器能通过该代理工作,凭据正确,端口也有响应。有三件事经常不同,值得先查:代理所说的 SOCKS 版本、客户端选择的传输方式,以及 名称解析发生的位置。在断定防火墙有问题之前,先逐项确认。
先匹配 SOCKS 版本,再谈防火墙
社区版 OpenVPN 为 socks-proxy 使用 SOCKS5。把它指向只提供 SOCKS4 的代理,TCP 连接会打开,然后停滞,因为客户端在等待一个对端从不解析的问候的回复。从外部看,这种停滞与端口被过滤完全一样,这就是版本问题要排在任何防火墙规则之前的原因。一次支持 SOCKS 的抓取就能用一条命令定论。
curl --socks5-hostname 127.0.0.1:9050 gw.lab.example
一次完成的请求同时证明两件事:代理接受了 SOCKS 请求,并且它解析了名称。不需要 URL 方案,因为 curl 把裸主机当作 HTTP,这对本测试已经足够。
那次抓取也排除了客户端日志所掩盖的故障。代理可以放行浏览器却拒绝 VPN 目的地,此时拒绝发生在 CONNECT 阶段,早于任何 VPN 认证。OpenVPN 只报告超时,所以代理自己的回答才是你需要的证据。
两端的传输都必须是 TCP
这里的 SOCKS5 承载 TCP 流,因此隧道本身需要 proto tcp-client。仍设为 UDP 的配置无法走这条路径,因为客户端不使用该协议的 UDP 关联模式。翻转配置是真正的改动,而不是形式:远端必须在同一端口上监听 TCP。随后载荷变成 TCP 套 TCP,丢失的报文段会被重传两次、节奏变差,交互式会话对此的感受远超大文件传输。
proto tcp-client
remote gw.lab.example 8443
socks-proxy 127.0.0.1 9050
该指令接受代理主机、可选端口和可选的凭据文件。把它们放进配置文件本身,这样在别处复制的副本行为一致。
把整条链路关进一个容器
在主机上做这条链路意味着接管它的默认路由,并把解析器留在以后没人解释得清的状态。命名空间或容器可以把它圈起来,于是主机保留自己的路径,而 SOCKS 跳跃与隧道共处一室。里面什么也起不来,除非有隧道设备、配置该设备的权限,以及可达的代理。Compose 服务靠 cap_add: NET_ADMIN 和 bind 挂载的 /dev/net/tun 拿到这些。
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
这一对权限比以特权方式运行容器更窄,而且已经是设备访问所需的全部。Tor 客户端或单独的代理容器都可以提供监听者。无论哪种情况,都把它绑定到 loopback,并从命名空间经由主机访问它,而不是把代理暴露到网络上。
名称解析才是隔离失效的地方
一个在承载数据包的同时又去问主机解析器的隧道,什么也没隔离。两件事让隔离成真:命名空间有自己的解析器,并且它的查询经由代理或隧道,绝不走物理接口。测试用两个名字:一个只存在于隧道之后,另一个是应当走同一条路径的公共名字。
docker compose exec vpn cat /etc/resolv.conf
docker compose exec vpn getent hosts internal.lab.example
如果远端名字能解析而公共名字不能,链路就是有选择性的,而不是坏的,这个差别会告诉你每个查询走了哪条路径。如果两者在主机上都能解析,但在容器里都解析不了,那容器仍在借用主机的解析器。把找到的解析器文件记下来,因为那正是需要更改的设置。
按顺序逐一证明每一跳
清单很短。SOCKS5 请求得到回答,隧道经由代理到达它的监听者,离开命名空间的数据包带着远端期望的地址。一次只确认一项,而不是继续叠加配置。当客户端说已连接却什么都不动时,断点几乎总在第二或第三个事实上。
curl --socks5-hostname 127.0.0.1:9050 gw.lab.example
docker compose exec vpn ip route get 203.0.113.10
docker compose exec vpn getent hosts internal.lab.example
把奏效的组合写下来:SOCKS 版本、传输、解析器与命名空间设置。凭记忆再解一次这条链的代价高于这条笔记。把那四个事实留在工单里,而不是聊天记录里。
一条报告已连接却什么都不通过的链路,通常失败在你还没证明的那一跳。在再加一层之前,先测那一跳。