为什么办公室 LAN 访问不到 VPN 客户端上的服务
VPN 客户端可以承载 LAN 打不开的服务。缺的通常是回程路径:一条路由、一对 DNAT,或者没人写的 SNAT 规则。
隧道是通的,服务在监听,办公室的 LAN 却还是什么都收不到。没有人弄坏 VPN。从 LAN 一侧看,笔记本的隧道地址并不存在,除非有东西告诉 LAN 怎么到达它,而且回应必须沿请求出去的路径返回。我见过有人在任何人看回程路径之前,把一个完全正常的隧道重建了三次。
坏的不是隧道
两端都在私有地址段,这是正常情况,不是配置错误。像 10.8.0.6 这样的地址在隧道内真实存在,在其他任何地方都不存在,所以 LAN 没有理由往那里发东西。在动路由之前,先确认服务在隧道内到底能不能访问:在客户端上,ss -ltn 会显示监听者是绑定在 127.0.0.1、LAN 接口还是隧道地址上。只绑定 loopback 的守护进程除了自己谁也不回应,任何路由、DNAT 或端口转发都改变不了这一点。
80 或 443 端口上的代理是另一类工具,可见性也不同。它搬运的是请求而不是报文,所以对原始数据库连接来说层级不对;正是伸手去抓它,才让这个问题在有人测量之前就深了三层工具。
路由形态:告诉 LAN 地址池在哪里
当 LAN 与隧道地址池不重叠时,LAN 网关上的静态路由是最干净的答案。把地址池指向终结隧道的机器:
ip route add 10.8.0.0/24 via 192.168.1.12
route add -p 10.8.0.0 mask 255.255.255.0 192.168.1.12 # Windows, persistent
这只修好了一个方向,所以这个改动的半成品看起来像是进展。隧道一侧需要对应的另一半,否则回应会按客户端自己的默认路由出去,死在路上:
push "route 192.168.1.0 255.255.255.0"
对于位于某个客户端之后、而不是服务器之后的子网,服务器既需要系统路由,也需要在该客户端文件中的 iroute 记录(client-config-dir 之下)。没有 iroute,OpenVPN 知道这个前缀存在,却不知道哪个客户端拥有它:报文到了,守护进程却没有东西可匹配。
这里有一个值得点名的迷思。路由让地址可达,并不让对端愿意回应。站点到站点的对端又叠了一层,因为远端网关只接受其策略中所列前缀内部的来源,而来自家庭地址池的笔记本不在其中。
NAT 形态:向内的 DNAT,以及所有人都会漏掉的那条 SNAT
当你碰不到 LAN 网关,或者只想发布恰好一个端口时,VPN 主机上的转换可以在 LAN 上完全不设路由。发布端口、转发它,并在进入隧道的路上转换源地址:
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 10.8.0.6:3000
iptables -A FORWARD -i eth0 -o tun0 -p tcp -d 10.8.0.6 --dport 3000 -j ACCEPT
iptables -t nat -A POSTROUTING -o tun0 -d 10.8.0.6 -p tcp --dport 3000 -j SNAT --to-source 10.8.0.1
第三行就是会消失的那一行。漏掉它,客户端会看到原始 LAN 地址作为源,按自己的默认路由回应,而那个回应会进入隧道,而不是回到 LAN。连接能不能活下来,取决于你从未规划过的路由。加上 SNAT 之后,客户端看到的是服务器的隧道地址,于是回应沿隧道回到唯一能撤销转换的主机。
消费级固件用自己的链名,不会替你生成那项转换。在 Tomato 上,入站规则会落进 WANPREROUTING,接着进入 wanin 这样的过滤链,而看起来完整的规则集仍然需要手写 POSTROUTING 行。那不是固件缺陷;那是只为入站流量写的规则集。
看起来正确的规则为什么还是丢包
这些故障大多不在你盯着的那条规则里。按这个顺序检查四件事,并在第一个不成立的地方停下:
- 链的顺序。
iptables -I插到最上面,-A追加到最后,所以更早的放行或拦截规则先做决定。 - 链错了。 发往路由器本身的流量走 INPUT,被转发的流量则从不经过它。发布一个服务可能两条链都要规则。
- 方向标记。
-i tun0匹配来自客户端的流量,-o tun0匹配发往客户端的流量。标记用错的 REJECT 会挡住你并不想挡的东西。 - 来源限制。 只接受一个客户端地址的转发,只能从那个客户端使用,其他来源会被无声地丢掉。
包计数器为零的规则根本不在路径上,读计数器是区分链错了和规则错了最快的办法。
本地转发,当 LAN 上什么都改不了的时候
SSH 本地转发绕开了整个问题。它不需要 LAN 上的路由,不需要客户端防火墙配合,也不需要在哪里发布端口:
ssh -N -f -L 3000:10.8.0.6:3000 user@vpn-host
服务随后出现在运行隧道那台机器的 localhost:3000 上,别处都没有。这就是边界:一台工作站、一个端口,而且除非有人重启,转发会随会话一起消失。对检查预发数据库的管理员来说,它是正确的工具。对其他人都要访问的摄像头或扫描仪来说不是,因为每个用户都需要自己的转发。
再加一条规则之前,先把两个方向都验一遍
转换会留下痕迹,读痕迹胜过猜。在 VPN 主机上,conntrack -L 显示转换到底有没有发生,iptables -t nat -L -n -v 显示计数器有没有动,tcpdump -ni any port 3000 显示究竟是哪个源地址到了。在客户端上,ss -ltn 确认监听者在隧道能到达的地址上。
然后选定你要保留的形态。发布一个端口、为整个地址池加一条路由、为单个管理员做转发,是三种不同的承诺;把它们混在一起,就是让一套能工作的配置在半年后变成没人能解释的东西。写清楚哪个地址从哪里可达,下一个来排查的人就会从答案开始,而不是从隧道开始。