简介

network namespace是linux内核提供的一个功能,是用来实现网络虚拟化的重要功能,可以创建多个相互隔离的网络空间,它们有独自的网络栈信息。不管是虚拟机还是容器,运行的时候仿佛自己就在独立的网络中。Docker的网络隔离实现便用到了该技术。
主要是实现方法是使用ip命令,由于需要修改系统的网络配置,因此需要使用root身份。
关于network namespace的命令都是使用ip netns子命令,可以输入ip netns help来获取命令帮助信息。

root@VM-102-49-ubuntu:/home/ubuntu# 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

ip netns list可以使用ip netns ls来代替,刚开始显然是什么都没有,命令什么也不会输出。
这时候尝试创建一个network namespace,名字就叫net0

root@VM-102-49-ubuntu:/home/ubuntu# ip netns add net0
root@VM-102-49-ubuntu:/home/ubuntu# ip netns ls
net0

这时候就创建成功了一个network namespace。这时候这个net0会被创建在/var/run/netns/中,可以进入到该目录中验证一下。如果想要一并去管理不是用netns创建的network namespace,比如容器或者虚拟机创建的,那么只要在这里创建一个指向文件的链接即可。
创建出来之后,那么就相当于拥有了一个属于自己的网络空间,会有自己独立的网卡、路由表、ARP 表、iptables 等和网络相关的资源,当你想查看该空间的信息的时候,可以使用ip netns exec命令进入空间去执行相应的命令,相应的命令放到后面。例如,想查看这个network namespace的所有虚拟网卡信息:

root@VM-102-49-ubuntu:/home/ubuntu# ip netns exec net0 ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

可以看到,里边只有一个默认的lo虚拟网卡,处于DOWN状态,相当于是在net0这个network namespace里边执行的ip addr这个命令。
默认情况下,network namespace 是不能和主机网络,或者其他 network namespace 通信的。但是这样创建出来的network namespace就毫无用武之地了,所以得用ip link的子命令来进行连接,使得可以进行通信,模拟一个网络集群。现在我们来实现一下下面两种典型情况。

实验1.


这个是最简单的,直接将两个network namespace连接起来。这时候需要一根网线来连接,这根网线就是采用的veth pair技术。这根网线是双向的,而且当数据包来到某一端的时候,会无条件的传输到另一端。需要注意的是veth pair只能是成对存在的,只要创建就会成对的创建出来,一旦删除其中一端,那么另一端也会跟着消失。

我们先来创建一对veth pair:

root@VM-102-49-ubuntu:/home/ubuntu# ip link add type veth

然后用ip link show命令来查看所有的虚拟设备信息,会看到最后两个分别是veth0和veth1,即我们刚刚创建出来的两个veth pair,当然也可以用ip link add name1 type veth peer name name2来创建出两个指定名字为name1和name2的veth pair。接下来开始进行环境的搭建。先创建另一个network namespace:

root@VM-102-49-ubuntu:/home/ubuntu# ip netns add net1
root@VM-102-49-ubuntu:/home/ubuntu# ip netns ls
net1
net0

然后将其中一端veth0挂到net0上,配置上本地ip地址并且启动它:

root@VM-102-49-ubuntu:/home/ubuntu# ip link set veth0 netns net0
root@VM-102-49-ubuntu:/home/ubuntu# ip netns exec net0 ip link set veth0 up
root@VM-102-49-ubuntu:/home/ubuntu# ip netns exec net0 ip addr add 10.0.1.1/24 dev veth0

再这中间可以不断用ip netns exec net0 ip addr来观察变化,加深理解。同理,将veth1也这样挂到net1上,配置上本地ip地址并且启动它。
这时候虚拟网络集群就搭建起来了,我们来ping一下验证:

root@VM-102-49-ubuntu:/home/ubuntu# ip netns exec net0 ping -c 3 10.0.1.2
PING 10.0.1.2 (10.0.1.2) 56(84) bytes of data.
64 bytes from 10.0.1.2: icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from 10.0.1.2: icmp_seq=2 ttl=64 time=0.051 ms
64 bytes from 10.0.1.2: icmp_seq=3 ttl=64 time=0.050 ms

