部落格好读版


在之前的章节中,我们用了偷吃步的方法,将 iptables 的 FORWARD 预设行为改成 ACCEPT。然而,当我们在上一章将 FORWARD 的预设行为改回 DROP 后,先前建立的网路连线却断掉了。这是怎么回事呢?

测试网路连线:ns1 到 ns0

sudo ip netns exec ns1 ping -c 1 172.18.0.2
# output
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.

--- 172.18.0.2 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

测试网路连线:ns1 到外网

sudo ip netns exec ns1 ping -c 1 8.8.8.8
# output
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

完全断线了!为什么会这样呢?原因就在于 iptables 的预设行为的差异。

iptables 预设行为的差异

iptables 的预设行为,主要体现在三个内建 chains (INPUT, OUTPUT, FORWARD) 的 policy 上:

  • ACCEPT (预设允许):採黑名单模式,只有明确被拒绝的流量才会被阻挡,其余都允许通过。
  • DROP (预设丢弃):採白名单模式,只有明确被允许的流量才能通过,其余都会被丢弃。

所以,当我们把 FORWARD 的 policy 改成 DROP 后,所有未被明确允许的流量,包含容器之间的通讯,都被阻挡了。我们必须重新设定 iptables 规则,才能让网路恢复正常。

分析:安装 Docker 前后,iptables 条目的变化

为了找出问题所在,我们比较了安装 Docker 前后 iptables 规则的差异。

Docker 安装前:

sudo iptables -t filter -nvL --line-numbers
# output
Chain INPUT (policy ACCEPT)
num target prot opt source destination

Chain FORWARD (policy ACCEPT)
num target prot opt source destination

Chain OUTPUT (policy ACCEPT)
num target prot opt source destination

Docker 安装后:

sudo iptables -t filter -nvL --line-numbers
# output
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination

Chain FORWARD (policy DROP 1 packets, 84 bytes)
num pkts bytes target prot opt in out source destination
1 664 2569K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
2 664 2569K DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
3 340 2547K ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
4 0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
5 171 11618 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
6 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination

Chain DOCKER (1 references)
num pkts bytes target prot opt in out source destination

Chain DOCKER-ISOLATION-STAGE-1 (1 references)
num pkts bytes target prot opt in out source destination
1 171 11618 DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
2 664 2569K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0

