Keep learning, keep living...

0%

从外部访问Docker桥接网络容器路径分析

Docker默认的网络模式是bridge模式, 在宿主机上创建一个Linux bridge:docker0,并分配一个网段给该网桥使用。该模式下启动的容器,会分配一个该网段的IP, 并通过veth-pair接入网桥。为了能够从宿主机外部访问容器,需要在创建容器时指定-p参数,在宿主机上将某个宿主机的端口映射到容器的端口。
如:

1
docker run --rm  -itd -p 80:80 nginx

本文来简要分析一下从宿主机外访问bridge网络模式下docker容器的数据包路径。

整体的网络架构如图所示:

容器网络大量应用了netfilteriptablesnetfilter是内核协议栈的包过滤框架,iptables是建立在netfilter框架之上的基于规则的过滤防火墙,它将规则组织成结构对数据包进行操作和过滤。之前写过一篇简单的介绍<<IPTABLES机制分析>>

netfilteriptables整体处理路径如图:

netfilter有5个挂载点:

  • NF_INET_PRE_ROUTING:
    在网卡驱动收到数据包之后会调用ip_rcv()函数进行协议栈处理过程。它会调用NF_INET_PRE_ROUTING挂载点上注册的函数。

  • NF_INET_LOCAL_IN:
    在处理完上一阶段的函数后,会针对数据包进行路由选择,如果数据包是发送给本机的,则调用ip_local_deliver()函数处理。它会调用NF_INET_LOCAL_IN挂载点上注册的函数。

  • NF_INET_FORWARD:
    如果上述的数据包不是发送给本机的网络地址的,则会调用ip_forward()进行处理,它会调用NF_INET_FORWARD挂载点上注册的函数。

  • NF_INET_POST_ROUTING:
    对于上述来自转发的数据包处理完挂载点函数后会调用ip_output函数将数据进行发送。这个函数会调用NF_INET_POST_ROUTING挂载点上注册的函数。
    对于来自主机上应用程序的数据包在经过下边的NF_INET_LOCAL_OUT挂载点后,也会调用到ip_output进行数据包发送。

  • NF_INET_LOCAL_OUT:
    当前主机产生的TCP/UDP数据包经过路由选择后调用ip_local_out, 它会调用NF_INET_LOCAL_OUT挂载点上的函数。之后调用到ip_output函数进而流经上述的NF_INET_POST_ROUTING阶段。

从这里来看,NF_INET_POST_ROUTING阶段并不是直接位于路由过程之后,而是路由 -> FORWARD -> POST_ROUTING路由 -> LOCAL_OUT -> POST_ROUTING这样的路径中,名称叫做POST_ROUTING不是特别准确。

创建完容器后,网络接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@default ~]# ip a
......
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:d0:e3:37 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.11/24 brd 192.168.0.255 scope global noprefixroute eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fed0:e337/64 scope link
valid_lft forever preferred_lft forever
......
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:e4:0f:0f:26 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:e4ff:fe0f:f26/64 scope link
valid_lft forever preferred_lft forever
11: vethfd5436d@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 86:81:2a:a8:5a:c8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::8481:2aff:fea8:5ac8/64 scope link
valid_lft forever preferred_lft forever

查看iptables规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[root@default ~]# iptables-save
# Generated by iptables-save v1.4.21 on Thu Aug 24 01:52:06 2023
*filter
:INPUT ACCEPT [169:13126]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [111:11866]
: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 -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -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
# Completed on Thu Aug 24 01:52:06 2023
# Generated by iptables-save v1.4.21 on Thu Aug 24 01:52:06 2023
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [8:544]
:POSTROUTING ACCEPT [8:544]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80
COMMIT
# Completed on Thu Aug 24 01:52:06 2023

入包

当从图中节点node2上访问192.168.0.11:80的数据包到达网卡eth1之后,网卡驱动收包之后进入PRE_ROUTING挂载点。此时,会以raw,mangle,nat的表顺序进行规则匹配。

由于目的IP: 192.168.0.11是本机IP地址,因而会匹配到:

1
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER

而在DOCKER链中会匹配到:

1
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.17.0.2:80

从而进行DNAT转换,数据包变为:

1
192.168.0.12:44444 -> 172.17.0.2:80

然后进行路由,根据本地路由表,确实目标设备为docker0:

1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1

接下来进入FORWARD挂载点,开始匹配filter表中的FORWARD链。FORWARD链会直接跳转至DOCKER-USER链,该链中只有一条RETURN的规则。因而继续匹配DOCKER-ISOLATION-STAGE-1DOCKER-ISOLATION-STAGE-2。这两个链用于保证不同docker网络之间的隔离。我们这里都匹配不上,最终进入DOCKER链,匹配到下面规则,进行放行数据包:

1
2
-A FORWARD -o docker0 -j DOCKER
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

接下来进入到POST_ROUTING挂载点,匹配nat表的POSTROUTING链。不能匹配任意规则,于是继续放行,从而最终调用到docker0的网卡驱动将数据包发出。docker0Linux bridge设备,bridge的实现将数据包送到容器的namespace中。后续可以写文章分析bridge发送数据包的具体实现。

以上是外部数据包访问容器的转发路径,整体过程如图:

回包

当容器内的回包通过veth-pair设备到达docker0后,与上述的入包一样,首先进入PRE_ROUTING挂载点。此时不会匹配到任何规则,继续放行进行路由。根据目的IP: 192.168.0.12查找路由条目, 确定目标设备为eth1:

1
192.168.0.0/24 dev eth1 proto kernel scope link src 192.168.0.11 metric 101

接下来进入到FORWARD挂载点。与入包一样,跳过DOCKER-USER,DOCKER-ISOLATION-STAGE-1, DOCKER-ISOLATION-STAGE-2DOCKER链中也匹配不到规则,最终命中:

1
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT

接着进入POST_ROUTING挂载点。匹配到规则:

1
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

进行SNAT操作,将数据包转换为:

1
192.168.0.11:80 -> 192.168.0.12:44444

之后将数据包由eth1网卡发到物理网络,完成一次网络数据包交互流程。

回包的整体路径如下图:

本文介绍了从外部访问端口映射到宿主机的容器的数据包路径。而对于相同网桥下的不同容器之间的网络访问路径,和参数net.bridge.bridge-nf-call-iptables有关,后续可以再写文章分析一下。

参考