云原生容器化-2 Docker网络原理

背景:

Docker作为容器化解决方案,特点之一是沙箱机制,以互不感知、互不影响的方式运行在宿主机上。通过网路命名空间实现网络层次上的隔离,使得每个容器拥有自己的网络协议栈、路由表、IP和端口等。通过网桥和veth-pair等虚拟网络设备以及iptables和路由规则的组合使用,为容器通讯提供了一个解决方案。
本文将以Docker网络的实现原理为主体内容进行介绍,会涉及veth-pair、网桥、Iptables和路由等网络知识,这些在前文都已介绍,可参考:

云原生容器化-1 Linux虚拟网络介绍

云原生容器化-1 Linux虚拟网络介绍2—netfilter/iptables框架

1.Docker网络结构图:

首先从整体上看,环境上安装Docker并运行容器后,宿主机的网络结构可以表示为:
在这里插入图片描述
上图中:
(1)新增docker0网桥,可在attach设备间转发数据包,实现二层通讯;
(2)docker0网桥被赋予IP地址,且连接在协议栈上,可作为网卡工作在IP层;
(3)容器处于不同的命名空间,因此容器间、容器与宿主机之间网络相互隔绝,互不感知、互不影响。
(4)每个运行的容器对应一对虚拟网卡veth-pair, 用于连接容器和网桥;
(3)veth-pair一端连接在docker上,另一端被虚拟化为容器的物理网卡eth0;

从整体上了解Docker网络结构后,下文将从以下几个方面介绍实现原理:
(1)同节点上容器间通讯;
(2)容器访问外网;
(3)外网访问容器内服务;

说明:本文主题内容围绕Docker默认的网络模式——网桥模式;host模式、自定义网桥模式、None模式以及container模式在本文中不涉及,文中不指定时默认为网桥模式。

2.同节点容器间通讯

整体上来看,同节点上容器间的通讯可以表示为:
在这里插入图片描述

容器运行时,Docker在docker0所在网段随机给容器分配一个IP;每个容器如此, 从而所有容器的IP处于同一网段。容器通过veth-pair连接到docker0网桥上,并将docker0作为自己的默认网关;从而容器、网桥间形成一个二层网络,容器间因此可通过docker0收发二层消息,实现容器间通讯。

案例分析:
tips:
一般容器为最简化安装,没有网络工具,可使用以下命令进行工具安装:

#安装网络工作
apt-get update  &&  apt install net-tools  && apt install iputils-ping && apt install iproute2

busybox支持的命令较多(包括ip和route),不需要手动安装,因此案例分析以busybox为例进行介绍
【1】运行两个busybox容器:

docker run -it -d --name=sy1 busybox
docker run -it -d --name=sy2 busybox

【2】进入容器1内部查看IP和路由:
在这里插入图片描述
容器1中IP为:172.17.0.3/16;
路由信息表明:目标为172.17.00/16网段的数据包,与自己在同一局域网(不需经过网关),并通过eth0向发出;

【3】进入容器2内部查看IP和路由:
在这里插入图片描述
容器2中IP为:172.17.0.4/16;
路由信息表明:目标设备为172.17.0.0/16网段的数据包,与自己在同一局域网(不需经过网关),并通过eth0向发出;

【4】在容器1中向容器2发送ICMP消息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9DuYi34-1659014236194)(C:\Users\0216001379\AppData\Roaming\Typora\typora-user-images\1658751725414.png)]
发现仍然可以ping通;

