在 VM 中让主机流量经 VPN 路由:六项检查
把 VM 变成其自身主机的 VPN 网关的六项按序检查:网卡模式、转发、NAT、主机路由、DNS、MTU。
按顺序执行的六项检查决定一台 VM 能否充当其自身主机的 VPN 网关。虚拟机监控程序不会替你完成其中任何一步:隧道归客体,路由归主机,而两者之间的网络只是普通路由。我之所以保留这份清单,是因为跳过任何一步都会得到同一个症状:沉默——主机能顺利 ping 通客体,却到不了任何内部资源。
先问清原因。要么企业客户端只能装在客体里,要么策略要求它必须留在那里,而你又确实需要从主机访问内部构建服务器。如果主机被允许使用自己的客户端,就应当那样做:主机上的一条隧道永远胜过两张网络加一层 NAT。
检查 1:主机需要一条能配置地址的链路
主机必须拥有一块与客体第二块网卡处于同一前缀的接口。VirtualBox 的 host-only、VMware Workstation 的 host-only 或自定义 VMnet、QEMU/KVM 的内部网桥或 tap 设备、Hyper-V 的 Internal 交换机都算合格。客体 192.168.56.2、主机 192.168.56.1、前缀 192.168.56.0/24 是我在实验环境里使用的布局。
QEMU 的用户模式网络(-netdev user,默认的 SLIRP 后端)和 VirtualBox 的 NAT 在这一步彻底不合格。客体的数据包只会到达主机上的代理进程,主机始终看不到承载客体地址的设备,也就没有任何东西可以指向路由。Hyper-V 的 Default Switch 也是同一回事。如果从主机执行 ping 192.168.56.2 失败,就停下:事后添加任何路由都修不好它。
检查 2:客体负责转发和转换
来自主机的数据包带着 host-only 侧源地址和内部目的地址到达,因此客体必须先转发它们,再把它们藏在自身隧道地址之后。在 Linux 客体上,这意味着开启转发、一对 FORWARD accept 规则,以及朝隧道方向的源 NAT:
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -A FORWARD -i eth1 -o tun0 -j ACCEPT
sudo iptables -A FORWARD -i tun0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 192.168.56.0/24 -o tun0 -j MASQUERADE
MASQUERADE 规则正是人们最常漏掉的部分。企业路由器很少知道 192.168.56.0/24 的存在;有了这条规则,内部服务器看到的客体隧道地址并对其作答,回包也能找到返回客体的路。
Windows 客体更笨拙。SYSTEM\CurrentControlSet\Services\Tcpip\Parameters 之下的 IPEnableRouter=1 只开启转发而不做转换,所以只有当企业路由已经把回程指向你的 host-only 前缀时才有效。Internet Connection Sharing 确实会做转换,但它会把共享网卡改成 192.168.137.1,所以在写主机路由之前先读出它实际取得的地址。
检查 3:主机路由指向客体,而不是隧道
只把主机需要的前缀经客体送出。在 Linux 上,每个前缀一条路由就够,而且能干净地回退:
sudo ip route add 10.20.0.0/16 via 192.168.56.2
ip route get 10.20.30.40
在 Windows 上对应的是 route add 10.20.0.0 mask 255.255.0.0 192.168.56.2。测试期间先不要加 -p,让错误的路由在重启后消失,等路径跑通再加。除非主机上每个应用都该走隧道,否则不要删掉主机的默认路由;如果确实需要全局隧道,就加上两条 /1 路由,并保留通往虚拟机监控程序网络的直连路由:
sudo ip route add 0.0.0.0/1 via 192.168.56.2
sudo ip route add 128.0.0.0/1 via 192.168.56.2
在 Windows 主机上还要检查接口跃点数。在 host-only 网卡被禁用期间添加的路由会留在表里却从不承载流量,而跃点数更低的第二块网卡可能赢得同一前缀的查找。
检查 4:DNS 跟随路由,而不是隧道的默认值
路由搬运数据包,解析器决定哪些域名产生数据包。把企业解析器限定在企业域内,而不是替换主机解析器,并把它指向新路由已经能到达的 DNS 服务器。在主机上使用 systemd-resolved 时:
resolvectl dns eth1 10.20.0.53
resolvectl domain eth1 '~corp.example.com'
在 Windows 上,名称解析策略表里的一条记录起同样作用:Add-DnsClientNrptRule -Namespace ".corp.example.com" -NameServers 10.20.0.53。如果客体在 host-only 地址上运行自己的解析器,就用那个地址,但必须先确认它是在 192.168.56.2 上应答,而不是在 loopback 上。
接着在把慢页面归咎于 DNS 之前先查 MTU。隧道接口小于主机 1500 字节的接口,所以发送带 DF 位置位的全尺寸报文的主机会在客体内部某处被丢弃。典型症状是 ping 能通,而内部 Web 应用在 TLS 握手之后卡住。在客体的转发路径上钳制 MSS:
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
检查 5:从两端验证
从主机对内部地址执行 traceroute 或 tracert,第一跳应显示 192.168.56.2,而不是物理网关。在客体里观察同一次尝试如何离开:tcpdump -ni any host 10.20.30.40 会在 host-only 侧显示转发后的请求、在隧道上显示转换后的请求。请求发出、返回,主机却依然沉默,说明问题在回程路径。
两种故障解释了其中大部分。要么因为缺少 NAT 规则,客体隧道 没有为 host-only 前缀携带回程路由;要么主机路由指向了客体重启后不再持有的地址。用 ip -br addr 和 ip route get 重新读取地址,而不是相信上一次会话的笔记。
检查 6:让它在重启和虚拟机监控程序升级后依然成立
把两半都持久化:主机上的 netplan 或 NetworkManager 路由配置、Windows 上的 route add ... -p,以及 /etc/sysctl.d/ 里的 net.ipv4.ip_forward=1 外加一套在客体中重新加载的防火墙规则。配置验证通过后立刻给客体做快照,赶在下一次企业客户端更新之前。
虚拟机监控程序重建网卡时,host-only 网络会被重新编号,从而悄悄让建立在 192.168.56.0/24 之上的每条路由和每条 NAT 规则失效。升级之后按顺序重跑前三项检查;这只要一分钟,却能抓住主机仍保留路由、而客体已搬到新子网的情况。
IPv6 需要单独过一遍。ip6tables 规则、AAAA 记录和第二套主机路由族不会自动跟随 IPv4 配置,能通过 IPv6 访问的企业服务会在 IPv4 正常时看起来永久损坏。
无论通过与否,按顺序看:主机能 ping 通客体的 host-only 地址;客体朝隧道方向转发并做 MASQUERADE;ip route get 返回客体作为内部前缀的网关;企业域名经限定解析器解析;traceroute 首先显示客体;同样的六个答案在重启后依然成立。