qemu/KVM iptables 埠轉發
經過數十篇文章、blob、教程,甚至在 stackoverflow 上回答了問題,我仍然堅持我的問題:如何設置從主機到來賓 VM 的埠轉發。
第一:對不起我的英語水平很差,我會盡量說清楚
第二:我在網路方面絕對是新手,但我必須為我的同事設置這個伺服器
我們有一個公共託管的 Centos 7 伺服器,我們希望在其上設置 3 個 KVM 虛擬機來為我們的 Web 軟體提供多個測試環境。我的想法是為每個 VM 分配要轉發的埠範圍,假設埠 10001:19999 轉發到 VM 1 的 1:9999,埠 20001:29999 轉發到 VM 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 root@xx.xxx.xx.xxx:10022
它不起作用。
我錯過了什麼?
多個不同的問題
閱讀此示意圖將有助於了解數據包發生的操作順序,以進行以下說明:
filter/FORWARD
: 正確地允許 DNAT-ed 數據包目前這條規則:
... /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
: 原始目的地不是虛擬機的地址由於無法直接訪問 VM,因此客戶端不會嘗試連接到 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
僅在連接流的第一個數據包(作為所有nat掛鉤)中發生,並且僅在第一個數據包已被接收而不是發出時發生。當從遠端系統(不是libvirtd主機)嘗試時,OP 的規則應該正確觸發。測試應始終像最終案例一樣進行:如果用於遠端訪問,則測試應來自遠端(可能是這種情況,但 OP 沒有說明)。
當從主機進行測試時,這是不同的,因為第一個數據包永遠不是接收到的數據包,而是一個發出的數據包:它不會遍歷
PREROUTING
. 然後,VM 的回復不再是流中的第一個數據包(因此不再處於conntrack狀態NEW
),就像上面一樣,跳過所有這些,因為 NAT 完全由 Netfilter 處理並產生conntrack條目。之前的示意圖說明了這一點:僅針對“新”連接諮詢“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
幾乎所有嘗試都可以進行,如果沒有,很可能無法達到 VM 上的預期服務。... -p tcp --dport 10001:19999 -j DNAT --to 192.168.122.101:1-9999
結果不會神奇地將埠值減去 10000。它將在 1-9999 範圍內為每個不同的流選擇一個可用的任意埠(即:不匹配先前的*conntrack條目)。*因此,埠 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 將不得不被替換,所以……
他自己由於以下兩個原因不能使用:
- 除了地址之外,沒有ipset類型中有兩個埠可以從一個埠映射到另一個埠。
- 更重要的是,iptables的
DNAT
目標沒有使用這個子系統的規定。表格
使用nftables 1.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
部分甚至通用規則都可以與iptablesMASQUERADE
保持原樣,只有處理埠轉換到 VM 的 NAT 必須使用nftables完成。
- 按位運算
這可能是對nftables和核心版本要求最低的選項(但 CentOS 7 的核心 3.10 還不夠)。
nftables有更多的功能,但仍然不能做減法,所以不能減去 10000 到一個 tcp 埠。但是它可以很好地執行按位運算。因此,如果該範圍與 2 的冪的倍數對齊,則不是分配 10000 個埠範圍,那麼這種靜態埠映射是可能的。在 10000 附近,可能使用 8192(總共達到 65536/8192-1=7 個 VM)或 16384(總共 3 個 VM)。
讓我們使用 16384:第一個可用範圍 16384-32767 (0x4000-0x7fff) 和網路遮罩 16383 (0x3fff)。
|
由於埠轉換映射到目標埠的第一個範圍(0-16383),因此除了按位與(&
下面)之外,沒有按位或( )應用。對於類似的鉤子,優先級 -110 用於優先於iptables的 -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 root@192.0.2.2
在 TCP 3 次握手期間,這些conntrack條目會顯示在主機上:
conntrack -E
[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用於具有連接(和typeof語法)的 NAT 映射和核心 5.6。
nftables的
dnat
語句可以使用映射來輔助其更改(而iptables的DNAT
目標不能使用ipset)。使用之前的骨架:
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) 查找時間,這可能會開始影響數千條規則的性能。