通过 TUN OpenVPN 服务器路由 host-only 网络中的 VM 流量
将隧道与 host-only 前缀分开,推送来宾路由,然后让每台虚拟机都有一条经由宿主机的、刻意设计的回程路径。
host-only 适配器创建本地虚拟网络,而经过路由的 TUN 隧道在地址之间搬运 IP 包。两者都不是通往对方的网桥,因此可用的配置必须说明谁负责转发、回程路由归谁所有。把这两个职责弄反,来宾就会用隧道客户端从未请求过的源地址回应隧道。下面的设计让两个前缀彼此分开,并把回程路径显式化。
两个网络,两个前缀
给隧道地址池和 host-only 网络分配不同的前缀,绝不要拿其中一个顶替另一个。在这个例子里,VPN 池是 10.44.0.0/24,host-only 网络是 192.168.99.0/24,宿主机在 192.168.99.1,来宾在 192.168.99.20。把同一个前缀同时分配给 TUN 设备和 host-only 适配器并不会把两者合并。它只是对同一个地址范围提出两条主张,路由表随后会为碰巧留下的那条主张作答。
host-only 适配器既存在于宿主机上,也存在于来宾内部,而它自身没有离开这台机器的路径。桥接和 NAT 连接暴露面不同,而仅仅为了访问某一个来宾服务,并不需要切到桥接模式。
宿主机已经握有路径
在添加任何东西之前,先看看已经有什么。宿主机在 host-only 网络上有一个接口,也有一条该前缀的路由,二者都在适配器启用时创建。
ip -br addr show
ip route show 192.168.99.0/24
sysctl net.ipv4.ip_forward
第三行是把宿主机变成一跳的机制。没有它,从隧道抵达的包虽然指向宿主机可达的网络,却不会被转发。转发是必要的,但并不充分。过滤策略仍然决定隧道接口与 host-only 接口之间的某个具体流是否被允许,而这个决定应当写下来,而不是留在没人读过的默认策略里。
推送路由,然后过滤它能到达的范围
服务器可以把 host-only 前缀提供给接受推送路由的客户端,这与其他任何位于隧道之后的子网所用的机制相同。
push "route 192.168.99.0 255.255.255.0"
iptables -A FORWARD -i tun0 -o vboxnet0 -d 192.168.99.20 -p tcp --dport 5432 -j ACCEPT
iptables -A FORWARD -i vboxnet0 -o tun0 -s 192.168.99.20 -p tcp --sport 5432 -m conntrack --ctstate ESTABLISHED -j ACCEPT
这两条规则允许一个方向上的一个服务及其回应。覆盖整个前缀的路由加上只放行一个端口的过滤器是有意为之的组合;路由加上无条件放行,就是实验室服务变成暴露服务的方式。推送路由也不是授权。客户端接受它,宿主机转发它,来宾防火墙和服务本身仍然有话要说。
来宾需要回程路径
从 10.44.0.x 访问到的来宾必须把回应发往宿主机的 host-only 地址,或者由宿主机做源地址转换。从 VirtualBox DHCP 拿到地址,并不能说明这两种安排中的任何一种。
ip route add 10.44.0.0/24 via 192.168.99.1 dev enp0s8
ip route get 10.44.0.15
在来宾内部添加该路由可以保留原始源地址,从而让按客户端区分的策略和日志仍然有意义。在宿主机上做有范围的源地址转换是另一种选择,它同样可行,代价是隐藏了到底是哪个客户端连上来。重叠是另一种失败模式。如果一个远程客户端的家庭 LAN 已经在用 192.168.99.0/24,它会把推送来的路由读成自己的网络,而服务器端的任何改动都无法消除这种歧义。重新给一侧编号,或者挑一个几乎不会冲突的前缀。
TUN 设备承载 IP 包,因此在来宾内部添加默认网关并不会让隧道变成与 host-only 适配器同处一个以太网段。TAP 桥接是另一种设计,它有自己的客户端支持和地址归属,把两种描述混在一起,会得到永远收敛不了的配置。
先验证,再让它扛得住一次重启
用一个普通的远程客户端测试一个无害服务,检查来宾的回程路由,并确认你没有放行的服务是被拒绝的,而不是变慢。
ss -lntp '( sport = :5432 )'
conntrack -L | grep 192.168.99.20
然后重启 VPN 服务和虚拟机的网络,再重复同样的三项检查。手工添加的路由只能撑到下一次重启,而只在热会话里生效的配置算不上设计。把两个前缀、转发策略和来宾路由记在一起。下一个添加虚拟机的人会需要这三样,否则他将在一个又一个失败的连接里重新发现它们。