我们一定听过容器的基础原理,namespace做隔离,Cgroups做限制,rootfs做文件系统,容器本质上是linux的一个进程,那么为什么大多数场景下,容器不直接使用宿主机上的网络,而要是通过network namespace隔离出一组专属的网络空间呢?(容器的基础原理,可参考:https://coolshell.cn/articles/17010.html)
原因很简单,直接使用宿主机的网络,会带来资源冲突的问题,比如:容器与宿主机的端口冲突。大多数情况下,我们都希望容器能够使用自己network namespace里的网络栈,及拥有自己的IP地址加端口。
那么,此时,在宿主机上的容器网络就面临着需要解决以下几个问题:
好在,linux操作系统了提供了一些列工具,可以帮助我们完美的解决这些问题。接下来,我们在linux宿主机上使用系统自带的工具,来运行一个仅仅包括网络栈的容器进行验证。包括以下几个组件:
linux完成网络通信所需的网络栈包括了:网卡(Network interface),回环设备(Loopback Device),路由表(Routing Table)和iptables。对于一个进程来说,这些要素,构成了其网络通信的基础环境。
我们可以通过以下命令查看自己的,网络栈:
$ ifconfig
$ ip route
$ iptables --list-rules
为了便于对比root和我们即将要创建的容器network namespace,我们将现有iptables添加一条规则,以便于区分宿主机和容器的运行环境。此时,通过执行iptables --list-rules可以看到多了一条-N ROOT_NS.
现在,我们来创建一个network namaspace netns0,可以通过linux 的 ip 工具来实现。
$ sudo ip netns add netns0
那么,怎么使用刚刚我们创建的network namespace netns0呢? linux提供了相应的工具nsenter,顾名思义,nsenter提供了进入其他ns执行给定命令的权限。我们进入容器netns0的命名空间。然后查看基础网络栈信息。
$ sudo nsenter --net=/var/run/netns/netns0
$ ifconfig -a
$ ip route
$ iptables --list-rules
可以看到,我们在netns0 namespace只能看到lo设备,不能看到eth0,并且iptables中,是看不到-N ROOT_NS,由此可知,网络已经与root进行了隔离。
下图,总结了容器network ns和宿主机 ns的区别。
我们已经创建了容器专属的network namespace netns0,此时的netns0是无法和主机进行通信的。幸运的是,linux提供了veth-pair来帮助我们。Veth Pair的特点是:它被创建出来后,总是以两张虚拟网卡(Veth Peer)的形式成对出现的。并且,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,哪怕这两个“网卡”在不同的NS里。
这使得Veth Pair常常被用作连接不同Network Namespace的“网线”。
通过以下命令,我们可以创建一对相互连接的veth pair:veth0和ceth0.
$ sudo ip link add veth0 type veth peer name ceth0
$ ip link
当veth0和ceth0创建出来后,全部归属在宿主机 network namespace。如果需要netns0和宿主机network namespace互通,需要将veth pair的一端绑定到netns0上,一端继续绑定在宿主机network namespace上。可通过如下命令实现:
$ sudo ip link set ceth0 netns netns0
一旦我们启动 “veth pair”,从一端“网卡”发出去的包会立刻出现在相对应的另一端”网卡“上,我们从宿主机namespace开始,设置root端IP地址为172.18.0.11/16。
$ sudo ip link set veth0 up
$ sudo ip addr add 172.18.0.11/16 dev veth0
然后,继续配置netns0. 启动lo设备
$ sudo nsenter --net=/var/run/netns/netns0
$ ip link set lo up
$ ip addr add 172.18.0.10/16 dev ceth0
$ ip link
我们来检查一下网络的联通性。在netns0内部,ping宿主机ip:172.18.0.11
退出netns0,在宿主机 ns内ping netns0 ip : 172.18.0.10
此时,我们在netns0中,访问任何其他的网络均不可达,比如,ping宿主机ip:10.0.0.15
问题很简单,因为我们没有访问其他地址的路由表,可通过查看netns0路由表来获知。
目前为止,我们已经知道了如何进行隔离、虚拟化以及连通linux的网络栈。
现在,我们再次新建一个”容器“,来解决容器间的通信问题。
# From root namespace
$ sudo ip netns add netns1
$ sudo ip link add veth1 type veth peer name ceth1
$ sudo ip link set ceth1 netns netns1
$ sudo ip link set veth1 up
$ sudo ip addr add 172.18.0.21/16 dev veth1
$ sudo nsenter --net=/var/run/netns/netns1
$ ip link set lo up
$ ip link set ceth1 up
$ ip addr add 172.18.0.20/16 dev ceth1
此时,校验新建容器netns1的连通性,发现在nsnet1内部,无法ping 通宿主机网络IP:172.18.0.21. 在宿主机network namespace内,也无法ping通nsnet1的网络。并且检查nsnet1路由表,发现路由正确。
推出netns1,我们再次核查root 的路由表,可清晰的发现问题:原来是路有冲突,有2条匹配172.18.0.0/16网段的路由,并且第一条的路由优先级高于第二条。所以宿主机ns和netns1 ns无法连通。
那怎么解决该问题呢?如果选择另一个网段,那自然不会有任何冲突,但实际中,多个容器处于同一网段司空见惯,必须要解决其网络通信问题。好在,linux提供了网桥(bridge)帮我们解决该问题。
网桥:在linux中能够起到虚拟交换机的作用,工作在L2(数据链路层),主要功能是根据MAC地址学习来将数据包转发到网桥的不同端口上。
从前面的章节中可以得知,网桥:类似于交换机,实现L2层通信,Veth: 类似于网线,实现了点到点的通信。那么,怎么将容器连在交换机(网桥)上呢,实际中,当然要靠网线(veth pair),我们通过实验来验证。
我们先将前面的实验环境清空:
sudo ip netns delete netns0
sudo ip netns delete netns1
然后,我们快速创建2个容器,与之前的区别是,我们不为veth0和veth1分配任何IP地址。
$ sudo ip netns add netns0
$ sudo ip link add veth0 type veth peer name ceth0
$ sudo ip link set veth0 up
$ sudo ip link set ceth0 netns netns0
$ sudo nsenter --net=/var/run/netns/netns0
$ ip link set lo up
$ ip link set ceth0 up
$ ip addr add 172.18.0.10/16 dev ceth0
$ exit
$ sudo ip netns add netns1
$ sudo ip link add veth1 type veth peer name ceth1
$ sudo ip link set veth1 up
$ sudo ip link set ceth1 netns netns1
$ sudo nsenter --net=/var/run/netns/netns1
$ ip link set lo up
$ ip link set ceth1 up
$ ip addr add 172.18.0.20/16 dev ceth1
$ exit
检查一下,确保宿主机上没有新增路由。
$ ip route
最后,开始创建并启动网桥br0。
$ sudo ip link add br0 type bridge
$ sudo ip link set br0 up
现在,将veth0和veth1”连“在网桥上。
$ sudo ip link set veth0 master br0
$ sudo ip link set veth1 master br0
再来检查一下两个容器间网络的连通性。可以发现,此时,两个容器间的网络已经可以互通。为什么不需要配置ip地址,只是”连“在交换机(网桥)上,两个容器也可以互通了呢?
回顾一下前面的知识,网桥类似一个交换机,是通过MAC地址来将数据包发送到不同的接口上。
通过前面的实验,我们已经解决了容器间通过网桥bridge和veth pair进行互通的问题,此时,我们试着在容器内访问宿主机网络,会发现网络不可达。
原因很简单,查看容器netns0路由表可发现我们并没有前往宿主机网段10.0.0.0网段的路由。
同理,宿主机ns也无法和容器ns进行通信。
为了在宿主机和容器ns间实现连接,我们需要为网桥分配IP地址,作为容器netns0和netns1的网关。
$ ip addr add 172.18.0.1/16 dev br0
可以发现,完成添加IP地址后,IP route会自动出现新路由条目。
进入nsnet0和nsnet1添加默认路由做bridge网关后,再次进行root和容器ns间联通性测试。
此时,root和容器ns,以及容器之间已完成互通。
我们知道,内网和外网是无法直接互通的,因为容器在内网,外网并不知道容器的回程路由,只能知道信息来自宿主机的公网IP。所以,容器要访问外网,就需要进行NAT转换。好在,我们可以通过linux的iptables来实现NAT的转换。
linux操作系统,默认情况下,禁止了IP转发功能,可以通过如下命令打开。
$ sudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
然后,执行iptables指令,增加一条NAT转发规则。
$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE
此时,进入容器内部,ping公网IP进行测试,发现容器已可访问公网。
我们在容器启动一个简单的python3 http服务。
$ sudo nsenter --net=/var/run/netns/netns0
$ python3 -m http.server --bind 172.18.0.10 80
如果我们从主机的宿主机 ns发送http请求到netns0,会发现该应用服务正常工作。
如果我们从外部访问该http服务,我们用哪个IP地址呢?私有IP地址我们不能用,那么能用的只有宿主机的公网IP地址了。此时,我们可以测试一下访问宿主机IP地址时http服务是否正常。发现并不可达。
要解决这个问题,我们需要将容器的端口发布到宿主机的eth0网卡上,可以通过iptables工具来实现。
# External traffic
$ sudo iptables -t nat -A PREROUTING -d 10.0.0.15 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.18.0.10:80
# Local traffic (since it doesn't pass the PREROUTING chain)
$ sudo iptables -t nat -A OUTPUT -d 10.0.0.15 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.18.0.10:80
然后,启动iptables intercepting traffic over bridged networks功能
sudo modprobe br_netfilter
再次进行测试。
我们通过一系列实验,对单机容器网络的基础原理进行了初步的探索,我们来回顾一下。
本章主要介绍的是容器bridge网络模式,实际上,容器的网络不是远远不止一种。容器还可以直接使用宿主机的网络,也可以使用overlay技术,具体可参考容器网络模式的介绍:https://docs.docker.com/network/
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。