怎么从传统的Linux网络视角理解容器网络?《二》

使用虚拟网络switch(网桥)连接容器

容器化思想的驱动力是高效的资源共享。所以,一台机器上只运行一个容器并不常见。相反,最终目标是尽可能地在共享的环境上运行更多的隔离进程。因此,如果按照上述veth方案,在同一台主机上放置多个容器的话会发生什么呢?让我们尝试添加第二个容器。

# 从 root 命名空间   
    $ 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无法连通root 命名空间!   
    $ ping -c 2 172.18.0.21   
    PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.   
    From 172.18.0.20 icmp_seq=1 Destination Host Unreachable   
    From 172.18.0.20 icmp_seq=2 Destination Host Unreachable   
    --- 172.18.0.21 ping statistics ---   
    2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 55ms pipe 2   
    # 但是路由是存在的!   
    $ ip route   
    172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20   
    # 离开 `netns1`   
    $ exit    
    # 从 root 命名空间无法连通`netns1`   
    $ ping -c 2 172.18.0.20   
    PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.   
    From 172.18.0.11 icmp_seq=1 Destination Host Unreachable   
    From 172.18.0.11 icmp_seq=2 Destination Host Unreachable   
  
--- 172.18.0.20 ping statistics ---   
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 23ms pipe 2   
    # 从netns0可以连通 `veth1`   
    $ sudo nsenter --net=/var/run/netns/netns0   
    $ ping -c 2 172.18.0.21   
    PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.   
    64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms   
    64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms   
    --- 172.18.0.21 ping statistics ---   
    2 packets transmitted, 2 received, 0% packet loss, time 33ms   
    rtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms   
    # 但是仍然无法连通netns1   
    $ ping -c 2 172.18.0.20   
    PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.   
    From 172.18.0.10 icmp_seq=1 Destination Host Unreachable   
    From 172.18.0.10 icmp_seq=2 Destination Host Unreachable   
    --- 172.18.0.20 ping statistics ---   
    2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 63ms pipe 2

晕!有地方出错了……netns1有问题。它无法连接到root,并且从root命名空间里也无法访问到它。但是,因为两个容器都在相同的IP网段172.18.0.0/16里,从netns0容器可以访问到主机的veth1。

这里花了些时间来找到原因,不过很明显遇到的是路由问题。先查一下root命名空间的路由表:

$ ip route   
    # ... 忽略无关行... #   
    172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11   
    172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21

在添加了第二个veth对之后,root的网络栈知道了新路由172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21,但是之前已经存在该网络的路由了。当第二个容器尝试ping veth1时,选中的是第一个路由规则,这导致网络无法连通。如果我们删除第一个路由sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11,然后重新检查连通性,应该就没有问题了。netns1可以连通,但是netns0就不行了。


如果我们为netns1选择其他的网段,应该就都可以连通。但是,多个容器在同一个IP网段上应该是合理的使用场景。因此,我们需要调整veth方案。

别忘了还有Linux网桥——另一种虚拟化网络技术!Linux网桥作用类似于网络switch。它会在连接到其上的接口间转发网络包。并且因为它是switch,它是在L2层完成这些转发的。

试试这个工具。但是首先,需要清除已有设置,因为之前的一些配置现在不再需要了。删除网络命名空间:

$ sudo ip netns delete netns0   
$ sudo ip netns delete netns1   
$ sudo ip link delete veth0   
$ sudo ip link delete ceth0   
$ sudo ip link delete veth1   
$ sudo ip link delete ceth1

快速重建两个容器。注意,我们没有给新的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   
default via 10.0.2.2 dev eth0 proto dhcp metric 100   
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100

最后创建网桥接口:

$ 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


检查容器间的连通性:

$ sudo nsenter --net=/var/run/netns/netns0   
$ ping -c 2 172.18.0.20   
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.   
64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.259 ms   
64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.051 ms   
--- 172.18.0.20 ping statistics ---   
2 packets transmitted, 2 received, 0% packet loss, time 2ms   
rtt min/avg/max/mdev = 0.051/0.155/0.259/0.104 ms
$ sudo nsenter --net=/var/run/netns/netns1   
$ ping -c 2 172.18.0.10   
PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.   
64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.037 ms   
64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.089 ms   
--- 172.18.0.10 ping statistics ---   
2 packets transmitted, 2 received, 0% packet loss, time 36ms   
rtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms

太好了!工作得很好。用这种新方案,我们根本不需要配置veth0和veth1。只需要在ceth0和ceth1端点分配两个IP地址。但是因为它们都连接在相同的Ethernet上(记住,它们连接到虚拟switch上),之间在L2层是连通的:

$ sudo nsenter --net=/var/run/netns/netns0   
$ ip neigh   
172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE   
$ exit   
  
$ sudo nsenter --net=/var/run/netns/netns1   
$ ip neigh   
172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE   
$ exit

太好了,我们学习了如何将容器变成友邻,让它们互不干扰,但是又可以连通。

连接外部世界(IP路由和地址伪装)

容器间可以通信。但是它们能和主机,比如root命名空间,通信吗?

$ sudo nsenter --net=/var/run/netns/netns0   
$ ping 10.0.2.15 # eth0 address   
connect: Network is unreachable

这里很明显,netns0没有路由:

$ ip route   
172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

root命名空间不能和容器通信:

# 首先使用 exit 离开netns0:   
    $ ping -c 2 172.18.0.10   
    PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.   
    From 213.51.1.123 icmp_seq=1 Destination Net Unreachable   
    From 213.51.1.123 icmp_seq=2 Destination Net Unreachable   
    --- 172.18.0.10 ping statistics ---   
    2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms   
  
