Docker
默认的网络模式是bridge
模式, 在宿主机上创建一个Linux bridge
:docker0
,并分配一个网段给该网桥使用。该模式下启动的容器,会分配一个该网段的IP
, 并通过veth-pair
接入网桥。为了能够从宿主机外部访问容器,需要在创建容器时指定-p
参数,在宿主机上将某个宿主机的端口映射到容器的端口。
如:
1 | docker run --rm -itd -p 80:80 nginx |
本文来简要分析一下从宿主机外访问bridge
网络模式下docker
容器的数据包路径。
整体的网络架构如图所示:
容器网络大量应用了netfilter
和iptables
。netfilter
是内核协议栈的包过滤框架,iptables
是建立在netfilter
框架之上的基于规则的过滤防火墙,它将规则组织成表
和链
结构对数据包进行操作和过滤。之前写过一篇简单的介绍<<IPTABLES机制分析>>。
netfilter
和iptables
整体处理路径如图:
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 | [root@default ~]# ip a |
查看iptables
规则:
1 | [root@default ~]# iptables-save |
入包
当从图中节点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-1
和DOCKER-ISOLATION-STAGE-2
。这两个链用于保证不同docker
网络之间的隔离。我们这里都匹配不上,最终进入DOCKER
链,匹配到下面规则,进行放行数据包:
1 | -A FORWARD -o docker0 -j DOCKER |
接下来进入到POST_ROUTING
挂载点,匹配nat
表的POSTROUTING
链。不能匹配任意规则,于是继续放行,从而最终调用到docker0
的网卡驱动将数据包发出。docker0
为Linux 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-2
。DOCKER
链中也匹配不到规则,最终命中:
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
有关,后续可以再写文章分析一下。