为了更好的理解Docker网络,对相关网络技术基础,做一个简单总结,内容来自《Linux虚拟网络技术》、《Linux中的虚拟网络》和《Kubernetes权威指南:从Docker到Kubernetes实践全接触(第4版)》。理解本节内容对于后面理解docker网络和k8s网络都有不少帮助
Network Namespace 是 Linux 内核提供的功能,是实现网络虚拟化的重要功能,它能创建多个隔离的网络空间,它们有独自网络栈信息。不管是虚拟机还是容器,运行的时候仿佛自己都在独立的网络中。而且不同Network Namespace的资源相互不可见,彼此之间无法通信。如下图所示
可以借助ip netns命令来完成对 Network Namespace 的各种操作。ip netns命令来自于iproute2安装包,一般系统会默认安装
注意:ip netns命令修改网络配置时需要 sudo 权限。
可以通过ip netns命令完成对Network Namespace 的相关操作,可以通过ip netns help查看命令帮助信息:
dd@ubuntu04:~$ ip netns help
Usage: ip netns list
ip netns add NAME
ip netns set NAME NETNSID
ip [-all] netns delete [NAME]
ip netns identify [PID]
ip netns pids NAME
ip [-all] netns exec [NAME] cmd ...
ip netns monitor
ip netns list-id
默认情况下,Linux系统中是没有任何 Network Namespace的,所以ip netns list命令不会返回任何信息。下面,我们通过命令创建一个名为ns0的命名空间:
dd@ubuntu04:~$ ip netns add ns0
dd@ubuntu04:~$ ip netns list
ns0
新创建的 Network Namespace 会出现在/var/run/netns/目录下。如果相同名字的 namespace 已经存在,命令会报Cannot create namespace file “/var/run/netns/ns0”: File exists的错误。
对于每个 Network Namespace 来说,它会有自己独立的网卡、路由表、ARP 表、iptables 等和网络相关的资源。
ip命令提供了ip netns exec子命令可以在对应的 Network Namespace 中执行命令。
查看新创建 Network Namespace 的网卡信息
dd@ubuntu04:~$ sudo ip netns exec ns0 ip addr
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
可以看到,新创建的Network Namespace中会默认创建一个lo回环网卡,此时网卡处于关闭状态。此时,尝试去 ping 该lo回环网卡,会提示Network is unreachable
dd@ubuntu04:~$ sudo ip netns exec ns0 ping 127.0.0.1
connect: Network is unreachable
通过下面的命令启用lo回环网卡:
dd@ubuntu04:~$ sudo ip netns exec ns0 ip link set lo up
然后再次尝试去 ping 该lo回环网卡:
dd@ubuntu04:~$ sudo ip netns exec ns0 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
dd@ubuntu04:~$ sudo ip netns exec ns0 ping -c 3 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.106 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.033 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.024 ms
--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2042ms
rtt min/avg/max/mdev = 0.024/0.054/0.106/0.037 ms
我们可以在不同的 Network Namespace 之间转移设备(如veth)。由于一个设备只能属于一个 Network Namespace ,所以转移后在这个 Network Namespace 内就看不到这个设备了。
在设备里面有一个重要的属性:NETIF_F_ETNS_LOCAL,如果这个属性为on,就不能被转移到其他命名空间中。Veth设备属于可以转移的设备,而很多其他设备如lo设备、vxlan设备、ppp设备、bridge设备等都是不可以转移的。将无法转移的设备移动到别的命名空间时,会得到无效参数的错误提示。
dd@ubuntu04:~$ ip link set br0 netns ns0
RTNETLINK answers: Invalid argument
如何知道这些设备是否可以转移呢?可以使用ethtool工具查看:
dd@ubuntu04:~$ ethtool -k br0
netns-local: on [fixed]
netns-local的值是on,说明不可以转移,否则可以转移。
veth pair 全称是 Virtual Ethernet Pair,是一个成对的端口,所有从这对端口一 端进入的数据包都将从另一端出来,反之也是一样。
引入veth pair是为了在不同的 Network Namespace 直接进行通信,利用它可以直接将两个 Network Namespace 连接起来。
整个veth的实现非常简单,有兴趣的读者可以参考源代码drivers/net/veth.c的实现。
dd@ubuntu04:~$ sudo ip link add type veth
dd@ubuntu04:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:d0:84:b3 brd ff:ff:ff:ff:ff:ff
inet 192.168.31.204/24 brd 192.168.31.255 scope global ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fed0:84b3/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:da:1e:df:ff 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:daff:fe1e:dfff/64 scope link
valid_lft forever preferred_lft forever
61: vethdf550a9@if60: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 9a:47:ef:86:86:ba brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::9847:efff:fe86:86ba/64 scope link
valid_lft forever preferred_lft forever
64: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 4e:84:ed:8a:6f:14 brd ff:ff:ff:ff:ff:ff
65: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 66:69:e6:13:88:45 brd ff:ff:ff:ff:ff:ff
可以看到,此时系统中新增了一对veth pair,将veth0和veth1两个虚拟网卡连接了起来,此时这对 veth pair 处于”未启用“状态。
如果我们想指定 veth pair 两个端点的名称,可以使用下面的命令:
dd@ubuntu04:~$ ip link add vethfoo type veth peer name vethbar
dd@ubuntu04:~$ sudo ip link add vethfoo type veth peer name vethbar
dd@ubuntu04:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:d0:84:b3 brd ff:ff:ff:ff:ff:ff
inet 192.168.31.204/24 brd 192.168.31.255 scope global ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fed0:84b3/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:da:1e:df:ff 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:daff:fe1e:dfff/64 scope link
valid_lft forever preferred_lft forever
61: vethdf550a9@if60: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 9a:47:ef:86:86:ba brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::9847:efff:fe86:86ba/64 scope link
valid_lft forever preferred_lft forever
66: vethbar@vethfoo: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 76:17:63:68:fc:06 brd ff:ff:ff:ff:ff:ff
67: vethfoo@vethbar: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether b2:2d:ed:43:a5:78 brd ff:ff:ff:ff:ff:ff
下面我们利用veth pair实现两个不同的 Network Namespace 之间的通信。刚才我们已经创建了一个名为ns0的 Network Namespace,下面再创建一个信息Network Namespace,命名为ns1
dd@ubuntu04:~$ sudo ip netns add ns1
dd@ubuntu04:~$ ip netns list
ns1
ns0
然后我们将veth0加入到ns0,将veth1加入到ns1,如下所示:
dd@ubuntu04:~$ sudo ip link set veth0 netns ns0
dd@ubuntu04:~$ sudo ip link set veth1 netns ns1
然后我们分别为这对veth pair配置上ip地址,并启用它们:
dd@ubuntu04:~$ sudo ip netns exec ns0 ip link set veth0 up
dd@ubuntu04:~$ sudo ip netns exec ns0 ip addr add 10.0.1.1/24 dev veth0
dd@ubuntu04:~$ sudo ip netns exec ns1 ip link set veth1 up
dd@ubuntu04:~$ sudo ip netns exec ns1 ip addr add 10.0.1.2/24 dev veth1
查看这对veth pair的状态
dd@ubuntu04:~$ sudo ip netns exec ns0 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noqueue state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
64: veth0@if65: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 4e:84:ed:8a:6f:14 brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet 10.0.1.1/24 scope global veth0
valid_lft forever preferred_lft forever
inet6 fe80::4c84:edff:fe8a:6f14/64 scope link
valid_lft forever preferred_lft forever
dd@ubuntu04:~$ sudo ip netns exec ns1 ip addr
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
65: veth1@if64: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 66:69:e6:13:88:45 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.1.2/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::6469:e6ff:fe13:8845/64 scope link
valid_lft forever preferred_lft forever
从上面可以看出,我们已经成功启用了这个veth pair,并为每个veth设备分配了对应的ip地址。我们尝试在ns1中访问ns0中的ip地址:
dd@ubuntu04:~$ sudo ip netns exec ns1 ping -c 3 10.0.1.1
PING 10.0.1.1 (10.0.1.1) 56(84) bytes of data.
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.132 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=0.041 ms
64 bytes from 10.0.1.1: icmp_seq=3 ttl=64 time=0.044 ms
--- 10.0.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2051ms
rtt min/avg/max/mdev = 0.041/0.072/0.132/0.042 ms
可以看到,veth pair成功实现了两个不同Network Namespace之间的网络交互。
一旦将veth pair的peer段放入另一个Network Namespace,我们在当前Namespace中就看不到它了。那么,我们怎么才能知道这个veth pair的对端在哪里呢?
可以通过ethtool工具来查看(当Network Namespace很多时,操作会比较麻烦):
dd@ubuntu04:~$ sudo ip netns exec ns1 ethtool -S veth1
NIC statistics:
peer_ifindex: 64
得知另一端的接口设备序列号是64,我们再到另一个命名空间中查看序列号5代表什么设备:
dd@ubuntu04:~$ sudo ip netns exec ns0 ip link | grep 64
64: veth0@if65: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
veth pair打破了 Network Namespace 的限制,实现了不同 Network Namespace 之间的通信。但veth pair有一个明显的缺陷,就是只能实现两个网络接口之间的通信。
如果我们想实现多个网络接口之间的通信,就可以使用下面介绍的网桥(Bridge)技术。
简单来说,网桥就是把一台机器上的若干个网络接口“连接”起来。其结果是,其中一个网口收到的报文会被复制给其他网口并发送出去。以使得网口之间的报文能够互相转发。
网桥对报文的转发基于MAC地址。网桥能够解析收发的报文,读取目标MAC地址的信息,和自己记录的MAC表结合,来决策报文的转发目标网口。
为了实现这些功能,网桥会学习源MAC地址,在转发报文时,网桥只需要向特定的网口进行转发,从而避免不必要的网络交互。
如果它遇到一个自己从未学习到的地址,就无法知道这个报文应该向哪个网口转发,就将报文广播给所有的网口(报文来源的网口除外)。
Linux内核是通过一个虚拟的网桥设备(Net Device)来实现桥接的。这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。如下图所示:
如上图所示,网桥设备 br0 绑定了 eth0 和 eth1。
对于网络协议栈的上层来说,只看得到 br0,上层协议栈需要发送的报文被送到 br0,网桥设备的处理代码判断报文该被转发到 eth0 还是 eth1,或者两者皆转发;反过来,从eth0 或 eth1 接收到的报文被提交给网桥的处理代码,在这里会判断报文应该被转发、丢弃还是提交到协议栈上层。
而有时eth0、eth1 也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收,从而绕过网桥。
和网桥有关的操作可以使用命令 brctl,这个命令来自 bridge-utils 这个包。
dd@ubuntu04:~$ sudo apt-get install bridge-utils
创建网桥
dd@ubuntu04:~$ sudo brctl addbr dd_br0
删除网桥
dd@ubuntu04:~$ sudo brctl delbr dd_br0
绑定网口
建立一个逻辑网段之后,我们还需要为这个网段分配特定的端口。在Linux 中,一个端口实际上就是一个物理或虚拟网卡。而每个网卡的名称则分别为eth0 ,eth1 ,eth2 。我们需要把每个网卡一一和br0 这个网段联系起来,作为br0 中的一个端口。
# 让eth0 成为br0 的一个端口
dd@ubuntu04:~$ sudo brctl addif dd_br0 eth0
# 让eth1 成为br0 的一个端口
dd@ubuntu04:~$ sudo brctl addif dd_br0eth1
# 让eth2 成为br0 的一个端口
dd@ubuntu04:~$ sudo brctl addif dd_br0eth2
iptables是Linux实现的软件防火墙,用户可以通过iptables设置请求准入和拒绝规则,从而保护系统的安全。
我们也可以把iptables理解成一个客户端代理,用户通过iptables这个代理,将用户安全设定执行到对应的安全框架中,这个“安全框架”才是真正的防火墙,这个框架的名字叫netfilter。
iptables其实是一个命令行工具,位于用户空间。
iptables/netfilter(以下简称iptables)组成了Linux平台下的包过滤防火墙,可以完成封包过滤、封包重定向和网络地址转换(NAT)等功能。
iptables不仅要处理本机接收到的消息,也要处理本机发出的消息。这些消息需要经过一系列的”关卡“才能被本机应用层接收,或者从本机发出,每个”关卡“担负着不同的工作。这里的”关卡“被称为”链“。
从上面我们知道,iptables是按照规则来办事的,这些规则就是网络管理员预定义的条件。规则一般的定义为:如果数据包头符合这样的条件,就这样处理“。这些规则并不是严格按照添加顺序排列在一张规则表中,而是按照功能进行分类,存储在不同的表中,每个表存储一类规则:
表和链共同完成了iptables对数据包的处理。但并不是每个链都包含所有类型的表,所以,有些链是天生不具备某些功能的。就像我们去车站乘车的时候,”关卡A“只负责检查身份证,”B关卡”只负责检查行李,而“C关卡”功能比较齐全,即负责检查身份证,又负责检查行李。二者的关系如下图所示:
Linux通过Network Namespace实现了网络的隔离,使网络协议栈之间互不干扰;并通过veth pair和网桥实现了相同主机上多个Network Namespace之间的数据通信;iptables则可以帮助我们实现网络安全和数据包的路由转发功能,从而使主机和主机、容器与容器、容器和宿主机之间可以相互收发消息。在这些技术的共同协作下,才有了现在安全、稳定的虚拟网络。
[1] Linux虚拟网络技术
[2] Linux中的虚拟网络
[3] 《Kubernetes权威指南:从Docker到Kubernetes实践全接触(第4版)》
[4] 详解:Linux网络虚拟化技术
[5] Linux虚拟网络设备之veth
[6] 常见的单机虚拟网络类型