3

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

 8 months ago
source link: http://just4coding.com/2023/08/24/docker-bridge/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

2023-08-242023-08-25 Kernel

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

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

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

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

1.png

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

netfilteriptables整体处理路径如图:

2.png

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不是特别准确。

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

[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规则:

[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地址,因而会匹配到:

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

而在DOCKER链中会匹配到:

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

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

192.168.0.12:44444 -> 172.17.0.2:80

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

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链,匹配到下面规则,进行放行数据包:

-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发送数据包的具体实现。

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

3.png

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

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链中也匹配不到规则,最终命中:

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

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

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

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

192.168.0.11:80 -> 192.168.0.12:44444

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

回包的整体路径如下图:

4.png

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK