在阅读了数十篇文章、blob、教程,甚至在 stackoverflow 上回答了问题之后,我仍然陷入我的问题:如何设置从主机到来宾虚拟机的端口转发。
首先:抱歉我的英语水平很差,我会尽量说得清楚
第二:我在网络方面绝对是新手,但我必须为我的同事设置这个服务器
我们有一个公共托管的 Centos 7 服务器,我们希望在其上设置 3 个 KVM 虚拟机来为我们的 Web 软件提供多个测试环境。我的想法是分配一系列要转发到每个虚拟机的端口,假设端口 10001:19999 转发到虚拟机 1 的 1:9999,端口 20001:29999 转发到虚拟机 2 的 1:9999,依此类推。
我尝试了很多解决方案,但没有一个有效。这是我当前的设置:
#> ifconfig
eno2: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet xx.xxx.xx.xxx netmask 255.255.255.0 broadcast xx.xxx.xx.255
ether aa:aa:aa:aa:aa:aa txqueuelen 1000 (Ethernet)
RX packets 10190055 bytes 644136763 (614.2 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 338010 bytes 27222247 (25.9 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0x92b00000-92bfffff
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 2283 bytes 4633913 (4.4 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2283 bytes 4633913 (4.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
virbr0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255
ether bb:bb:bb:bb:bb:bb txqueuelen 1000 (Ethernet)
RX packets 4448 bytes 566487 (553.2 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 3374 bytes 1243921 (1.1 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vnet0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
ether cc:cc:cc:cc:cc:cc txqueuelen 1000 (Ethernet)
RX packets 268 bytes 23314 (22.7 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2071 bytes 114034 (111.3 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
#> cat /etc/sysctl.conf
# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv4.ip_forward = 1
#> cat /etc/libvirt/hooks/qemu
#!/bin/bash
v=$(/sbin/iptables -L FORWARD -n -v | /usr/bin/grep 192.168.122.0/24 | /usr/bin/wc -l)
# avoid duplicate as this hook get called for each VM
[ $v -lt 1 ] && /sbin/iptables -I FORWARD 1 -o virbr0 -m state -s xx.xxx.xx.xxx/32 -d 192.168.122.0/24 --state NEW,RELATED,ESTABLISHED -j ACCEPT
update(){
if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then
/sbin/iptables -t nat -D PREROUTING 1 -d $GUEST_IP -p tcp --dport $HOST_PORT -j DNAT --to-destination $GUEST_IP:$GUEST_PORT -m comment --comment "$1 VM port forwarding"
fi
if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then
/sbin/iptables -t nat -I PREROUTING 1 -d $GUEST_IP -p tcp --dport $HOST_PORT -j DNAT --to-destination $GUEST_IP:$GUEST_PORT -m comment --comment "$1 VM port forwarding"
fi
}
GUEST_PORT=1-9999
if [ "${1}" = "VM1" ]; then
GUEST_IP=192.168.122.101
HOST_PORT=10001:19999
elif [ "${1}" = "VM2" ]; then
GUEST_IP=192.168.122.102
HOST_PORT=20001:29999
fi
update $1 $2
#>virsh net-edit default
<network>
<name>default</name>
<uuid>0db10b13-21c6-45c3-a891-ec46509b2121</uuid>
<forward mode='nat'/>
<bridge name='virbr0' stp='on' delay='0'/>
<mac address='aa:aa:aa:aa:aa:aa'/>
<ip address='192.168.122.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.122.2' end='192.168.122.254'/>
<host mac='bb:cc:dd:ee:ff:01' name='VM1' ip='192.168.122.101'/>
<host mac='bb:cc:dd:ee:ff:02' name='VM2' ip='192.168.122.102'/>
</dhcp>
</ip>
</network>
qemu 钩子似乎工作正常,iptables 规则正如我所希望的那样
#> iptables -L FORWARD -nv --line-number
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT all -- * virbr0 xx.xxx.xx.xxx 192.168.122.0/24 state NEW,RELATED,ESTABLISHED
2 185 13612 ACCEPT all -- virbr0 * 192.168.122.0/24 0.0.0.0/0
3 0 0 ACCEPT all -- virbr0 virbr0 0.0.0.0/0 0.0.0.0/0
4 186 13704 REJECT all -- * virbr0 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable
5 0 0 REJECT all -- virbr0 * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable
6 0 0 ACCEPT all -- * virbr0 xx.xxx.xx.xxx 192.168.122.0/24 state NEW,RELATED,ESTABLISHED
7 0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
#> iptables -t nat -L -n -v
Chain PREROUTING (policy ACCEPT 25580 packets, 2244K bytes)
pkts bytes target prot opt in out source destination
0 0 DNAT tcp -- * * 0.0.0.0/0 192.168.122.101 tcp dpts:10001:19999 /* VM1 port forwarding */ to:192.168.122.101:1-9999
Chain INPUT (policy ACCEPT 774 packets, 46800 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 578 packets, 44429 bytes)
pkts bytes target prot opt in out source destination
Chain POSTROUTING (policy ACCEPT 578 packets, 44429 bytes)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- * * 192.168.122.0/24 224.0.0.0/24
0 0 RETURN all -- * * 192.168.122.0/24 255.255.255.255
4 240 MASQUERADE tcp -- * * 192.168.122.0/24 !192.168.122.0/24 masq ports: 1024-65535
159 12084 MASQUERADE udp -- * * 192.168.122.0/24 !192.168.122.0/24 masq ports: 1024-65535
0 0 MASQUERADE all -- * * 192.168.122.0/24 !192.168.122.0/24
但是当我尝试通过 ssh 访问我的虚拟机时,假设从我的电脑访问 VM1,
ssh [email protected]:10022
它不起作用。
我缺少什么?
答案1
多个不同的问题
阅读此原理图将有助于理解数据包发生的操作顺序,以了解以下说明:
filter/FORWARD
:正确允许 DNAT 数据包
目前这条规则:
... /sbin/iptables -I FORWARD 1 -o virbr0 -m state -s xx.xxx.xx.xxx/32 -d 192.168.122.0/24 --state NEW,RELATED,ESTABLISHED -j ACCEPT
永远不会匹配:当转发(路由)时,数据包永远不会有主机地址的源,或者它将由主机发出,但不会遍历 PREROUTING 挂钩(但 OUTPUT)。nat/PREROUTING
更改目的地,而不是源。
要允许远程访问,请为每个允许的远程源指定一条规则(将主机的 xx.xxx.xx.xxx/32 替换为允许的远程客户端 yy.yyy.yy.yyy 的地址),或者不指定任何源以允许任何远程客户端:
/sbin/iptables -I FORWARD 1 -o virbr0 -d 192.168.122.0/24 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
如果想要具体匹配数据包首先到达目的地 xx.xxx.xx.xxx/32(例如,对于具有多个公共地址和一个专用于此角色的主机很有用),然后转换为 192.168.122.0/24比赛仍然有可能conntrack
(取代OP的state
比赛):
/sbin/iptables -I FORWARD 1 -o virbr0 -d 192.168.122.0/24 -m conntrack --ctstate NEW,RELATED,ESTABLISHED --ctorigdst xx.xxx.xx.xxx/32 -j ACCEPT
还有其他可能性,最简单的是简单地接受已经经历了先前规则中发出的任何 DNAT 转换的流的任何数据包部分,因为这样的 DNAT 仅当流被接受时才有用:
/sbin/iptables -I FORWARD 1 -m conntrack --ctstate DNAT -j ACCEPT
nat/PREROUTING
: 原始目的地不是虚拟机的地址
由于无法直接访问虚拟机,因此客户端不会尝试连接到 192.168.122.101:10022。如果它可以直接到达 192.168.122.101,那么它可以简单地连接到 192.168.122.101:22,并且不会问这个问题。
客户端将连接到主机的单个公共IP(甚至是备用公共IP)。主办方的iptables然后规则将端口转换为 IP 和端口。因此,如果主机具有 xx.xxx.xx.xxx 地址,则nat/PREROUTING
规则不得尝试匹配 VM 的 IP 目标,而是尝试匹配主机的 IP 目标。一旦发生这种情况,filter/FORWARD
就会看到最终目的地(如上一点所述)。
最后不要使用:
/sbin/iptables -t nat -I PREROUTING 1 -d $GUEST_IP -p tcp --dport $HOST_PORT -j DNAT --to-destination $GUEST_IP:$GUEST_PORT -m comment --comment "$1 VM port forwarding"
但例如这个(还指定传入接口以避免复杂化):
/sbin/iptables -t nat -I PREROUTING 1 -i eno2 -d xx.xxx.xx.xxx/32 -p tcp --dport $HOST_PORT -j DNAT --to-destination $GUEST_IP:$GUEST_PORT -m comment --comment "$1 VM port forwarding"
nat/OUTPUT 与 nat/PREROUTING
从主机本地发出的数据包的处理方式与转发(路由)数据包的处理方式不同。
OP 的附加规则位于nat/PREROUTING
.PREROUTING
当收到数据包时(并且在路由决策之前)发生。具体nat/PREROUTING
只发生在第一的连接流的数据包(如所有纳特hooks),并且仅当第一个数据包已被接收而不是发出时。
当尝试从偏僻的系统(不是库虚拟机主机),OP 的规则应该正确触发。测试应该始终像最终用例一样进行:如果是用于远程访问,测试应该来自远程(情况可能就是这样,但OP没有说明)。
当从主机测试时,这是不同的,因为第一的packet 永远不是接收到的数据包,而是发出的数据包:它不会遍历PREROUTING
。然后虚拟机的回复不再是流中的第一个数据包(因此不在连线状态NEW
),像上面一样,跳过所有这些,因为 NAT 完全由 Netfilter 处理,结果是连线入口。之前的示意图说明了这一点:
仅针对“新”连接查阅“NAT”表
因此,在主机案例中,nat/PREROUTING
与上一点相比的变化是:
/sbin/iptables -t nat -I PREROUTING 1 -i eno2 -d xx.xxx.xx.xxx/32 -p tcp --dport $HOST_PORT -j DNAT --to-destination $GUEST_IP:$GUEST_PORT -m comment --comment "$1 VM port forwarding"
永远不会触发:更改的效果尚不可用,因此目的地仍然是 xx.xxx.xx.xxx。正是这条规则将改变它。
在 中完成的类似设置nat/PREROUTING
也必须在 中完成nat/OUTPUT
:
iptables -t nat -I OUTPUT 1 -d xx.xxx.xx.xxx/32 -p tcp --dport $HOST_PORT -m comment --comment "VM1 port forwarding" -j DNAT --to-destination $GUEST_IP:$GUEST_PORT -m comment --comment "$1 VM port forwarding test from host"
现在将正确匹配。
另一方面,当使用 xx.xxx.xx.xxx 而不是 127.0.0.1 时,主机在端口 10001-19999 上的 tcp 服务将无法从其自身访问。由于主机具有直接连接性,人们仍然可以使用-d xx.xxx.xx.xxx/32
替换为 OP 原始规则的规则$GUEST_IP
,但除了测试规则集之外,它不会很有用。
但无论如何,在所有情况下...
iptables无法进行静态端口范围映射
...现在很可能导致Connection refused
几乎所有尝试都失败,如果没有,很可能无法达到虚拟机上的预期服务。
... -p tcp --dport 10001:19999 -j DNAT --to 192.168.122.101:1-9999
结果不会神奇地将端口值减去 10000。它将选择一个可用的任意端口(即:与之前的端口不匹配)连线每个不同流的 1-9999 范围内的条目)。因此,端口 10022 不会转换为端口 22,而是转换为该范围内的随机值,例如 6456,并且每次连续尝试新连接时,它都会是不同的端口值。
我没有基于的解决方案iptables为此,除了必须添加 9999 条规则(每个端口一条)之外。特别是对于端口 22,这可以工作:
... -p tcp -m tcp --dport 10022 -j DNAT --to 192.168.122.101:22
因此,人们确实应该选择一小部分端口,并一次执行一个规则的单个端口映射。如果 VM1 是 HTTP 1.x 服务器,则可以使用 3 条规则,一条用于端口 22,一条用于端口 80,一条用于端口 443(但主机上的 HTTP 反向代理可能是更好的解决方案)。
... -p tcp -m tcp --dport 10022 -j DNAT --to 192.168.122.101:22
... -p tcp -m tcp --dport 10080 -j DNAT --to 192.168.122.101:80
... -p tcp -m tcp --dport 10443 -j DNAT --to 192.168.122.101:443
奖励:更轻松的静态端口范围映射的方法
作为奖励,这里讨论了如何进行静态端口转换。无法在 CentOS 7 上使用因为它缺乏所需的功能。但迟早(2024-06-30) CentOS 7 必须更换,所以......
IP集
由于以下两个原因无法使用:
不IP集type 有两个端口,除了地址之外,还可以从一个端口映射到另一端口。
更重要的是,iptables'
DNAT
目标没有使用该子系统的规定。
nftables
测试用nftables1.0.2 和内核 5.16.x。
这需要最新版本的nftables和内核。 CentOS 7 不符合条件。nftables从内核 3.13 开始可用。 CentOS 7 使用内核 3.10:仅存在nftables已经是 Red Hat 的内核功能向后移植。许多较新的nftables该工具和内核中将缺少功能。
特别是在较旧的内核上nftables和iptables(遗留)特别与 NAT 挂钩冲突,因此不能一起用于执行 NAT(将无法注册或被默默忽略),而它们可以在较新的内核中很好地协同工作。
该filter/FORWARD
部分甚至通用MASQUERADE
规则可以保持原样iptables,只有处理到虚拟机的端口转换的 NAT 必须使用nftables。
按位运算
这可能是要求最低的选项nftables和内核版本(但 CentOS 7 的内核 3.10 还不够)。
nftables有更多功能,但仍然无法进行减法,因此无法将 10000 减去 tcp 端口。然而它可以很好地执行按位运算。因此,如果该范围与 2 的幂的倍数对齐,则可以实现这种静态端口映射,而不是分配 10000 个端口范围。接近 10000 范围时,可能会使用 8192(总共 65536/8192-1=7 个虚拟机)或 16384(总共 3 个虚拟机)。
让我们使用 16384:第一个可用范围 16384-32767 (0x4000-0x7fff) 和网络掩码 16383 (0x3fff)。由于端口转换映射到目标端口的第一个范围 (0-16383),因此除了按位与(如下)
|
之外,无需应用按位或 ( ) 。&
优先级-110用于优先于iptables' 对于类似的 hook,优先级为 -100。对于 NAT 如果nftables的 NAT 挂钩不匹配,iptables像往常一样,NAT 等效挂钩稍后仍然有机会匹配。
nft add table rangenat nft add chain rangenat prerouting '{ type nat hook prerouting priority -110; }' nft add chain rangenat output '{ type nat hook output priority -110; }'
对于第一个虚拟机:
nft add rule rangenat prerouting 'ip daddr xx.xxx.xx.xxx/32 tcp dport 16384-32767 dnat to 192.168.122.101:tcp dport & 0x3fff' nft add rule rangenat output 'ip daddr xx.xxx.xx.xxx/32 tcp dport 16384-32767 dnat to 192.168.122.101:tcp dport & 0x3fff'
使用地址为 203.0.113.11 的客户端连接到端口 16384+22=16406 上地址为 192.0.2.2 的主机的模型测试,就像此命令所做的那样(OP 的语法错误,应使用参数指定端口
-p
):ssh -p 16406 [email protected]
结果在这些连线
conntrack -E
TCP 3 次握手期间主机上显示的条目:[NEW] tcp 6 120 SYN_SENT src=203.0.113.11 dst=192.0.2.2 sport=42458 dport=16406 [UNREPLIED] src=192.168.122.101 dst=203.0.113.11 sport=22 dport=42458 [UPDATE] tcp 6 60 SYN_RECV src=203.0.113.11 dst=192.0.2.2 sport=42458 dport=16406 src=192.168.122.101 dst=203.0.113.11 sport=22 dport=42458 [UPDATE] tcp 6 432000 ESTABLISHED src=203.0.113.11 dst=192.0.2.2 sport=42458 dport=16406 src=192.168.122.101 dst=203.0.113.11 sport=22 dport=42458 [ASSURED]
回复端口 src (在第二部分)
sport=22
按预期显示。-
需要nftables 0.9.4用于具有串联的 NAT 映射(以及类型语法)和内核 5.6。
nftables'
dnat
语句可以使用地图协助其改变(同时iptables'DNAT
目标不能使用IP集)。与之前的骨架:
nft add table rangenat nft add chain rangenat prerouting '{ type nat hook prerouting priority -110; }' nft add chain rangenat output '{ type nat hook output priority -110; }'
添加地图:
nft add map rangenat port2ipport '{ typeof tcp dport : ip daddr . tcp dport; }'
这些通用规则:
nft add rule rangenat prerouting 'ip daddr xx.xxx.xx.xxx/32 dnat ip to tcp dport map @port2ipport nft add rule rangenat output 'ip daddr xx.xxx.xx.xxx/32 dnat ip to tcp dport map @port2ipport
然后在任何时候使用循环,都可以像这样填充:
nft add element rangenat port2ipport '{ 10001: 192.168.122.101 . 1 }' ... nft add element rangenat port2ipport '{ 10022: 192.168.122.101 . 22 }' ... nft add element rangenat port2ipport '{ 19999: 192.168.122.101 . 9999 }' nft add element rangenat port2ipport '{ 20001: 192.168.122.102 . 1 }' ...
或者只是将最少需要的部分组合在一起,例如:
nft add element rangenat port2ipport '{ 10022: 192.168.122.101 . 22, 10080: 192.168.122.101 . 80, 10443: 192.168.122.101 . 443, 20022: 192.168.122.102 . 22, 30022: 192.168.122.103 . 22 }'
作为其散列值,典型的查找时间地图是 O(1),与之前基于位运算的方法相当。以最简单的方式为每个端口使用一条规则iptables或者nftables将有 O(n) 查找时间,这可能会开始影响数千条规则的性能。