--- 10.0.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.050/0.060/0.079/0.013 ms

很显然,ping通了,可以再用net1去ping net0试一下,结果也是一样。因此搭建成功。

实验2.


上一个是仅仅两个虚拟网络的情况下,那么如果多了起来,再去两两进行配对就明显不太可行了。所以引入了网桥bridge这个技术。将网络全都与bridge连通,那么所有的两两之间的网络就都可以通信了。

首先创建两个network namespace:

ip netns add net0
ip netns add net1

然后创建网桥且开启它:

ip link add br0 type bridge
ip link set dev br0 up

然后创建第一对veth pair,像实验1一样,将veth pair的一端挂在net0上,并且给配上ip地址启动它:

ip link add type veth
ip link set dev veth0 netns net0
ip netns exec net0 ip link set dev veth0 name eth0
ip netns exec net0 ip addr add 10.0.1.1/24 dev eth0
ip netns exec net0 ip link set dev eth0 up

再将另一端挂在bridge上:

ip link set dev veth1 master br0
ip link set dev veth1 up

这时候net0与bridge便可以通信了。同理,将net1与bridge进行同样的操作,使两者可以通信:

ip link set dev veth0 netns net1
ip netns exec net1 ip link set dev veth0 name eth0
ip netns exec net1 ip addr add 10.0.1.2/24 dev eth0
ip netns exec net1 ip link set dev eth0 up
ip link set dev veth2 master br0
ip link set dev veth2 up

这时候,这个环境就基本搭建完成了。但是别忘了去ping一下试试:

root@VM-102-49-ubuntu:/home/ubuntu# ip netns exec net0 ping -c 3 10.0.1.2
PING 10.0.1.2 (10.0.1.2) 56(84) bytes of data.

--- 10.0.1.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2007ms

咦?没有ping通??
当我做到这一步的时候几乎崩溃了,查看各种环境变量都没有问题。用tcpdump去抓包也没有发现问题,再往下深究就牵扯到linux内核和网络协议栈的实现了,这块基础并不是很好。。后来去论坛求救,终于有一个大牛给出了原因:

原因是因为系统为bridge开启了iptables功能,导致所有经过br0的数据包都要受iptables里面规则的限制,而docker为了安全性,将iptables里面filter表的FORWARD链的默认策略设置成了drop,于是所有不符合docker规则的数据包都不会被forward,导致你这种情况ping不通。
解决办法有两个,二选一:
1. 关闭系统bridge的iptables功能,这样数据包转发就不受iptables影响了:echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables
2. 为br0添加一条iptables规则,让经过br0的包能被forward:iptables -A FORWARD -i br0 -j ACCEPT
第一种方法不确定会不会影响docker,建议用第二种方法。
大概分析过程:1.先跑一下你的命令,检查相应的arp缓存和br0里面端口和mac地址的对应关系,发现没有错误。2.注意到br0不转发icmp报文,但转发arp报文,说明二层通信没有问题,有可能是因为什么原因将icmp报文过滤了。3.由于br0工作在第二层,所以开始根本没有往iptables方面想,一直在网上搜索br0不转发数据包的原因,直到发现有些文章提到有可能是iptables的原因。4.搜索bridge跟iptables相关的配置,知道了有相应的选项控制bridge的数据包是否会调用iptables的规则进行过滤。5.查看iptables的规则,发现经过docker配置后的iptables默认会丢弃掉forward的数据包。

然后按照大牛说的添加了一条iptables规则就好了。并且跪着求大牛给出了分析过程,。。
最后再去ping一下验证一下:

root@VM-102-49-ubuntu:~# ip netns exec net1 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.078 ms
64 bytes from 10.0.1.1: icmp_seq=2 ttl=64 time=0.081 ms
64 bytes from 10.0.1.1: icmp_seq=3 ttl=64 time=0.078 ms

--- 10.0.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.078/0.079/0.081/0.001 ms

终于可以了!于是大功告成。
这时候研究过docker网络模式的可能会发现,实验2的拓扑结构跟bridge网络模式基本是一样的。当然要实现每个 namespace 对外网的访问还需要额外的配置(设置默认网关,开启 ip_forward,为网络添加 NAT 规则等)。