Docker 破坏了 libvirt 桥接网络

Lau*_*ent 16 iptables bridge ufw docker

这个问题让我发疯。我运行全新安装的 Ubuntu 18.04,使用:

  • ufw 来管理防火墙
  • 一座 br0 桥
  • lxd 和 libvirt (KVM)

我尝试了股票 docker.io 包和包形成 docker 自己的 deb 存储库。

我希望能够部署 docker 容器,选择 ip 来绑定其端口(例如 -p 10.58.26.6:98800:98800),然后使用 UFW 打开端口。

但是 docker 似乎创建了干扰 br0 桥的 iptables 规则(例如,主机无法 ping libvirt 来宾)

我环顾四周,找不到好的、具有安全意识的解决方案。

手动操作iptables -I FORWARD -i br0 -o br0 -j ACCEPT似乎可以使一切正常。

"iptables": falsedocker 守护进程的设置也允许网桥正常运行,但会破坏 docker 的容器出口网络。

我发现这个解决方案看起来很简单,通过编辑单个 UFW 的文件/sf/answers/3621911961/,但它根本不起作用。

永久解决这个问题的最佳实践和安全方法是什么,幸免于重新启动?

编辑: 我结束了加-A ufw-before-forward -i br0 -o br0 -j ACCEPT在年底/etc/ufw/before.rules前提交。我可以将其视为解决方案还是不会引起一些问题?

A.B*_*A.B 19

问题,其实是一个特性:br_netfilter

从描述中,我相信唯一合乎逻辑的解释是启用了网桥 netfilter 代码:用于有状态网桥防火墙或利用iptables的匹配和来自网桥路径的目标,而不必(或能够)将它们全部复制在ebtables 中。完全不考虑网络分层,网络第 2 层的以太网桥代码现在调用在 IP 级别(即网络第 3 层)工作的iptables。它只能在全局启用:主机和每个容器,或不启用。一旦了解正在发生的事情并知道要寻找什么,就可以做出适当的选择。

netfilter 项目描述了启用br_netfilter时的各种ebtables/iptables交互。特别有趣的是第 7 节解释了为什么有时需要一些没有明显效果的规则来避免来自桥接路径的意外影响,例如使用:

iptables -t nat -A POSTROUTING -s 172.16.1.0/24 -d 172.16.1.0/24 -j ACCEPT
iptables -t nat -A POSTROUTING -s 172.16.1.0/24 -j MASQUERADE
Run Code Online (Sandbox Code Playgroud)

避免同一 LAN 上的两个系统被...桥(参见下面的示例)进行 NAT。

您有几种选择可以避免出现问题,但是如果您不想知道所有详细信息,也不想验证某些 iptables 规则(有时隐藏在其他命名空间中)是否会被破坏,那么您所采取的选择可能是最好的:

  • 永久阻止加载br_netfilter模块。通常blacklist是不够的,install必须使用。对于依赖br_netfilter 的应用程序来说,这是一个容易出现问题的选择:显然是 Docker、Kubernetes ......

    echo install br_netfilter /bin/true > /etc/modprobe.d/disable-br-netfilter.conf
    
    Run Code Online (Sandbox Code Playgroud)
  • 加载模块,但禁用其效果。对于iptables的效果是:

    sysctl -w net.bridge.bridge-nf-call-iptables=0
    
    Run Code Online (Sandbox Code Playgroud)

    如果在启动时放置它,则应首先加载模块,否则此切换尚不存在。

