前言
在之前的文章中,我们介绍了Docker容器隔离机制namespace,资源管理工具CGroup以及Docker使用的文件系统unionFS。我们知道了Container其实就是被Linux内核用”沙箱”隔离起来的一组进程。
但是光靠这些还不够,linux新创建的network namespace
初始状态下是不带任何网络功能的,这篇文章就会为大家解析容器是如何实现与外界的网络交互的。
虚拟网络设备
Linux 网络数据流向
计算机与外界网络通讯往往要依赖网卡,网卡和物理网络线路相连,将电信号/光信号转化为数字信号。然后内存对信号进行解析,交由上层的协议栈,最终交付给用户层。
而Linux内核在将网络包交付给协议栈之前,有一个网络设备层
层。其中的网络设备可以是真实的(比如默认网卡eth0)或者虚拟的,对内核来说,他们之间没有区别。
这里我们不去讨论具体虚拟网络设备是如何实现的,大家只需要建立一个概念,就是我们可以在内核中添加虚拟网络设备来创建一个内核中的”虚拟网络”。就像局域网一样大家都可以有自己不同的ip地址、mac地址。
而发送数据包的流程和接受类似,希望能更深入了解的可以阅读附录中的文章。
network namespace的协议栈
对于一个network netspace
来说,他拥有自己独立的协议栈,以及独立的网络设备挂载表。
接下来,我们会介绍如何使用veth和bridge设备让子network namespace可以和其他子空间或外部的网络进行交互。
veth
使用veth连接netns
veth(virtual ethernet)设备具有以下特点:
- 成对出现
- 一端连接协议栈,另一端peer之间对接
一对veth可以连接不同的协议栈,即当veth挂载到不同的network namespace
或者container
时,就变成了这样:
如何使用veth
首先利用ip
命令创建两个netns:
$ ip netns add ns1
$ ip netns add ns2
创建一对veth:
$ ip link add veth1 type veth peer name veth2
启动这两个veth,分配ip并分别设置到新创建的两个netns
中:
$ ip link set dev veth1 up
$ ip link set veth1 netns ns1
$ ip netns exec ns1 ifconfig veth1 192.168.1.1/24 up
$ ip link set dev veth2 up
$ ip link set veth2 netns ns2
$ ip netns exec ns2 ifconfig veth2 192.168.1.2/24 up
设置完成后,我们就可以在netns中测试网络可达:
$ ip netns exec ns2 ping -c 1 192.168.1.1 -I veth2
PING 192.168.1.1 (192.168.1.1) from 192.168.1.2 veth2: 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.057 ms
--- 192.168.1.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.057/0.057/0.057/0.000 ms
我们在ns2中的veth2上进行抓包:
$ ip netns exec ns2 tcpdump -nn -i veth2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth2, link-type EN10MB (Ethernet), capture size 262144 bytes
11:09:11.150688 IP 192.168.1.1 > 192.168.1.2: ICMP echo request, id 12550, seq 1, length 64
11:09:11.150771 IP 192.168.1.2 > 192.168.1.1: ICMP echo reply, id 12550, seq 1, length 64
11:09:12.679528 IP6 fe80::80de:ccff:fe40:dfe4 > ff02::2: ICMP6, router solicitation, length 16
ns1发出的ICMP包转发到了veth2上,至此ns1和ns2之间建立了网络通道。
bridge
在上面的例子中,虽然ns1和ns2通过一对veth连接了起来,但是这时只做到了2个namespace之间的访问。如果我们有多个namespace,那所有需要访问的namespace之间都需要一对veth,很明显这不是一种聪明的做法。
bridge介绍
bridge作为一个虚拟网络设备,也有自己的ip地址、mac地址。而和普通的网络设备不同,bridge的数据出口有多端,会根据mac地址进行转发,原理和交换机类似。
我们可以对上面的veth进行些许改造,在netns之外创建一个bridge,而netns则通过veth对接入到bridge之上:
使用bridge连接veth-pair
使用brctl
创建一个bridge设备并分配ip:
$ brctl addbr br0
$ ip link set dev br0 up
$ ifconfig br0 192.168.1.11/24 up
创建veth-pair,将ns1通过veth-pair连接到br0上:
$ ip link add veth1 type veth peer name br-veth1 # 创建veth-pair
$ brctl addif br0 br-veth1 # 将br-veth1端连接到br0上
$ ip link set dev br-veth1 up # 启动br-veth1
$ ip link set veth1 netns ns1 # 将veth1设置到ns1中
$ ip netns exec ns1 ip link set dev veth1 up # 启动veth1
$ ip netns exec ns1 ifconfig veth1 192.168.1.1/24 up # 给veth1分配ip
$ ip netns exec ns1 ip route add default via 192.168.1.11 # 设置ns1默认路由
对ns2做相同的处理。
设置完成后,我们就可以测试ns1和ns2之间网络可达:
$ ip netns exec ns1 ping 192.168.1.2 -c 1
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=0.134 ms
--- 192.168.1.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.134/0.134/0.134/0.000 ms
在br0进行抓包:
$ tcpdump -nn -i br0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
12:38:50.845461 IP 192.168.1.1 > 192.168.1.2: ICMP echo request, id 13124, seq 1, length 64
12:38:50.845549 IP 192.168.1.2 > 192.168.1.1: ICMP echo reply, id 13124, seq 1, length 64
在ns2中抓包:
$ ip netns exec ns2 tcpdump -nn -i veth2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth2, link-type EN10MB (Ethernet), capture size 262144 bytes
12:39:41.973639 IP 192.168.1.1 > 192.168.1.2: ICMP echo request, id 13128, seq 1, length 64
12:39:41.973680 IP 192.168.1.2 > 192.168.1.1: ICMP echo reply, id 13128, seq 1, length 64
可以看到实际上包是由br0转到ns中的veth2的。
再尝试ping一个外网的ip,不过在这之前我们需要先修改下iptables的规则,否则收到的私有地址包默认会被丢弃:
$ iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j MASQUERADE
$ iptables -t nat -D POSTROUTING -s 192.168.1.0/24 -j MASQUERADE # 还原
然后我们尝试ping一下baidu的ip:
$ ip netns exec ns1 ping 220.181.38.251 -c 1
PING 220.181.38.251 (220.181.38.251) 56(84) bytes of data.
64 bytes from 220.181.38.251: icmp_seq=1 ttl=52 time=5.23 ms
--- 220.181.38.251 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
成功ping通,现在nets内、外网的通讯都建立了。
清理实验环境
$ ip netns delete ns1 ns2 # 删除ns1和ns2后,创建的veth-pair会自动删除
$ ifconfig br0 down # 停止br0
$ brctl delbr br0 # 删除br0
结束语
本文解析了如何利用虚拟网络设备让容器具有网络交互的能力,结合之前的文档,我们的docker实现原理基础就告一段落了。接下来会从0开始一步一步的实现容器,逐步为它加上隔离、挂载、网络通讯等功能。
敬请期待吧~