访问 VPN 客户端上的服务:反向隧道还是 DNAT
在没有端口转发的情况下访问 OpenVPN 客户端上的服务:比较反向 SSH 隧道与服务器端 DNAT,以及决定取舍的三个条件。
即使笔记本位于运营商 NAT 之后,只要 VPN 服务器充当会合点,它也能为同事提供服务。有两种机制可以到达那里:客户端向外拨号并请求服务器持有一个监听端口,或者服务器发布一个端口并将其转换进隧道。两者解决同一个可达性问题,但在故障、监督和日志方面的表现不同。三个条件决定哪一种适合你的配置。
两种形态,一个会合点
第一种形态下,客户端向外建立 SSH 会话,请求服务器在选定端口上监听,并把每条连接沿该会话送回。服务器上没有任何转发,也不发生数据包转换,因此唯一的要求是一个 SSH 账号、一条被允许的转发以及该端口的防火墙规则。服务仍然绑定在客户端的回环地址上,这通常正是你想要的。
ssh -N -R 8081:127.0.0.1:8080 -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 [email protected]
第二种形态下,服务器拥有公网端口,并把每条连接改写指向客户端的隧道地址。这需要开启 IP 转发,入口处做目的地址转换,出口处做源地址转换,而且即使没有 SSH 会话也能继续工作。端口属于服务器而不是某个会话,正是这一属性决定了大多数真实场景。
两种形态在构造上都不更安全。它们最终都把一个服务端口暴露给任何能到达服务器公网地址的人,而且都仍然依赖服务自身的认证。
条件一:端口是否必须比会话活得更久
反向转发存在的时长恰好等于创建它的 SSH 会话。链路一旦断开,监听就消失,同事看到的是连接被拒绝,而不是一条慢路径。用 systemd 单元和重启策略监督该命令可以解决这个问题,而 ExitOnForwardFailure=yes 让绑定失败立即可见,而不是留下一个没有转发的会话继续运行。
如果规则被持久化,转换后的端口可以熬过重启,但这取决于客户端是否保持同一个隧道地址。一个在重连后给笔记本分配另一个地址的动态池,会悄悄地把规则指向空处。先保留隧道地址,再写规则,并把两者当作同一次变更。
如果服务必须在凌晨三点无人值守时也能应答,那么由服务器发布端口的那种形态可动部件更少,因为唯一必须保持运行的进程就是隧道本身。
条件二:防火墙和日志归谁
反向形态把全部策略放在两个地方。sshd 通过 AllowTcpForwarding 决定是否允许远程转发,GatewayPorts 决定监听绑定到回环还是绑定到所有地址。使用默认值 no 时,服务器外部的同事即使转发成功也无法到达该端口,这看起来像隧道坏了,实际上却是绑定地址的问题。
sudo ss -ltn '( sport = :8081 )'
sudo grep -E '^(GatewayPorts|AllowTcpForwarding)' /etc/ssh/sshd_config
转换形态需要开启转发并写入两条规则,而第二条规则正是大多数教程出错的地方。把源地址转换限制在 conntrack 的 DNAT 状态,意味着只有目的规则创建的那些连接会被改写,而不是服务器送进隧道的每一个数据包。
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8081 -j DNAT --to-destination 10.88.0.6:8080
sudo iptables -t nat -A POSTROUTING -o tun0 -m conntrack --ctstate DNAT -j SNAT --to-source 10.88.0.1
这次改写也摧毁了归属性。此后客户端看到的每个调用方都是服务器的隧道地址,于是任何按地址的限速都会塌缩进同一个桶,也无法看出谁发了什么。如果归属性重要,就在服务器上终止连接,或者把放行决定留在服务器,并在服务所信任的标头中传递身份。在这两种修法之间做选择,取决于服务是否读取代理标头。
条件三:一个端口还是多个,名字归谁
单端口上的单个服务是反向转发在投入上胜出的场景。你只需要一条命令、一个账号和一条防火墙规则,清理工作就是结束会话。同一个公网地址上的多个服务会把你推向服务器发布的形态,因为每次反向转发都需要自己的服务器端口,集合会随服务数量增长。
名字会让情况更复杂。反向转发可以放在按 SNI 路由的 TCP 代理后面,而转换后的端口通常是每个服务一个端口。如果服务自己终止 TLS 并需要原始主机名,代理方案能保住它,DNAT 方案只有在服务于该端口上接受该名字时才能保住它。
写下三个条件中究竟是哪一个决定了形态。否则下一个添加第二个服务的人会以更少的上下文重新打开同一场比较。
先决定,再从外部测试
从一个没有通往笔记本路由的网络测试路径,并在请求执行期间观察服务器上的计数器。从服务器朝它自己的公网地址测试在这里什么也证明不了,因为数据包是在本地生成的,永远不会到达入站转换钩子,所以测试可能通过而真实路径已经损坏。
sudo iptables -t nat -L PREROUTING -n -v
sudo tcpdump -ni any 'tcp port 8081' -c 20
计数器静止不动,说明数据包从未到达规则,这指向运营商或主机防火墙。计数器在服务拒绝连接时仍在增长,则指向客户端自己的防火墙或服务监听的地址。一条承载了数据包却什么也不返回的隧道,是路由或反向路径问题,而且这三种结果都不应该通过编辑客户端适配器来排查。在改动任何东西之前,先记下哪个计数器动了,因为下一次测试需要这条基准线。
形态一旦可用,就把端口、负责人和监督方法记录在隧道配置旁边。只存在于某人 shell 历史里的可达性修复,距离一次重启就会重演的事故只有一步。