利用 iptables 实现 Proxmox VE 下虚拟机的 NAT 端口转发

0

一台物理机托管到机房,国内一般也就给 1 个 v4 公网 IP。所有业务都将通过使用不同端口的方式来复用这个 IP,那么就有以下两种方案:
1)把所有业务放在同一个系统内跑。显然,这种把鸡蛋放在同一个笼子里的方案不够优雅,即便使用容器等虚拟化技术进行隔离,系统内核也始终是共享的,非常不灵活。
2)使用内核级的虚拟化技术(例如 KVM)。这种方式更加灵活,可以充分发挥物理机的优势(托管到机房的服务器一般都具有海量 CPU 核心和内存),各个业务间的隔离也更加彻底。但这也将大大增加网络的复杂度,配置起来有些棘手。

我采用的就是方案 2,并且采用下文将要介绍的方式(iptables NAT 端口转发)来优雅地解决网络配置问题。


1. 了解网络拓扑架构

直接上图:

请注意这并不是默认的 PVE 环境网络拓扑架构。在 PVE 默认配置下,宿主机和虚拟机都直接连接虚拟网桥 0,虚拟网桥 0 桥接到物理机的网卡端口,网卡连接到机房网关,接入互联网。IP 地址在网桥上配置,而不是直接配置在网卡上。
实际上,可以把这样的架构理解为宿主机 / 虚拟机均直连机房网关的设计,把虚拟网桥理解为交换机。

我曾试过直接把 IP 地址配在网口上,让网桥 0 充当纯内网交换机。但这样一来,宿主机的网络直接断掉了。我没有细究原因,猜测是 PVE 的网络架构设计所致(我们常用的手机、电脑等设备,以及 PVE 下的虚拟机,都是直接把 IP 配置在其网口上的)。

图中的拓扑架构是配置完成后的样子。建立在宿主机上的虚拟网桥 1 充当内网交换机,连接各台虚拟机;虚拟网桥 0 提供互联网访问,作为宿主机的默认网关。宿主机启用了 IP 转发功能,使得虚拟机的请求能够从网桥 1 转发到网桥 0 ,从而访问互联网。

是不是有点熟悉?

没错,这种网络架构几乎就是路由器的架构。这时候,宿主机充当了软路由的角色。

了解了拓扑架构之后,就可以进行详细的操作了。

 


2. 添加虚拟网桥、启用 IP 转发并配置 SNAT

@YFSama 已经写好了便捷的一键配置脚本,适用于 PVE 6.x点击立即访问。由于不同 PVE 版本所使用的 Debian 系统版本也不同,所以不同版本 PVE 之间的脚本并不能通用

虽然有现成的轮子,但是原理还是要讲一下的。

所谓转发,即当主机拥有多于一块网络适配器(包括网卡、网桥等)时,能够根据数据包的目的 IP 地址和系统配置的路由表,将该数据包转发到相应的网关并处理。其实这就是路由器的原理之一。

开启 IP 转发的方法如下(这里只配置 IPv4),编辑 /etc/sysctl.conf ,根据实际情况取消以下行的注释添加该行到文件中

net.ipv4.ip_forward=1

执行 sysctl -p 使其生效。

然后添加虚拟网桥 1 。编辑 /etc/network/interfaces 文件,添加以下内容:

auto vmbr1
iface vmbr1 inet static
    address  172.22.161.254/24
    bridge-ports none
    bridge-stp off
    bridge-fd 0

请根据实际网络情况灵活修改以上内容。此处填写的地址将作为虚拟机网关地址(即宿主机在此网段的 IP 地址)。
各虚拟机将网口的 IP 地址配置到此网段下即可(也可在宿主机安装 DHCP 服务器之后令虚拟机自动获取 IP 地址),务必将子网掩码和网关填写正确。

最后在 iptables 上配置 SNAT 规则,将来源为内网网段 IP 封包中的来源地址替换为公网 IP,并使其永久生效:

# 添加 SNAT 规则
iptables -t nat -A POSTROUTING -s '172.22.161.0/24' -o vmbr0 -j MASQUERADE

# 确保网络启动时运行的脚本有 iptables 相关
touch /etc/network/if-pre-up.d/iptables