分析:
容器1与容器2交互流程如下:
(1) 容器1构造ICMP数据包准备发送;在自己的ARP缓存中查询目标IP为172.17.0.4的MAC地址;
(2) 未找到对应地址,广播ARP-request: who has 172.17.0.4;
(3) docker0收到消息后,因暂时没有ARP缓存和端口学习记录,进行全量端口转发,从而发送给veth2;
(4) 因此ARP-request请求:容器1:eth0->veth1->docker0->veth2->容器2:eth0;
(5) 容器2收到后响应ARP请求,数据包沿着:容器12:eth0->veth2->docker0->veth1->容器1:eth0到达容器1;
(6) 容器1收到ARP消息后,记录MAC地址和IP关系在自己的ARP缓存中,并向指定MAC地址发送ICMP-request;
(7) 沿着容器1:eth0->veth1->docker0->veth2->容器2:eth0 的线路到达容器2;
(8) 容器2构造ICMP-reply包进行响应,流程同容器1发送ICMP-reuqest(由于时初次,也需要经理ARP过程);
至此,容器1与容器2之间完成ping的通讯。
这里,读者也可以尝试在veth1、veth2和docker0上抓包进行结合上述分析流程进行分析;

3.容器访问外网

在这里插入图片描述

容器以及容器IP对外网是不可见的,因此需要通过SNAT进行通讯:容器向外发送数据包时,将源地址修改为宿主机的物理网卡地址;
Docker通过在iptables中添加如下规则实现SNAT:

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

表示外发数据包时,如果数据包源地址是172.17.0.0/16(容器所在网段),则将源地址MASQUERADE为宿主机物理网卡地址。

案例介绍:
以busybox为例进行介绍,在容器内ping外网地址(www.baidu.com),并在docker0和eth0上抓包
查看容器IP;
在这里插入图片描述
容器的IP和路由信息如上图所示:此时容器IP为172.17.0.2;
此时路由表中有一个默认网关以及一条路由:
路由表明,可以处理172.17.0.0/16网段的数据包;目标地址是112.80.248.75(www.baidu.com)时该路由信息无法匹配,因此数据包会通过eth0丢给默认网关,即网桥处理;
在容器内向112.80.248.75(www.baidu.com)发出ping请求:
在这里插入图片描述
在docker0网卡上抓包:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CaDKR5w6-1659014236198)(C:\Users\0216001379\AppData\Roaming\Typora\typora-user-images\1658907549126.png)]
docker0网卡上的数据包由容器->docker0: 源地址是172.17.0.2为容器地址,目标地址为112.80.248.75为外网地址;

在物理网卡上抓包:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EZbbaxPP-1659014236199)(C:\Users\0216001379\AppData\Roaming\Typora\typora-user-images\1658907525857.png)]
物理网卡上抓到数据包:源地址10.0.4.3为宿主机地址,目标地址保持不变;此时,源地址经历了由容器IP nat 为宿主机IP地址的过程。

4.外网访问容器

容器启动后可对外提供服务,因容器IP对外网不可见,需要借助宿主机的IP与外网通讯。宿主机虽然不可见容器IP, 但是借助路由表以及docker0可以通过容器IP访问容器。参考IPV4网络的端口NAT映射(公网无法访问局域网内的宿主机),可通过宿主机上的端口映射到容器内,从而实现外网与容器通讯。Docker基于此提供了两种机制实现对外提供服务:
(1)利用netfilter/iptables框架,通过在nat表中添加iptables规则实现端口映射;
(2)通过docker-proxy进程实现,每一对端口映射生成一个docker-proxy进程;
以下结合案例分别对两种机制进行介绍;其中案例使用httpd镜像,并指定宿主机的999端口映射到容器的80端口:

docker run -it -p 9999:80 --name=sy4 -d httpd

在这里插入图片描述此时容器IP为:172.17.0.3:
在这里插入图片描述

4.1 iptables规则

在宿主机上查看端口映射相关的iptables命令:
在这里插入图片描述
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER 表示数据包流入时,如果目标地址是本机, 则调用DOCKER链;
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9999 -j DNAT --to-destination 172.17.0.3:80 表示不是由docker0发向协议栈(容器外发)的数据包,且端口为9999时,将目标地址nat为172.17.0.3:80; 由此访问宿主机的8080端口的数据包会被转发到容器的80端口上。
数据包的流向可以用下图表示:
在这里插入图片描述
外网发送的数据包经过eth0传输至协议栈时:
(1) 在pre-routing阶段进行nat,目标地址被设置为172.17.0.3:80;
(2) 经过路由表时,发现地址不是本地地址,会经过forward进行转发;
(3) 由于路由表中显示172.17.0.0/16网段的数据包转发给docker0, 因此数据包沿着协议栈->docker0->容器sy4中;
读者可在eth0和docker0上分别进行抓包,会发现eth0处数据包地址为:10.0.4.3:9999; docker0处数据包的地址为172.17.0.3:80;