前面的两个选择肯定会破坏iptables匹配-m physdevxt_physdev模块在自身加载时,自动加载br_netfilter模块(即使从容器添加的规则触发了加载,也会发生这种情况)。现在br_netfilter不会被加载,-m physdev可能永远不会匹配。

  • 在需要时解决 br_netfilter 的影响,例如 OP:如第 7 节所述,在各种链(PREROUTING、FORWARD、POSTROUTING)中添加那些明显的无操作规则。例如:

    iptables -t nat -A POSTROUTING -s 172.18.0.0/16 -d 172.18.0.0/16 -j ACCEPT
    
    iptables -A FORWARD -i br0 -o br0 -j ACCEPT
    
    Run Code Online (Sandbox Code Playgroud)

    这些规则不应该匹配,因为同一 IP LAN 中的流量不会被路由,除了一些罕见的 DNAT 设置。但是多亏了br_netfilter,它们确实匹配,因为它们首先被调用以获取穿越网桥的交换帧(“升级”为 IP 数据包)。然后它们被再次调用,以获取通过路由器到达不相关接口的路由数据包(但不会匹配)。

  • 不要将 IP 放在网桥上:将该 IP 放在veth接口的一端,将其另一端放在网桥上:这应该确保网桥不会与路由交互,但这不是大多数容器/虚拟机所做的常见的产品。

  • 您甚至可以将网桥隐藏在其自己隔离的网络命名空间中(这仅在这次想要与其他ebtables规则隔离时才有用)。

  • 将所有内容都切换到nftables,这在既定目标中将避免这些桥梁交互问题。目前,桥接防火墙没有可用的状态支持,它仍然是WIP,但承诺在可用时更干净,因为不会有任何“upcall”。

您应该搜索是什么触发了br_netfilter的加载(例如:)-m physdev,看看是否可以避免它,以选择如何继续。


网络命名空间示例

让我们使用网络命名空间重现一些效果。请注意,不会在任何地方使用任何ebtables规则。另请注意,此示例依赖于通常的 legacy iptables,而不是Debian buster 上默认启用的iptables over nftables

让我们重现一个与许多容器用法类似的简单案例:路由器 192.168.0.1/192.0.2.100 执行 NAT,后面有两个主机:192.168.0.101 和 192.168.0.102,与路由器上的网桥相连。两台主机可以通过网桥在同一个 LAN 上直接通信。

#!/bin/sh

for ns in host1 host2 router; do
    ip netns del $ns 2>/dev/null || :
    ip netns add $ns
    ip -n $ns link set lo up
done

ip netns exec router sysctl -q -w net.ipv4.conf.default.forwarding=1

ip -n router link add bridge0 type bridge
ip -n router link set bridge0 up
ip -n router address add 192.168.0.1/24 dev bridge0

for i in 1 2; do
    ip -n host$i link add eth0 type veth peer netns router port$i
    ip -n host$i link set eth0 up
    ip -n host$i address add 192.168.0.10$i/24 dev eth0
    ip -n host$i route add default via 192.168.0.1
    ip -n router link set port$i up master bridge0
done

#to mimic a standard NAT router, iptables rule voluntarily made as it is to show the last "effect"
ip -n router link add name eth0 type dummy
ip -n router link set eth0 up
ip -n router address add 192.0.2.100/24 dev eth0
ip -n router route add default via 192.0.2.1
ip netns exec router iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -j MASQUERADE
Run Code Online (Sandbox Code Playgroud)

让我们加载内核模块br_netfilter(以确保它不会稍后)并使用(非每个命名空间)切换bridge-nf-call-iptables禁用其效果,仅在初始命名空间中可用:

modprobe br_netfilter
sysctl -w net.bridge.bridge-nf-call-iptables=0
Run Code Online (Sandbox Code Playgroud)

警告:同样,这可能会破坏iptables规则,例如-m physdev主机上或依赖于br_netfilter加载和启用的容器中的任何地方。

让我们添加一些 icmp ping 流量计数器。

ip netns exec router iptables -A FORWARD -p icmp --icmp-type echo-request
ip netns exec router iptables -A FORWARD -p icmp --icmp-type echo-reply
Run Code Online (Sandbox Code Playgroud)

让我们ping:

# ip netns exec host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.058 ms

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1017ms
rtt min/avg/max/mdev = 0.047/0.052/0.058/0.009 ms
Run Code Online (Sandbox Code Playgroud)

计数器不匹配:

# ip netns exec router iptables -v -S FORWARD
-P FORWARD ACCEPT -c 0 0
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 0 0
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 0 0
Run Code Online (Sandbox Code Playgroud)

让我们启用bridge-nf-call-iptables并再次 ping:

# sysctl -w net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-iptables = 1
# ip netns exec host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.094 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.163 ms

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1006ms
rtt min/avg/max/mdev = 0.094/0.128/0.163/0.036 ms
Run Code Online (Sandbox Code Playgroud)

