全部的 K8S学习笔记总目录,请点击查看。
docker容器是一块具有隔离性的虚拟系统,容器内可以有自己独立的网络空间,
- 多个容器之间是如何实现通信的呢?
- 容器和宿主机之间又是如何实现的通信呢?
- 使用-p参数是怎么实现的端口映射?
带着这些问题,我们来学习一下docker的网络模型,最后我会通过抓包的方式,给大家演示一下数据包在容器和宿主机之间的转换过程。
网络模式
我们在使用 docker run 创建 Docker 容器时,可以用 –net 选项指定容器的网络模式,Docker 有以下4种网络模式:
- bridge模式,使用 –net=bridge 指定,默认设置
- host模式,使用 –net=host 指定,容器内部网络空间共享宿主机的空间,效果类似直接在宿主机上启动一个进程,端口信息和宿主机共用
- container模式,使用 –net=container:NAME_or_ID 指定,指定容器与特定容器共享网络命名空间
- none模式,使用 –net=none 指定,网络模式为空,即仅保留网络命名空间,但是不做任何网络相关的配置(网卡、IP、路由等)
bridge模式
那我们之前在演示创建docker容器的时候其实是没有指定的网络模式的,如果不指定的话默认就会使用bridge模式,bridge本意是桥的意思,其实就是网桥模式。
那我们怎么理解网桥,如果需要做类比的话,我们可以把网桥看成一个二层的交换机设备。网桥模式示意图:
Linux 中,能够起到虚拟交换机作用的网络设备,是网桥(Bridge)。它是一个工作在数据链路层(Data Link)的设备,主要功能是根据 MAC 地址将数据包转发到网桥的不同端口上。
1 | $ yum install -y bridge-utils |
有了网桥之后,那我们看下docker在启动一个容器的时候做了哪些事情才能实现容器间的互联互通
Docker 创建一个容器的时候,会执行如下操作:
- 创建一对虚拟接口/网卡,也就是veth pair网卡对;
- veth pair的一端桥接到默认的 docker0 或指定网桥上,并具有一个唯一的名字,如 vethxxxxxx;
- veth pair的另一端放到新启动的容器内部,并修改名字作为 eth0,这个网卡/接口只在容器的命名空间可见;
- 从网桥可用地址段中(也就是与该bridge对应的network)获取一个空闲地址分配给容器的 eth0
- 配置容器的默认路由
那整个过程其实是docker自动帮我们完成的,清理掉所有容器,来验证。
1 | ## 清掉所有容器 |
我们如何知道网桥上的这些虚拟网卡与容器端是如何对应?
通过ifindex,网卡索引号
1 | ## 查看test1容器的网卡索引 |
整理脚本,快速查看对应:
1 | for container in $(docker ps -q); do |
上面我们讲解了容器之间的通信,那么容器与宿主机的通信是如何做的?
添加端口映射:
1 | ## 启动容器的时候通过-p参数添加宿主机端口与容器内部服务端口的映射 |
端口映射如何实现的?先来回顾iptables链表图
iptables运维 https://www.zsythink.net/archives/category/%e8%bf%90%e7%bb%b4%e7%9b%b8%e5%85%b3/iptables
访问本机的8088端口,数据包会从流入方向进入本机,因此涉及到PREROUTING和INPUT链,我们是通过做宿主机与容器之间加的端口映射,所以肯定会涉及到端口转换,那哪个表是负责存储端口转换信息的呢,就是nat表,负责维护网络地址转换信息的。因此我们来查看一下PREROUTING链的nat表:
1 | $ iptables -t nat -nvL PREROUTING |
规则利用了iptables的addrtype拓展,匹配网络类型为本地的包,如何确定哪些是匹配本地,
1 | $ ip route show table local type local |
也就是说目标地址类型匹配到这些的,会转发到我们的TARGET中,TARGET是动作,意味着对符合要求的数据包执行什么样的操作,最常见的为ACCEPT或者DROP,此处的TARGET为DOCKER,很明显DOCKER不是标准的动作,那DOCKER是什么呢?我们通常会定义自定义的链,这样把某类对应的规则放在自定义链中,然后把自定义的链绑定到标准的链路中,因此此处DOCKER 是自定义的链。那我们现在就来看一下DOCKER这个自定义链上的规则。
1 | $ iptables -t nat -nvL DOCKER |
此条规则就是对主机收到的目的端口为8088的tcp流量进行DNAT转换,将流量发往172.17.0.2:80,172.17.0.2地址是不是就是我们上面创建的Docker容器的ip地址,流量走到网桥上了,后面就走网桥的转发就ok了。
所以,外界只需访问172.21.51.143:8088就可以访问到容器中的服务了。
数据包在出口方向走POSTROUTING链,我们查看一下规则:
1 | $ iptables -t nat -nvL POSTROUTING |
大家注意MASQUERADE这个动作是什么意思,其实是一种更灵活的SNAT,把源地址转换成主机的出口ip地址,那解释一下这条规则的意思:
这条规则会将源地址为172.17.0.0/16的包(也就是从Docker容器产生的包),并且不是从docker0网卡发出的,进行源地址转换,转换成主机网卡的地址。大概的过程就是ACK的包在容器里面发出来,会路由到网桥docker0,网桥根据宿主机的路由规则会转给宿主机网卡eth0,这时候包就从docker0网卡转到eth0网卡了,并从eth0网卡发出去,这时候这条规则就会生效了,把源地址换成了eth0的ip地址。
注意一下,刚才这个过程涉及到了网卡间包的传递,那一定要打开主机的ip_forward转发服务,要不然包转不了,服务肯定访问不到。
抓包演示
我们先想一下,我们要抓哪个网卡的包
首先访问宿主机的8088端口,我们抓一下宿主机的eth0
1
$ tcpdump -i eth0 port 8088 -w host.cap
然后最终包会流入容器内,那我们抓一下容器内的eth0网卡
1
2
3
4# 容器内安装一下tcpdump
$ sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
$ apk add tcpdump
$ tcpdump -i eth0 port 80 -w container.cap
到另一台机器访问一下,
1 | $ curl 172.21.51.143:8088/ |
停止抓包,拷贝容器内的包到宿主机
1 | $ docker cp test:/root/container.cap /root/ |
把抓到的内容拷贝到本地,使用wireshark进行分析。
1 | $ scp root@172.21.51.143:/root/*.cap /d/packages |
(wireshark合并包进行分析)
进到容器内的包做DNAT,出去的包做SNAT,这样对外面来讲,根本就不知道机器内部是谁提供服务,其实这就和一个内网多个机器公用一个外网IP地址上网的效果是一样的,那这也属于NAT功能的一个常见的应用场景。
Host模式
容器内部不会创建网络空间,共享宿主机的网络空间。比如直接通过host模式创建mysql容器:
1 | $ docker run --net host -d --name mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7 |
容器启动后,会默认监听3306端口,由于网络模式是host,因为可以直接通过宿主机的3306端口进行访问服务,效果等同于在宿主机中直接启动mysqld的进程。
Conatiner模式
这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
1 | ## 启动测试容器,共享mysql的网络空间 |
在一些特殊的场景中非常有用,例如,kubernetes的pod,kubernetes为pod创建一个基础设施容器,同一pod下的其他容器都以container模式共享这个基础设施容器的网络命名空间,相互之间以localhost访问,构成一个统一的整体。
None模式
只会创建对应的网络空间,不会配置网络堆栈(网卡、路由等)。
1 | # 创建none的容器 |
为了能让这个模式的docker容器能够访问外部网络,我们需要手动的创建虚拟网卡对,并将一端插入到docker0网桥中,另一端插入到容器的网络空间中。其本质就是bridge模式的实现。
1 | # 创建虚拟网卡对 |
前置知识:
- ip netns 命令用来管理 network namespace。它可以创建命名的 network namespace,然后通过名字来引用 network namespace
- network namespace 在逻辑上是网络堆栈的一个副本,它有自己的路由、防火墙规则和网络设备。
默认情况下,子进程继承其父进程的 network namespace。也就是说,如果不显式创建新的 network namespace,所有进程都从 init 进程继承相同的默认 network namespace。 - 根据约定,命名的 network namespace 是可以打开的 /var/run/netns/ 目录下的一个对象。比如有一个名称为 net1 的 network namespace 对象,则可以由打开 /var/run/netns/net1 对象产生的文件描述符引用 network namespace net1。通过引用该文件描述符,可以修改进程的 network namespace。