4.2 通过docker-proxy

每对端口映射,Docker都会生成一个docker-proxy进程;该案例中生成的docker-proxy进程侦听宿主机上的8080端口,收到后直接转发给容器的80端口,类似一个代理;数据包的流程图如下图所示:
在这里插入图片描述

测试前,需要取消4.1中的iptables规则,可以将原来原来对9999端口的nat修改成9998:

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

此时外网访问容器的9999端口时,数据包会经过内核协议栈->socket上传至docker-proxy进程(运行在用户空间),docker-proxy进程再经过socket->内核协议栈->docker0->容器的路线将数据包发送给容器sy4;
在这里插入图片描述
这里还可以进行一个测试:恢复iptables映射端口为9999,同时kill掉docker-proxy进程,再次访问容器内服务,仍然可以正常访问。

4.3 对比iptables与docker-proxy

从4.1和4.2图中的数据流向图可以看出,数据包在协议栈IP层会进行DNAT,走FORWARD链流程而不是INPUT链,即不会经过socket到达用户态,因此docker-proxy不会生效;且docker的每个容器的每对端口都会启动一个docker-proxy进程,且每个进程耗费约2M内存。
另外,Docker在没有ipv6table上为容器添加相应的DNAT规则。
因此ipv6场景下docker-proxy需要开启,其他情况建议关闭docker-proxy。

5.Docker与路由规则

前文在介绍Docker通讯时,已经简要介绍过路由表,这里进行总结一下,对路由表不熟悉的同学,可以参考云原生容器化-1 Linux虚拟网络介绍
容器之间能正常通讯离不开路由规则,这里的路由规则包括:容器内的路由规则和宿主机上的路由规则。
(1)宿主机路由表:
当Docker进程安装后,会在宿主机上创建一个docker0网卡,如图所示IP为172.17.0.1/16:
在这里插入图片描述
宿主机的路由规则:
在这里插入图片描述
该docker0网卡属于宿主机,IP(172.17.0.1)也被认为是本机地址; 同时会在路由表中加入一条路由信息,使得所有发送给172.17.0.0/16网段的数据包都会通过docker0网卡发送。

(2)容器路由表:
容器运行后,会虚拟一块物理网卡,并从docker0所在网关被赋予一个IP地址,因此所有容器都处于同一网段,IP如下所示:
在这里插入图片描述
容器内的路由规则:
在这里插入图片描述
路由规则表明:容器的默认网关是docker0网桥,可处理的IP段为172.17.0.0/16; 即:借助docker0的二层转发可以实现与172.17.0.0/16网段的IP通讯,其他IP转发给docker0网关,由网关负责转发和处理。

6.Docker与iptables规则

前文在介绍Docker通讯时,涉及到iptables规则,这里集中进行整理一下。对iptables不熟悉的同学,可以参考:云原生容器化-1 Linux虚拟网络介绍2—netfilter/iptables框架
Docker的网络通讯依赖iptables,docker进程启动以及容器启停过程中伴随着iptables规则的动态变化。以下通过Docker进程启动后、Docker容器运行后,Docker容器指定端口映射三种场景分别进行介绍。

6.1 Docker启动后

在分析iptables规则之前,先复习一下两个知识点:
(1)容器发出的数据包经过docker0网桥后,流入网络协议栈; 网络协议栈向容器发送数据时,先发送到docker0;
(2)-i 表示从某网卡流入,-o表示从某网卡流出;
由此,站在协议栈的角度:-o docker0 表示发送给容器;-i docker0 表示由容器发出;
在环境上安装Docker并启动后,iptables中新增如下规则:

*nat
-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

*filter
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -O docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT

可以看到nat表和filter表都进行了一些操作:

(1) nat表:

此时表中未见 -I/A DOCKER,即DOCKER链为空;-j DOCKER相当于调用了一个空函数-直接返回,因此前两条iptables规则这里暂时忽略 (后续在nat表里添加DOCKER链时进行介绍), 此时可以简化为:

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

表示POSTROUTING阶段:源地址是 172.17.0.0/16,且不是发向容器(即外网),需要使用MASQUERADE进行SNAT(nat至公网地址);
这里可以思考一下为什么不是发向容器就是发往外网的,而无可能是发给本地其他网卡的。
[发给本机的不会走到POST-ROUTING,而是走INPUT, 所以是转发的数据包]

(2) filter表:

*filter
-A FORWARD -O docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT

其中:
第一条:转发给容器的消息,如果是已经建立的链接(内核中会维持链接状态),允许数据包通过;
第二条表示:允许由容器发向外网的请求;
第三条表示:允许容器发送消息给容器,即主机上的容器通讯;

6.2 Docker容器运行后

iptables规则无变化;

6.3 Docker容器-指定端口映射运行后

启动容器并开启端口映射 -p 9999:80

*nat
-A POSTROUTING -s 172.17.0.3/16 -d 172.17.0.3/16 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9999 -j DNAT --to-destination 172.17.0.3:80

*filter
-A DOCKER -d 172.17.0.3/16 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

补充DOCKER链规则后,可以表示为:

*nat
-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.3/16 -d 172.17.0.3/16 -p tcp -m tcp --dport 80 -j MASQUERADE #待确认
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 9999 -j DNAT --to-destination 172.17.0.3:80

*filter
-A FORWARD -o docker0 -j DOCKER
-A DOCKER -d 172.17.0.3/16 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

如上所示,在nat表和filter表中各增加了一个自定义链DOCKER, 并给该链添加了规则;其中:nat表在PREROUTING和OUTPUT链引用了DOCKER链,filter在FORWARD链中引用了DOCKER链;注意:这里nat和filter表中的DOCKER链可以理解为完全不同的两个链,使用同一命名仅是起到分组作用,方便后期维护。

nat表:

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

表示:不是容器发出的,tcp端口为9999的数据包,进行DNAT,修改目标IP-端口为:172.17.0.3:80;
很早之前看到这里的时候有个疑问,这里不是NAT了一半,出去的时候不需要再NAT吗?
后面查阅大量资料发现,不需要,系统在内存里维持了一份连接状态,返回时会自动进行对应NAT;

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

表示:流入内核协议栈时-如果目标地址是本机地址,执行DOCKER链;

-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER

表示:流出内核协议栈时-如果目标地址不是lo,且是本机地址,执行DOCKER链;

filter表:

-A DOCKER -d 172.17.0.3/16 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT

目标地址是172.17.0.3/16,目标端口是80,且不是容器发出的以及时发向容器的数据包允许通过。
即:容器外通过docker0发给容器的,地址为172.17.0.3:80的数据包允许通过;

-A FORWARD -o docker0 -j DOCKER

转发时如果目标设备是容器,则进行判断:如果不是容器发出的,且地址为172.17.0.19:80的数据包允许通过。

本文中介绍的Docker的网络模式基于默认的网桥模式,类似的Docker还支持host模式、自定义网桥模式、None模式以及container模式等;在介绍完Docker网络原理后,理解后面几种模式也比较简单:
【1】host模式:使用宿主机的网络命名空间;此时可以把容器理解为运行在宿主机上的普通程序;
【2】none模式:不涉及网络交互,内部只有一个lo网卡,用于隔离网络(不需要与外界通讯)的场景;
【3】container模式:运行时需要指定共享网络的容器,容器运行后将与指定的容器共享同一命名空间,从而可以通过lo网卡实现相互通讯;
【4】自定义网桥,原理同默认的docker0;
读者可以对剩下的这几种网络模式按上述过程进行分析。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值