Docker 破坏了 libvirt 桥接网络

Docker 破坏了 libvirt 桥接网络

这个问题让我抓狂。我运行了全新安装的 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似乎可以使一切正常。

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

我发现这个解决方案似乎很简单,通过编辑单个 UFW 文件https://stackoverflow.com/a/51741599/1091772,但它根本不起作用。

永久解决此问题并重启的最佳实践和安全方法是什么?

编辑:-A ufw-before-forward -i br0 -o br0 -j ACCEPT我最终在/etc/ufw/before.rulesCOMMIT 之前 添加了。我可以将此视为修复吗?还是它不会引发一些问题?

答案1

这个问题,实际上是一个特征:br_netfilter

解释是桥接网络过滤器代码由 Docker 启用用于内部容器隔离:旨在用于状态桥防火墙或利用iptables' 匹配和目标来自桥接路径,而不必(或能够)将它们全部复制ebtables. 完全不考虑网络分层,以太网桥代码在网络层 2 上调用iptables工作在 IP 层,即网络层 3。它只能在全局启用之前启用。内核 5.3(但 Docker 无法处理新的内核 5.3 功能):要么针对主机和每个容器,要么不针对任何容器。一旦了解了正在发生的事情并知道要查找什么,就可以做出适当的选择。

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

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

避免同一 LAN 上的两个系统通过网桥进行 NAT(见下面的示例)。

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

  • 永久阻止br_netfilter要加载的模块。通常blacklist不够,install必须使用。对于依赖于br_netfilter:显然是 Docker、Kubernetes、......

      echo install br_netfilter /bin/true > /etc/modprobe.d/disable-br-netfilter.conf
    
  • 加载模块,但禁用其效果:与 Docker 的结果相同。对于iptables' 效果是:

      sysctl -w net.bridge.bridge-nf-call-iptables=0
    

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

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

  • 在需要时绕过 br_netfilter 的影响,如 OP:在各种链(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
    

这些规则永远不应该匹配,因为同一 IP LAN 中的流量不会被路由,除非是一些罕见的 DNAT 设置。但由于br_netfilter它们确实匹配,因为它们首先被调用切换帧(“升级”为 IP 数据包)穿越.然后他们再次被要求路由数据包穿越路由器到不相关的接口(但不会匹配)。

  • 不要将 IP 放在桥上:将该 IP 放在接口的一端veth,将另一端放在桥上:这应该可以确保桥不会与路由交互,但这不是大多数容器/VM 通用产品所做的。

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

  • 切换所有到nftables在既定目标中,哪一个可以避免这些桥梁互动问题。目前,桥接防火墙尚不支持状态功能,因此仍然在制品但承诺在可用时会更加清洁,因为不会有任何“上行呼叫”。

你应该搜索一下是什么触发了加载br_netfilter(例如-m physdev:)看看你是否可以避免它,并选择如何进行。

最小 Docker 集成

当故障发生在 Docker 运行的主机初始网络命名空间中,而不是在新的(例如容器)网络命名空间中时,应将 OP 的规则添加到DOCKER-USER而不是单独使用,因为 Docker 通常会在已经找到的规则之前插入自己的规则。这甚至可以添加到某些网络启动脚本中。

这是一个幂等OP 案例的方法。如果之前不存在此链,Docker 将创建此链,因此忽略故障会使其在 Docker 之前或之后启动时都能正常工作。Likewise-I是必需的,因为 Docker(或其某些版本)可能会将虚拟-j RETURN规则附加到DOCKER-USER,因此-I使其在 Docker 之前或之后启动时都能正常工作。

iptables -N DOCKER-USER 2>/dev/null || true
iptables -C DOCKER-USER -i br0 -o br0 -j ACCEPT >/dev/null 2>&1 || 
    iptables -I DOCKER-USER -i br0 -o br0 -j ACCEPT

网络命名空间示例

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

让我们重现一个类似的简单案例,但有许多容器使用情况:路由器 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

让我们加载内核模块br_netfilter(以确保它不会晚于此)并使用(不是每个命名空间)切换禁用其效果网桥-nf-调用-iptables,仅在初始命名空间中可用:

modprobe br_netfilter
sysctl -w net.bridge.bridge-nf-call-iptables=0

警告:再次强调,这可能会破坏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

让我们 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

计数器不匹配:

# 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

让我们启用网桥-nf-调用-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

这次交换的数据包在 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

让我们制定一个 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

桥接代码通过 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

让我们看看从 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

... 而不是源 192.168.0.101。MASQUERADE 规则也从桥接路径调用。为了避免这种情况,可以添加(如在第七节的示例)之前的例外规则,或者如果可能的话,声明一个非桥接传出接口(现在它可用,您甚至可以使用它,-m physdev如果它必须是桥接......)。


随机相关:

LKML/netfilter-dev:br_netfilter:在非初始网络中启用:这将有助于按命名空间而不是全局启用此功能,从而限制主机和容器之间的交互。更新:已在 Linux 内核 5.3 中添加,但 Docker 不支持。

netfilter-设备:netfilter:physdev:放宽 br_netfilter 依赖:仅尝试删除不存在的物理开发规则可能会产生问题。(更新:已修复)。

netfilter-设备:桥接器的连接跟踪支持:WIP 桥接 netfilter 代码使用 nftables 准备有状态桥接防火墙,这次更加优雅。我认为这是摆脱 iptables(内核端 API)的最后步骤之一。更新:已在内核 5.3 中添加,但只要 Docker 没有完全重做以使用这些功能,就不会改变任何东西。

答案2

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

  • 1、保存当前的 iptables

    iptables-save > your-current-iptables.rules
    
  • 2、删除全部Docker 创建的规则

    iptables -D <DOCKER-CHAIN-RULES> <target-line-number>
    
  • 第三,添加 itpables 规则以接受任何流量到 INPUT、FORWARD 和 OUTPUT

    iptables -I INPUT -j ACCEPT
    iptables -I FORWARD -j ACCEPT
    iptables -I OUTPUT -j ACCEPT
    
  • 第四,重启 Docker

    service docker restart
    

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

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

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

  • 恢复 iptables

    iptables-restore < your-current-iptables.rules
    

答案3

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

[Service]
ExecStartPost=iptables -I DOCKER-USER -i br0 -o br0 -j ACCEPT

请注意,我不必创建链或检查此规则是否存在,因为 Docker 总是先清除现有规则,然后在运行命令之前重新创建链ExecStartPost。非常感谢https://serverfault.com/a/964491/979362弄清楚这一点:)

答案4

以 root 身份将以下行添加到 docker 之后运行的任何启动脚本中:

iptables -P FORWARD ACCEPT

如果您不想更改任何启动脚本并按需运行虚拟机,只需记住在 docker 服务(重新)启动后运行上述命令一次。

请记住,在允许网络访问之前启动的任何虚拟机都不会分配 DHCP IP,直到重试超时或 DHCP 更新请求。

副作用是所有流量都将由您的主机在任何网络、VPN 等之间转发 - 这不是一个很安全的解决方案,因为 VPN 子网上的攻击者可以将您的 VPN IP 设置为网关并探测可用网络并进行扫描。

Docker 隔离不应受到此变化的影响。

相关内容