Docker单机网络模型动手实验

2023-11-24 15:17:38 浏览数 (1)

容器的本质

容器的本质就是一个进程,只不过对它进行了Linux Namesapce隔离,让它看不到外面的世界,用Cgroups限制了它能使用的资源,同时利用系统调用pivot_rootchroot切换了进程的根目录,把容器镜像挂载为根文件系统rootfsrootfs中不仅有要运行的应用程序,还包含了应用的所有依赖库,以及操作系统的目录和文件。rootfs打包了应用运行的完整环境,这样就保证了在开发、测试、线上等多个场景的一致性。

从上图可以看出,容器和虚拟机的最大区别就是,每个虚拟机都有独立的操作系统内核Guest OS,而容器只是一种特殊的进程,它们共享同一个操作系统内核。

看清了容器的本质,很多问题就容易理解。例如我们执行 docker exec 命令能够进入运行中的容器,好像登录进独立的虚拟机一样。实际上这只不过是利用系统调用setns,让当前进程进入到容器进程的Namesapce中,它就能“看到”容器内部的情况了。

容器网络

如何让容器之间互相连接保持网络通畅,Docker有多种网络模型。对于单机上运行的多个容器,可以使用缺省的bridge网络驱动。而容器的跨主机通信,一种常用的方式是利用Overlay 网络,基于物理网络的虚拟化网络来实现。

本文会在单机上实验展示bridge网络模型,揭示其背后的实现原理。下一篇文章会演示容器如何利用Overlay 网络进行跨主机通信。

我们按照下图创建网络拓扑,让容器之间网络互通,从容器内部可以访问外部资源,同时,容器内可以暴露服务让外部访问。

在开始动手实验之前,先简单介绍一下bridge网络模型会用到的Linux虚拟化网络技术。

Veth Pairs

Veth是成对出现的两张虚拟网卡,从一端发送的数据包,总会在另一端接收到。利用Veth的特性,我们可以将一端的虚拟网卡"放入"容器内,另一端接入虚拟交换机。这样,接入同一个虚拟交换机的容器之间就实现了网络互通。

Linux Bridge

交换机是工作在数据链路层的网络设备,它转发的是二层网络包。最简单的转发策略是将到达交换机输入端口的报文,广播到所有的输出端口。当然更好的策略是在转发过程中进行学习,记录交换机端口和MAC地址的映射关系,这样在下次转发时就能够根据报文中的MAC地址,发送到对应的输出端口。

我们可以认为Linux bridge就是虚拟交换机,连接在同一个bridge上的容器组成局域网,不同的bridge之间网络是隔离的。 docker network create [NETWORK NAME]实际上就是创建出虚拟交换机。

iptables

容器需要能够访问外部世界,同时也可以暴露服务让外界访问,这时就要用到iptables。另外,不同bridge之间的隔离也会用到iptables

我们说的iptables包含了用户态的配置工具(/sbin/iptables)和内核netfilter模块,通过使用iptables命令对内核的netfilter模块做规则配置。

netfilter允许在网络数据包处理流程的各个阶段插入hook函数,可以根据预先定义的规则对数据包进行修改、过滤或传送。

从上图可以看出,网络包的处理流程有五个关键节点:

  • PREROUTING:数据包进入路由表之前
  • INPUT:通过路由表后目的地为本机
  • FORWARDING:通过路由表后,目的地不为本机
  • OUTPUT:由本机产生,向外转发
  • POSTROUTIONG:发送到网卡接口之前

iptables 提供了四种内置的表 raw → mangle → nat → filter,优先级从高到低:

  • raw 用于配置数据包,raw中的数据包不会被系统跟踪。不常用。
  • mangle 用于对特定数据包的修改。不常用。
  • nat: 用于网络地址转换(NAT)功能(端口映射,地址映射等)。
  • filter:一般的过滤功能,默认表。

每个表可以设置在多个指定的节点,例如filter表可以设置在INPUTFORWARDINGOUTPUT等节点。同一个节点中的多个表串联成

iptables 是按照的维度来管理规则,中包含多个中包含规则列表。例如我们使用sudo iptables -t filter -L 查看filter表:

可以看到,filter表中包含三个,每个中定义了多条规则。由于filter是缺省表,上面的命令可以简化为:sudo iptables -L,即不通过-t指定表时,操作的就是filter表。

在容器化网络场景,我们经常用到的是在nat表中设置SNATDNAT。源地址转换是发生在数据包离开机器被发送之前,因此SNAT只能设置在POSTROUTIONG阶段。DNAT是对目标地址的转换,需要在路由选择前完成,因此可以设置在PREROUTINGOUTPUT阶段。

动手实验