$ ping -c 2 172.18.0.20   
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.   
From 213.51.1.123 icmp_seq=1 Destination Net Unreachable   
From 213.51.1.123 icmp_seq=2 Destination Net Unreachable   
--- 172.18.0.20 ping statistics ---   
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms

要建立root和容器命名空间的连通性,我们需要给网桥网络接口分配IP地址:

$ sudo ip addr add 172.18.0.1/16 dev br0

一旦给网桥网络接口分配了IP地址,在主机的路由表里就会多一条路由:

$ ip route   
    # ...忽略无关行 ...   
    172.18.0.0/16 dev br0 proto kernel scope link src 172.18.0.1   
  
$ ping -c 2 172.18.0.10   
PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.   
64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.036 ms   
64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.049 ms   
  
--- 172.18.0.10 ping statistics ---   
2 packets transmitted, 2 received, 0% packet loss, time 11ms   
rtt min/avg/max/mdev = 0.036/0.042/0.049/0.009 ms   
  
$ ping -c 2 172.18.0.20   
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.   
64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.059 ms   
64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.056 ms   
  
--- 172.18.0.20 ping statistics ---   
2 packets transmitted, 2 received, 0% packet loss, time 4ms   
rtt min/avg/max/mdev = 0.056/0.057/0.059/0.007 ms

容器可能也可以ping网桥接口,但是它们还是无法连接到主机的eth0。需要为容器添加默认的路由:

$ sudo nsenter --net=/var/run/netns/netns0   
$ ip route add default via 172.18.0.1   
$ ping -c 2 10.0.2.15   
PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.   
64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.036 ms   
64 bytes from 10.0.2.15: icmp_seq=2 ttl=64 time=0.053 ms   
--- 10.0.2.15 ping statistics ---   
2 packets transmitted, 2 received, 0% packet loss, time 14ms   
rtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms   
    # 为\`netns1\`也做上述配置

这个改动基本上把主机变成了路由,并且网桥接口变成了容器间的默认网关。


很好,我们将容器连接到root命名空间上。现在,继续尝试将它们连接到外部世界。Linux上默认disable了网络包转发(比如,路由功能)。我们需要先启用这个功能:

# 在 root 命名空间   
    sudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

再次检查连通性:

$ sudo nsenter --net=/var/run/netns/netns0   
$ ping 8.8.8.8   
    # hung住了...

还是不工作。哪里弄错了呢?如果容器可以向外部发包,那么目标服务器无法将包发回容器,因为容器的IP地址是私有的,那个特定IP的路由规则只有本地网络知道。并且有很多容器共享的是完全相同的私有IP地址172.18.0.10。这个问题的解决方法称为网络地址翻译(NAT)。

在到达外部网络之前,容器发出的包会将源IP地址替换为主机的外部网络地址。主机还会跟踪所有已有的映射,会在将包转发回容器之前恢复之前被替换的IP地址。听上去很复杂,但是有一个好消息!iptables模块让我们只需要一条命令就可以完成这一切:

$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE

命令非常简单。在nat表里添加了一条POSTROUTING chain的新路由,会替换伪装所有源于172.18.0.0/16网络的包,但是不通过网桥接口。

检查连通性:

$ sudo nsenter --net=/var/run/netns/netns0   
$ ping -c 2 8.8.8.8 PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.   
64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms   
64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms   
--- 8.8.8.8 ping statistics ---   
2 packets transmitted, 2 received, 0% packet loss, time 2ms   
rtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms

要知道这里我们用的默认策略——允许所有流量,这在真实的环境里是非常危险的。主机的默认iptables策略是ACCEPT:

sudo iptables -S   
-P INPUT ACCEPT   
-P FORWARD ACCEPT   
-P OUTPUT ACCEPT

Docker默认限制所有流量,随后仅仅为已知的路径启用路由。

如下是在CentOS 8机器上,单个容器暴露了端口5005时,由Docker daemon生成的规则:

$ sudo iptables -t filter --list-rules   
-P INPUT ACCEPT   
-P FORWARD DROP   
-P OUTPUT ACCEPT   
-N DOCKER   
-N DOCKER-ISOLATION-STAGE-1   
-N DOCKER-ISOLATION-STAGE-2   
-N DOCKER-USER   
-A FORWARD -j DOCKER-USER   
-A FORWARD -j DOCKER-ISOLATION-STAGE-1   
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT  
-A FORWARD -o docker0 -j DOCKER   
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT   
-A FORWARD -i docker0 -o docker0 -j ACCEPT   
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5000 -j ACCEPT   
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2   
-A DOCKER-ISOLATION-STAGE-1 -j RETURN   
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP   
-A DOCKER-ISOLATION-STAGE-2 -j RETURN   
-A DOCKER-USER -j RETURN   
  
$ sudo iptables -t nat --list-rules   
-P PREROUTING ACCEPT   
-P INPUT ACCEPT   
-P POSTROUTING ACCEPT   
-P OUTPUT ACCEPT   
-N DOCKER   
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER   
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE   
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5000 -j MASQUERADE  
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER   
-A DOCKER -i docker0 -j RETURN   
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5005 -j DNAT --to-destination 172.17.0.2:5000   
  
$ sudo iptables -t mangle --list-rules   
-P PREROUTING ACCEPT   
-P INPUT ACCEPT   
-P FORWARD ACCEPT   
-P OUTPUT ACCEPT  
 -P POSTROUTING ACCEPT   
  
$ sudo iptables -t raw --list-rules   
-P PREROUTING ACCEPT   
-P OUTPUT ACCEPT
上一篇:js之检查有效的URL


下一篇:种草推荐Zookeeper客户端