Chain DOCKER-ISOLATION-STAGE-2 (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * docker0 0.0.0.0/0 0.0.0.0/0
2 171 11618 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0

Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 664 2569K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0

sudo iptables-save -t filter
# output
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [1:84]
:OUTPUT ACCEPT [0:0]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
COMMIT

安装 Docker 后,多了许多 iptables 规则和 chains,这些都是 Docker 为我们自动建立的,用于管理容器网路。

解释:Docker 专用 chains 的规则

这些 chains 主要有两个目的:1. 安全隔离容器与外部网路;2. 集中管理容器流量。Docker 会自动建立必要的 rules,例如允许容器所需的 RELATED、ESTABLISHED 流量,以确保容器间以及容器与外部网路的正常通讯。

  • DOCKER

    • 处理容器 Network Address Translation (NAT) 映射、容器对外/对内连线等规则。
    • 例如:-A FORWARD -o docker0 -j DOCKER 表示从主机对外发送的封包,在通过 docker0 介面前,会先经过 DOCKER 链进行检查。
    • 若封包符合 Docker 建立的允许条件(包含连线状态、对应容器的埠等),就会被允许通过。
  • DOCKER-ISOLATION-STAGE-1、DOCKER-ISOLATION-STAGE-2

    • 这两个链负责不同网路介面(包含 docker0 与其他 Docker 桥接网路)之间的流量隔离。
    • STAGE-1:先判断封包来源与目的地是否为相同的 Docker 桥接网路,若否,则跳转到 STAGE-2。
    • STAGE-2:直接丢弃不允许跨桥接网路的流量。
  • DOCKER-USER

    • 提供使用者自订或覆盖 Docker 预设规则的空间。
    • 例如:若需在容器层新增额外过滤规则,可在 DOCKER-USER 中新增自订规则。
    • 预设最后有一行 -A DOCKER-USER -j RETURN,表示若无自订规则,则直接返回 FORWARD 链。
  • Docker 未建立容器时,这些链并无任何规则。让我们继续分析 FORWARD chain 的规则。

    解释:FORWARD chain 新增的规则

    让我们更仔细看看 FORWARD chain 中新增的规则:

    # iptables
    num pkts bytes target prot opt in out source destination
    1 664 2569K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
    2 664 2569K DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
    3 340 2547K ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
    4 0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
    5 171 11618 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
    6 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
    # iptables-save
    -A FORWARD -j DOCKER-USER
    -A FORWARD -j DOCKER-ISOLATION-STAGE-1
    -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
    -A FORWARD -o docker0 -j DOCKER
    -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
    -A FORWARD -i docker0 -o docker0 -j ACCEPT

  • -A FORWARD -j DOCKER-USER
    • 作用:跳转到 DOCKER-USER chain,让使用者可以在该 chain 里自行新增或覆盖 Docker 预设的规则。
  • -A FORWARD -j DOCKER-ISOLATION-STAGE-1
    • 作用:跳转到 DOCKER-ISOLATION-STAGE-1,这个 chain 主要用来隔离不同 Docker bridge 或者不同介面之间的流量。
    • 流程上:封包先进 DOCKER-USER,若无规则拦截就 RETURN 回 FORWARD,接着又跳到 DOCKER-ISOLATION-STAGE-1 做容器网路隔离检查。
  • -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
    • 作用:对 已有连线 (ESTABLISHED) 或相关连线 (RELATED) 且「流出(-o)docker0」的封包,直接放行。
    • 确保容器已建立连线的回应流量能正常到达容器。
  • -A FORWARD -o docker0 -j DOCKER
    • 作用:针对「流出 docker0(-o docker0)」但不符合前面 RELATED,ESTABLISHED的封包,跳转到 DOCKER chain 做后续处理。
    • DOCKER chain 是 Docker 自动新增的 chain,用来存放针对特定 Port / Protocol 的允许规则 (例如 docker run -p 8080:80 时会插入 DNAT、FORWARD 规则等)。
  • -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
    • 作用:当封包「从 docker0 进来」,但目的网卡不是 docker0(! -o docker0),就直接允许。
    • 典型情境:容器想要连到宿主机外部网路、或其他网卡,且不是在同一 docker0 bridge 上。
  • -A FORWARD -i docker0 -o docker0 -j ACCEPT
    • 作用:当封包「从 docker0 进来」且「出去也还是 docker0」,就允许。
    • 若有多个容器挂在同一个 docker0 bridge 上,彼此连线就会走这条规则

    简单来说

    • 第 1, 2 条对应的 DOCKER-USER 和 DOCKER-ISOLATION-STAGE-* chain 是 Docker 特别设计来让使用者自订规则、并且在容器与外部之间做网路隔离与控制。
    • 第 3 条是为了实现状态防火墙(Stateful firewall),不再次检查回应流量。
    • 第 4 条对应的 DOCKER chain 是设计对特定容器 Port / Protocol 做控制转发。
    • 第 5 条允许 Docker 容器对外连线。
    • 第 6 条允许 Docker 容器间的连线。

    为 docker1 新增 iptables 规则

    现在我们需要为自定义的 docker1 桥接网路新增相应的 iptables 规则,才能让 ns1 正常连线到 ns0 和外网。

    允许容器间的连线

    我们新增一条规则,类似于原来的第 6 条规则,但将网路介面替换为 docker1:

    sudo iptables -t filter -A FORWARD -i docker1 -o docker1 -j ACCEPT

    测试连线:

    sudo ip netns exec ns1 ping -c 1 172.18.0.2
    # output
    PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
    64 bytes from 172.18.0.2: icmp_seq=1 ttl=127 time=0.060 ms

    --- 172.18.0.2 ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 0.060/0.060/0.060/0.000 ms

    提醒若新增的规则错误,可以使用以下指令删除:sudo iptables -t filter -D FORWARD 7 (7 为该条目在 FORWARD 表中的编号,使用 iptables 命令的 --line-numbers flag 取得)

    允许容器到外网的连线

    同样地,我们需要新增规则允许 docker1 网路上的容器访问外部网路:

    sudo iptables -t filter -A FORWARD -i docker1 ! -o docker1 -j ACCEPT
    sudo iptables -t filter -A FORWARD -o docker1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

    测试连线:

    sudo ip netns exec ns1 ping -c 1 8.8.8.8
    # output
    PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
    64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=6.83 ms

    --- 8.8.8.8 ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 6.834/6.834/6.834/0.000 ms

    思考若只新增第 5 条规则,会发生什么事?因为第 5 条规则只允许从 docker1 进入,且目的网卡非 docker1 的流量通过。但在连线交握时,封包的进出方向会互换,因此无法符合规则,流量将被丢弃。

    总结

    在本章中,我们更加深入探讨 iptables 的运作原理,并成功让容器间以及从容器到外网的流量得以顺利传输。那么,如何从外网存取容器呢?

    ns1 的 IP 位址为 172.18.0.3,对于外网而言,其他主机的 Private IP 位址显然无法作为封包的目的位址。即使使用主机的 Public IP 位址作为目的位址,也无法精确得知容器内的 IP 位址。

    至于怎么解决,我们下一个章节再继续。

    参考

    • https://forums.docker.com/t/understanding-iptables-rules-added-by-docker/77210
    • https://moelove.info/2022/12/12/%E8%81%8A%E8%81%8A%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C%E5%92%8C-iptables/