这次交换的数据包在 iptables 的过滤器/FORWARD 链中匹配:

# ip netns exec router iptables -v -S FORWARD
-P FORWARD ACCEPT -c 4 336
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 2 168
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 2 168
Run Code Online (Sandbox Code Playgroud)

让我们设置一个 DROP 策略(将默认计数器归零)并重试:

# ip netns exec host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1008ms

# ip netns exec router iptables -v -S FORWARD
-P FORWARD DROP -c 2 168
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 4 336
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 2 168
Run Code Online (Sandbox Code Playgroud)

桥接代码通过 iptables 过滤交换的帧/数据包。让我们像在 OP 中一样添加绕过规则(它将再次将默认计数器归零),然后再试一次:

# ip netns exec router iptables -A FORWARD -i bridge0 -o bridge0 -j ACCEPT
# ip netns exec host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.132 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.123 ms

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1024ms
rtt min/avg/max/mdev = 0.123/0.127/0.132/0.012 ms

# ip netns exec router iptables -v -S FORWARD
-P FORWARD DROP -c 0 0
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 6 504
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 4 336
-A FORWARD -i bridge0 -o bridge0 -c 4 336 -j ACCEPT
Run Code Online (Sandbox Code Playgroud)

让我们看看在从 host1 ping 期间,现在在 host2 上实际收到了什么:

# ip netns exec host2 tcpdump -l -n -s0 -i eth0 -p icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:16:11.068795 IP 192.168.0.1 > 192.168.0.102: ICMP echo request, id 9496, seq 1, length 64
02:16:11.068817 IP 192.168.0.102 > 192.168.0.1: ICMP echo reply, id 9496, seq 1, length 64
02:16:12.088002 IP 192.168.0.1 > 192.168.0.102: ICMP echo request, id 9496, seq 2, length 64
02:16:12.088063 IP 192.168.0.102 > 192.168.0.1: ICMP echo reply, id 9496, seq 2, length 64
Run Code Online (Sandbox Code Playgroud)

... 而不是源 192.168.0.101。MASQUERADE 规则也是从网桥路径调用的。为避免这种情况,要么在之前添加(如第 7 节的示例中所述)异常规则,要么声明非桥接传出接口,如果可能的话(现在它可用,-m physdev如果它必须是桥接,您甚至可以使用它.. .)


随机相关:

LKML/netfilter-dev: br_netfilter: enable in non-initial netns : 这将有助于为每个命名空间而不是全局启用此功能,从而限制主机和容器之间的交互。

netfilter-dev: netfilter: physdev:relax br_netfilter 依赖:仅仅试图删除一个不存在的physdev规则可能会产生问题。

netfilter-dev:对桥的连接跟踪支持:WIP 桥 netfilter 代码使用 nftables 准备有状态的桥防火墙,这一次更优雅。我认为摆脱 iptables(内核端 API)的最后步骤之一。

  • @MadMike 理解是解决方案的一半。如果您不明白它是如何工作的,则每个配置都需要一个解决方案。此外,最近的内核(5.something)具有每个命名空间设置,而不是全局所有命名空间范围的设置。 (2认同)

小智 5

如果上述威胁无法解决您的问题,以下是我在 Debian Stretch 上解决问题的方法。

步骤 3 完成后,您可以从另一台 PC ping 被阻止的 libvert KVM 主机,您将看到 ICMP 响应。

重新启动 Docker 还会将其所需的 iptables 规则添加回您的计算机,但它不会再阻止您桥接的 KVM 主机。

如果上述解决方案不适合您,您可以使用以下命令恢复 iptables:


小智 5

我通过将以下行添加到解决了这个问题/usr/lib/systemd/system/docker.service

[Service]
ExecStartPost=iptables -I DOCKER-USER -i br0 -o br0 -j ACCEPT
Run Code Online (Sandbox Code Playgroud)

请注意,我不必创建链或检查此规则是否存在,因为 Docker 总是先清除现有规则并在运行命令之前重新创建链ExecStartPost。非常感谢https://serverfault.com/a/964491/979362中的出色解释来解决这个问题:)