有了前面的背景知识,我们就可以开始动手实验了。因为涉及到很多系统级设置,建议在一个“干净”的虚拟机内折腾,以免干扰到工作环境。我使用的实验环境是Ubuntu 18.04.1 LTS,不需要安装docker,我们使用系统命令模拟出容器网络环境。

场景一:容器间的网络互通

  • 创建“容器”

从前面的背景知识了解到,容器的本质是 Namespace Cgroups rootfs。因此本实验我们可以仅仅创建出Namespace网络隔离环境来模拟容器行为:

代码语言:javascript复制
sudo ip netns add docker0
sudo ip netns add docker1

查看创建出的网络Namesapce

代码语言:javascript复制
$ ls -l /var/run/netns
-r--r--r-- 1 root root 0 Nov 11 03:52 docker0
-r--r--r-- 1 root root 0 Nov 11 03:52 docker1

  • 创建Veth pairs
代码语言:javascript复制
sudo ip link add veth0 type veth peer name veth1
sudo ip link add veth2 type veth peer name veth3

查看创建出的Veth pairs

代码语言:javascript复制
$ip addr show
...
3: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 3e:fe:2b:90:3e:b7 brd ff:ff:ff:ff:ff:ff
4: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 6a:a3:02:07:f4:92 brd ff:ff:ff:ff:ff:ff
5: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 76:14:e5:0e:26:98 brd ff:ff:ff:ff:ff:ff
6: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 6a:0a:84:0f:a7:f7 brd ff:ff:ff:ff:ff:ff

  • 将Veth的一端放入“容器”

设置Veth一端的虚拟网卡的Namespace,相当于将这张网卡放入“容器”内:

代码语言:javascript复制
sudo ip link set veth0 netns docker0
sudo ip link set veth2 netns docker1

查看“容器” docker0 内的网卡:

代码语言:javascript复制
$ sudo ip netns exec docker0 ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: veth0@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 6a:a3:02:07:f4:92 brd ff:ff:ff:ff:ff:ff link-netnsid 0

ip netns exec docker0 ...的意思是在网络Namesapce docker0的限制下执行后面跟着的命令,相当于在“容器”内执行命令。

可以看到,veth0已经放入了“容器”docker0内。同样使用命令sudo ip netns exec docker1 ip addr show查看“容器”docker1内的网卡。

同时,在宿主机上查看网卡ip addr,发现veth0veth2已经消失,确实是放入“容器”内了。

  • 创建bridge

安装bridge管理工具brctl

代码语言:javascript复制
sudo apt-get install bridge-utils

创建bridge br0

代码语言:javascript复制
sudo brctl addbr br0

  • 将Veth的另一端接入bridge
代码语言:javascript复制
sudo brctl addif br0 veth1
sudo brctl addif br0 veth3

查看接入效果:

代码语言:javascript复制
sudo brctl show

两个网卡veth1veth3已经“插”在bridge上。

  • 为"容器“内的网卡分配IP地址,并激活上线

docker0容器:

代码语言:javascript复制
sudo ip netns exec docker0 ip addr add 172.18.0.2/24 dev veth0
sudo ip netns exec docker0 ip link set veth0 up

docker1容器:

代码语言:javascript复制
sudo ip netns exec docker1 ip addr add 172.18.0.3/24 dev veth2
sudo ip netns exec docker1 ip link set veth2 up

  • Veth另一端的网卡激活上线
代码语言:javascript复制
sudo ip link set veth1 up
sudo ip link set veth3 up

  • 为bridge分配IP地址,激活上线
代码语言:javascript复制
sudo ip addr add 172.18.0.1/24 dev br0
sudo ip link set br0 up

  • “容器”间的互通测试

我们可以先设置监听br0

代码语言:javascript复制
sudo tcpdump -i br0 -n

从容器docker0 ping 容器docker1

代码语言:javascript复制
sudo ip netns exec docker0 ping -c 3 172.18.0.3

br0上监控到的网络流量:

代码语言:javascript复制
05:53:10.859956 ARP, Request who-has 172.18.0.3 tell 172.18.0.2, length 28
05:53:10.859973 ARP, Reply 172.18.0.3 is-at 06:f4:01:c2:dd:6e, length 28
05:53:10.860030 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1310, seq 1, length 64
05:53:10.860050 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1310, seq 1, length 64
05:53:11.878348 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1310, seq 2, length 64
05:53:11.878365 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1310, seq 2, length 64
05:53:12.901334 IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1310, seq 3, length 64
05:53:12.901350 IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1310, seq 3, length 64
05:53:16.006471 ARP, Request who-has 172.18.0.2 tell 172.18.0.3, length 28
05:53:16.006498 ARP, Reply 172.18.0.2 is-at c2:23:fe:ac:f5:4e, length 28

