Centos

qemu/KVM iptables 埠轉發

  • March 19, 2022

經過數十篇文章、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

它不起作用。

我錯過了什麼?

多個不同的問題

閱讀此示意圖將有助於了解數據包發生的操作順序,以進行以下說明:

Netfilter 和通用網路中的數據包流

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類型中有兩個埠可以從一個埠映射到另一個埠。
  • 更重要的是,iptablesDNAT目標沒有使用這個子系統的規定。

表格

使用nftables 1.0.2 和核心 5.16.x 進行測試。

這需要最新版本的nftables和核心。CentOS 7 不符合條件。nftables從核心 3.13 開始可用。CentOS 7 使用核心 3.10:僅僅存在nftables就已經是 Red Hat 的核心特性反向移植。該工具和核心中將缺少許多較新的 nftables功能。

特別是在較舊的核心上, nftablesiptables(舊版)專門針對 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。

nftablesdnat語句可以使用映射來輔助其更改(而iptablesDNAT目標不能使用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),與之前基於位運算的方法相當。使用iptablesnftables以最簡單的方式為每個埠使用一條規則將需要 O(n) 查找時間,這可能會開始影響數千條規則的性能。

引用自:https://unix.stackexchange.com/questions/696034