我有一台服务器,上面有一些使用 libvirt 和桥接网络 (192.168.123.0/24) 运行的 KVM 虚拟机。我试图将主机公网 IP 上的一个端口转发到其中一台虚拟机 (192.168.123.103)。从互联网访问时,它可以正常工作,并且通过一条附加规则,它也可以从主机上正常工作,但我无法让它从虚拟网络内的机器上工作。
以下是 iptables 配置的相关部分(我认为):
# Generated by iptables-save v1.6.0 on Tue Sep 13 16:16:26 2016
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A PREROUTING -d x.x.x.x/32 -p tcp -m tcp --dport 7000 -j DNAT --to-destination 192.168.123.103
-A OUTPUT -d x.x.x.x/32 -o lo -p tcp -m tcp --dport 7000 -j DNAT --to-destination 192.168.123.103
-A POSTROUTING -s 192.168.123.0/24 -d 224.0.0.0/24 -j RETURN
-A POSTROUTING -s 192.168.123.0/24 -d 255.255.255.255/32 -j RETURN
-A POSTROUTING -s 192.168.123.0/24 ! -d 192.168.123.0/24 -p tcp -j MASQUERADE --to-ports 1024-65535
-A POSTROUTING -s 192.168.123.0/24 ! -d 192.168.123.0/24 -p udp -j MASQUERADE --to-ports 1024-65535
-A POSTROUTING -s 192.168.123.0/24 ! -d 192.168.123.0/24 -j MASQUERADE
COMMIT
# Completed on Tue Sep 13 16:16:26 2016
# Generated by iptables-save v1.6.0 on Tue Sep 13 16:16:26 2016
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A FORWARD -i lo -j ACCEPT
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -m conntrack --ctstate INVALID -j DROP
-A FORWARD -d 192.168.123.103/32 -p tcp -m tcp --dport 7000 -j ACCEPT
-A FORWARD -s 192.168.123.0/24 -i virbr1 -j ACCEPT
-A FORWARD -i virbr1 -o virbr1 -j ACCEPT
COMMIT
# Completed on Tue Sep 13 16:16:26 2016
当从 192.168.123.0/24 网络中的另一台机器访问 192.168.123.103:7000 时,我看到一个从 192.168.123.36 到 xxxx 的 SYN 数据包,然后是将目标替换为 192.168.123.103 的相同数据包,然后是来自 192.168.123.103 到 192.168.123.36 的 SYN-ACK 数据包,然后是来自 192.168.123.36 到 192.168.123.103 的重复 RST 数据包。
有人知道如何让它工作吗?
我现在看到的唯一解决方法是强制虚拟机内的 DNS 指向私有 IP,但我更希望可以保留公共 DNS。
答案1
首先:忽略 KVM、VM 和网桥。想象一下,任何地方都没有 VM 和网桥。这个网桥只是内部有服务器的“LAN”,并且有一个路由器(KVM 主机)将 LAN 路由到外部。这有助于思考,而且它更通用。
总结一下这些数据包发生的情况:
LAN(L) OUTSIDE(O)
C ----> R(B)
^ /
| /
| /
| /
| /
S<
网络 L 中的客户端 C(192.168.123.36)向另一个网络 O 中的另一个 IP B(xxxx)发送数据包。
路由器 R 将目的地转换为同一网络 L 中的服务器 S(192.168.123.103)并将其发送回网络 L。R 不会改变源 C。
S 收到来自同一网络内客户端的数据包并直接应答,无需路由器 R。
C 收到来自 S 的未知 SYN+ACK,用于从未请求的连接(请记住 C 请求 B,但不知道 S)。C 向 S 发送回 RST 以中止连接。
C 仍在等待来自 B 的 SYN+ACK,但始终未收到。C 重试...
如何让事情正常运作?找到一种方法,让从 S 返回的流量再次由 R 路由。路由器 R 知道该流量,因此它将能够处理该流量并将其返回到 C。对于 S 通过 R 将数据包发送回,目的地不能位于网络 L 中。最简单的配置是使用 B,因为您已经知道它(并且您已经在某些规则中拥有它,因此没有增加复杂性)。因此,当连接启动时,路由器 R 还必须将源地址从 C 更改为 B。
路由器 R 中更改源地址的规则必须在 POSTROUTING 中,但此时数据包不再携带目的地 B(它已在 PREROUTING 中更改),这增加了识别它的难度。
请注意,MASQUERADE 规则不是问题的一部分,只有 DNAT 和 C 和 S 位于同一 LAN 中的事实才导致了这个问题。
两种解决方法:
由于数据包从同一接口路由和返回的唯一方法是使用 DNAT 规则,因此假设所有来自 L 并传到 L 的数据包都是此类数据包。这很简单,但可能需要仔细检查安全注意事项,尤其是当规则使用 IP 而从不使用接口名称时。
附加到您的规则:iptables -t nat -A POSTROUTING -s 192.168.123.0/24 -d 192.168.123.0/24 -j SNAT --to-source x.x.x.x
不要假设任何事情,在 PREROUTING 中标记即将进行 DNAT 的数据包,并在稍后的 POSTROUTING 中使用该标记来识别它。只需注意,标记在路由器内部跟随数据包,当然不是在线路上。这里需要它并仅用于第一个数据包,在此之后,conntrack 将处理流程。此解决方案允许解决各种限制,例如想要
-i interface
在 POSTROUTING 中使用,打算在 PREROUTING 中使用 SNAT(如这里)...只需在第一步中标记它,然后在第二步中使用标记来执行它。
因此,请插入以下规则:#has to be before the DNAT: DNAT is final, MARK will continue iptables -t nat -I PREROUTING -s 192.168.123.0/24 -d x.x.x.x/32 -p tcp -m tcp --dport 7000 -j MARK --set-mark 1 iptables -t nat -A POSTROUTING -m mark --mark 1 -j SNAT --to-source x.x.x.x
注意:S 只会看到局域网 L 中所有客户端的 IP xxxx。还有其他方法可以实现此目的(例如:使用 NETMAP 和虚假的未使用的局域网,以便每个客户端都进行唯一映射以用于日志记录目的)