# 写入从文件恢复 iptables 规则的脚本
echo "
#!/bin/sh
/sbin/iptables-restore < /etc/iptables
" > /etc/network/if-pre-up.d/iptables

# 添加可执行权限
chmod +x /etc/network/if-pre-up.d/iptables

# 将规则保存到文件中
iptables-save > /etc/iptables

至此,位于内网的虚拟机就可以访问外网了。

简单说下 SNAT 的原理:
IP 数据包中包含源地址目的地址。源地址是网口的地址,目的地址当然就是要访问目标的地址了。对方收到此数据包后,将根据数据包中的目的地址返回数据,说白了就是把源地址和目的地址互换。
对虚拟机发出的数据包而言,源地址是内网 IP ,那么目标终端在返回数据的时候,就无法将数据包发送到正确的路由上去,从而导致虚拟机无法收到数据回包。SNAT 即在数据包被路由转发之后修改源地址到新的网关,从而使数据回包能够正常被接收。至于网关如何鉴别接收到的数据包属于哪一台内网机器,我查了一些资料也没能得到靠谱的回答。个人猜测可能是存在二层交换,即利用 MAC 地址转发;当然也可能会有一些自动化的 NAPT 策略

 


3. 配置 DNAT 以暴露内网服务

解决了内网访问外网的问题,接下来就是把内网的服务暴露到公网了。

这里还是利用 iptables ,对特定端口范围进行转发。

iptables -t nat -A PREROUTING -i vmbr0 -p tcp -d 19.19.8.10 --dport 10000:10999 -j DNAT --to 172.22.161.170:10000-10999

其中,vmbr0 为公网网关,tcp 可改为 udp/icmp/all,也可以添加多条相同端口但协议不同的记录。端口可以灵活配置,本例中即为所有到达 vmbr0 、目标地址为 19.19.8.10、目标端口为 10000~10999 范围的数据包,其目标地址将被修改为 172.22.161.170,同时目标端口也将被对应修改为 10000~10999 范围,接着再进行路由选择。

配置完成之后记得保存一下。如此一来,所有发送到公网 10000~10999 端口,且目的地址为 19.19.8.10 的 TCP 请求都将直接转发到内网 IP 为 172.22.161.170 的机器处理。

 


4. 解决内网机器无法使用公网 IP 互访的问题

你可能会注意到,在上面一切设置都完成之后,如果内网机器公网 IP + 端口的方式访问内网服务,将不能正常访问。然而,相同的服务从公网访问正常,内网机器的网络也通畅。

这是由于宿主机的路由策略导致的。这时候,你也许会问:不是所有到虚拟网桥 0 的网络封包都会匹配 iptables 规则进行转发吗?

问题就在这里。宿主机实际上是双栈网络,它同时拥有公网 IP 和内网 IP。换句话说,当宿主机收到目的地址是这两个 IP 的封包时,都将视为是发给“自己”的。公网发来的数据包会被虚拟网桥 0 接收,然后匹配规则转发;内网发来的数据包则会被虚拟网桥 1 接收,目的地址不属于内网的封包会被转发到虚拟网桥 0 上。这里是一种特殊情况,即目的地址宿主机公网 IP 的封包,根据前文所属,将被视为是发给“自己”的。

所以,这样的封包不会被转发到虚拟网桥 0,也就无法匹配配置在其上的路由规则。

那么我们要做的,就是把上面配置在虚拟网桥 0 的规则,在虚拟网桥 1 也配置一遍。这样一来,任何数据包都将匹配到端口转发的规则,从而解决了内网机器无法使用公网 IP 互访的问题。

公网网关配置的时候, -d <Destination IP Address> 参数实际是可以省略的,因为所有被发送到公网网关的数据包,其目的地址必然是公网 IP 地址(已由上级路由筛选),无需进行规则匹配筛选。
但是,在虚拟网桥 1 上配置的时候,数据包的目的地址可能是其下每台机器的任意一个,如果不加 -d <Destination IP Address> 参数,仅使用端口匹配,将使得内网部分服务由于匹配到了相应的规则,被转发到指定地址上去,而不是发送到我们本提供的地址。我们仅需要解决“内网机器无法使用公网 IP 互访”的问题,而不应破坏内网本应有的结构和功能,因此此参数是必需的,以使进行目的地址匹配,即可解决。