可以看到,先是172.18.0.2发起的ARP请求,询问172.18.0.3MAC地址,然后是ICMP的请求和响应,最后是172.18.0.3的ARP请求。因为接在同一个bridge br0上,所以是二层互通的局域网。

同样,从容器docker1 ping 容器docker0也是通的:

代码语言:javascript复制
sudo ip netns exec docker1 ping -c 3 172.18.0.2

场景二:从宿主机访问“容器”内网络

在“容器”docker0内启动服务,监听80端口:

代码语言:javascript复制
sudo ip netns exec docker0 nc -lp 80

在宿主机上执行telnet,可以连接到docker0的80端口:

代码语言:javascript复制
telnet 172.18.0.2 80

场景三:从“容器”内访问外网

  • 配置内核参数,允许IP forwarding
代码语言:javascript复制
sudo sysctl net.ipv4.conf.all.forwarding=1

  • 配置iptables FORWARD规则

首先确认iptables FORWARD的缺省策略:

代码语言:javascript复制
sudo iptables -L

如果缺省策略是DROP,需要设置为ACCEPT

代码语言:javascript复制
sudo iptables -P FORWARD ACCEPT

缺省策略的含义是,在数据包没有匹配到规则时执行的缺省动作。

  • 将bridge设置为“容器”的缺省网关
代码语言:javascript复制
sudo ip netns exec docker0 route add default gw 172.18.0.1 veth0
sudo ip netns exec docker1 route add default gw 172.18.0.1 veth2

查看“容器”内的路由表:

代码语言:javascript复制
$sudo ip netns exec docker0  route -n

Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.18.0.1      0.0.0.0         UG    0      0        0 veth0
172.18.0.0      0.0.0.0         255.255.255.0   U     0      0        0 veth0

可以看出,“容器”内的缺省Gatewaybridge的IP地址,非172.18.0.0/24网段的数据包会路由给bridge

  • 配置iptables的SNAT规则

容器的IP地址外部并不认识,如果它要访问外网,需要在数据包离开前将源地址替换为宿主机的IP,这样外部主机才能用宿主机的IP作为目的地址发回响应。

另外一个需要注意的问题,内核netfilter会追踪记录连接,我们在增加了SNAT规则时,系统会自动增加一个隐式的反向规则,这样返回的包会自动将宿主机的IP替换为容器IP。

代码语言:javascript复制
sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/24 ! -o br0 -j MASQUERADE

上面的命令的含义是:在nat表的POSTROUTING链增加规则,当数据包的源地址为172.18.0.0/24网段,出口设备不是br0时,就执行MASQUERADE动作。

MASQUERADE也是一种源地址转换动作,它会动态选择宿主机的一个IP做源地址转换,而SNAT动作必须在命令中指定固定的IP地址。

  • 从“容器”内访问外部地址
代码语言:javascript复制
sudo ip netns exec docker0 ping -c 3 123.125.115.110
sudo ip netns exec docker1 ping -c 3 123.125.115.110

我们确认在“容器”内是可以访问外部网络的。

场景四:从外部访问“容器”内暴露的服务

  • 配置iptables的DNAT规则

当外部通过宿主机的IP和端口访问容器内启动的服务时,在数据包进入PREROUTING阶段就要进行目的地址转换,将宿主机IP转换为容器IP。同样,系统会为我们自动增加一个隐式的反向规则,数据包在离开宿主机时自动做反向转换。

代码语言:javascript复制
sudo iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.18.0.2:80

上面命令的含义是:在nat表的PREROUTING链增加规则,当输入设备不是br0,目的端口为80时,做目的地址转换,将宿主机IP替换为容器IP。

  • 从远程访问“容器”内暴露的服务

在“容器”docker0内启动服务:

代码语言:javascript复制
sudo ip netns exec docker0 nc -lp 80

在和宿主机同一个局域网的远程主机访问宿主机IP:80

代码语言:javascript复制
telnet 192.168.31.183 80

确认可以访问到容器内启动的服务。

测试环境恢复

删除虚拟网络设备

代码语言:javascript复制
sudo ip link set br0 down
sudo brctl delbr br0
sudo ip link  del veth1
sudo ip link  del veth3

iptablersNamesapce的配置在机器重启后被清除。

总结

本文我们在介绍了vethLinux bridgeiptables等概念后,亲自动手模拟出了docker bridge网络模型,并测试了几种场景的网络互通。实际上docker network 就是使用了上述技术,帮我们创建和维护网络。通过动手实验,相信你对docker bridge网络理解的更加深入。

下一篇我将动手实验容器如何利用Overlay 网络进行跨主机通信。

0 人点赞