1. Network Namespace

本文主要介绍 Network namespace ,隔离网络资源。

首先查看一下你的网络设备,我们统一使用 iproute2 工具包

[root@local10 ~]# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:c2:71:fe brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:35:a2:93 brd ff:ff:ff:ff:ff:ff

这里第一个设备叫 lo ,也叫回环设备,用于本地通信。enp0s3enp0s8 是本机的2块网卡。

现在我们新建一个网络空间

[root@local10 ~]# ip netns list
[root@local10 ~]# ip netns add netns0
[root@local10 ~]# ip netns list
netns0
  • netns 是 net namespace 的简写。
  • add 表示新建,后面跟 namespace 的名字,示例中名称为 netns0。
  • 创建成功后,可以通过 list 查看存在的 net namespace。

如果创建了空间却不能进去,那不是毫无意义吗?让我们进去看看。这个时候我们需要使用 nsenter 命令。从名字上就能看出,这是 namespace enter 的简写。nsenter 除了可以进入不同的网络空间外,还可以进入其他一些空间。我们这次只说网络空间。

[root@local10 ~]# nsenter --net=/var/run/netns/netns0 bash
[root@local10 ~]# ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
[root@local10 ~]# ping 223.5.5.5
connect: Network is unreachable
[root@local10 ~]# exit
exit
  • 显然,--net 参数是为了指定网络空间的入口。在 linux 下,每个网络空间都有一个类似于文件的入口。通过 ip netns add 创建的入口都在 /var/run/netns 这个目录下。
  • 最后面的参数是 bash,意思是进新的空间并运行 bash 程序。这个时候我们再查看一下网络设备

新创建的 netns 只有一个 lo 设备了。如果你试着访问外网,你会发现网络不通

netns0 是一个完全独立的网络空间,它跟空间外的网络栈的关系是隔离的。

你需要给它「安装」网卡、配置IP地址、设置路由,才能在这个空间中访问外网。那要怎么做呢?我们需要虚拟网卡 virtual Ethernet devices (veth)。

提示:

除了 nsenter 命令外,ip 命令也可以进入网络空间,如下命令是等价的

[root@local10 ~]# nsenter --net=/var/run/netns/netns0  hostname -I
172.18.0.10
[root@local10 ~]# ip netns exec netns0 hostname -I
172.18.0.10

1.1. 虚拟网卡

物理网卡很好理解,就是电脑上插网线的设备。虚拟网卡要怎么理解呢?喜欢刨根问底的同学可以查询man veth。简单来说,一个veth就好比两块已经通过网线连接到一起的网卡,只要你把它的一端「插」到电脑A,另一端「插」到B,A跟B就能通信。

我们可以创建一个veth设备,将它的一头「 插」到根空间(默认的),另一头插到netns0就能实现两者的通信。lets go。

[root@local10 ~]# ip link add veth0 type veth peer name ceth0
[root@local10 ~]# ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:c2:71:fe brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:35:a2:93 brd ff:ff:ff:ff:ff:ff
4: ceth0@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether e6:cd:d8:6d:00:88 brd ff:ff:ff:ff:ff:ff
5: veth0@ceth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 8a:53:23:7a:d8:e0 brd ff:ff:ff:ff:ff:ff

这里用到了 link add 命令,veth0 是 veth 设备一端的名字;类型不用说,通过 type 指定为 veth;另一端的名字则是通过 peer name 指定,这里是 ceth0。名字可以随便取,没什么关系。ip link list看下新建的 veth 设备信息。

大家看到了没有,一下子多出来 ceth0@veth0 和 veth0@ceth0 两个设备(当然了,都是虚拟的)。它们是一根绳上的蚂蚱。我们随便选一个,将其插到 netns0 这个空间。

[root@local10 ~]# ip link set ceth0 netns netns0
[root@local10 ~]# ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:c2:71:fe brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:35:a2:93 brd ff:ff:ff:ff:ff:ff
5: veth0@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 8a:53:23:7a:d8:e0 brd ff:ff:ff:ff:ff:ff link-netnsid 0

注意,引用的时候不要写@后面的内容。现在我们看一下根空间的网络设备

根空间只剩下 veth0 了,注意最后的 link-netns netns0,说明它的另一端连着 netns0 这个空间。我们再到 netns0 空间内看一下

[root@local10 ~]# nsenter --net=/var/run/netns/netns0 bash
[root@local10 ~]# ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: ceth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether e6:cd:d8:6d:00:88 brd ff:ff:ff:ff:ff:ff link-netnsid 0

看到 ceth0 了吧,它的后面是 link-netnsid 0,指的是根空间。

接下来我们就可以给 veth0/ceth0 设置 IP 地址了。为了方便,我不再重复列出 nsenter 命令。

外层主机的 veth0 配置

[root@local10 ~]# ip link set veth0 up
[root@local10 ~]# ip addr add 172.18.0.11/16 dev veth0
[root@local10 ~]# ip addre show veth0
5: veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 8a:53:23:7a:d8:e0 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.11/16 scope global veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::8853:23ff:fe7a:d8e0/64 scope link
       valid_lft forever preferred_lft forever

进入 netns0 的 命名空间

[root@local10 ~]# nsenter --net=/var/run/netns/netns0 bash
[root@local10 ~]# ip a
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
4: ceth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether e6:cd:d8:6d:00:88 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root@local10 ~]# ip link set ceth0 up
[root@local10 ~]# ip addr add 172.18.0.10/16 dev ceth0
[root@local10 ~]# ip addr show ceth0
4: ceth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether e6:cd:d8:6d:00:88 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.10/16 scope global ceth0
       valid_lft forever preferred_lft forever
    inet6 fe80::e4cd:d8ff:fe6d:88/64 scope link
       valid_lft forever preferred_lft forever

因为默认都是关闭的,我们首先需要使用 ip link set up 来开启设备。然后使用 ip addr add 设置对应的 IP 地址。

然后,就可以在 nsnet0 空间内访问外面的根空间了。

[root@local10 ~]# hostname -I
172.18.0.10
[root@local10 ~]# ping -c 1 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms

--- 172.18.0.11 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.038/0.038/0.038/0.000 ms

好了,到现在我们有了一个 netns0 空间,而且将它和跟空间连到了一起。

我们平时用 docker 可能需同时启多个实例,这就需要创建多个空间。我们现在就再建一个 netns1。这次我把所有的命令都写到一起

# ip netns add netns1
# ip link add veth1 type veth peer name ceth1
# ip link set ceth1 netns netns1
# ip link set veth1 up
# ip addr add 172.18.0.21/16 dev veth1 

# nsenter --net=/var/run/netns/netns1 bash
# ip link set ceth1 up
# ip addr add 172.18.0.20/16 dev ceth1

好了,让我们看看能不能 ping 通 netns1 的网卡。

# ping 172.18.0.20
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.
^C
--- 172.18.0.20 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2029ms

显然失败了。让我们看一下现在的网络拓扑

现在根空间有两个网卡,veth0 和 veth1,都属于 172.18.0.0/16 网段。当我们尝试访问 172.18.0.20(也就是 ceth1),内核不知道要使用 veth0 还是 veth1 发送。如果我们在根空间查看路由表,我们会发现两条关于 172.18.0.0/16 网段的路由:

# 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

内核会使用第一条,也就是把本应该发给 ceth1 的包发给了 ceth0,自然得不到回复。

但这两条路由是怎么来的呢?答案是内核自己猜的。因为我们首先给 veth0 设了172.18.0.11/16这个IP地址,所以内核认为所有172.18.0.0/16网段的数据都应该通过 veth0 发送。一般来说这是正确的。但后来我们又加了 veth1,并且设置了172.18.0.21/16这个地址。内核又学了一遍,添加了一条路由。

很明显,我们给两个网卡设了同一网段的不同地址,导致网络没法互通。最简单的办法是一个空间用一个网段,但看起来又不那么优雅(一般所有的 docker 实例共用一个网段)。

那怎么办呢?这个时候我们要请出虚拟网桥了。

1.2. 虚拟网桥

怎么理解网桥呢?可以根前面的veth类比一下。veth可以看成一网线连接的两个网卡。网桥可以看成是可以连接多条网线的网卡。因为可以连接多条网线,网桥会随时把任意一条网线收到的数据转发给其他所有网线上去。网桥自己也可能会发送数据,这些数据也会发送到所有网线上去。

好了,先创建一个网桥。

[root@local10 ~]# ip link add br0 type bridge
[root@local10 ~]# ip link set br0 up
[root@local10 ~]# ip link show br0
6: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether 42:61:1d:23:86:17 brd ff:ff:ff:ff:ff:ff

同样使用ip link add,这次的类型是bridge。

接下来,我们需要删除 veth0 和 veth1 上的 IP 地址(不然内核的路由表会影响通信)。

# ip addr del 172.18.0.11/16 dev veth0
# ip addr del 172.18.0.21/16 dev veth1

最后,我们把veth0和veth0连到网桥上。

# ip link set veth0 master br0
# ip link set veth1 master br0

现在的网络拓扑变成了这样:

现在两个容器就可以相互通信了。

[root@local10 ~]# ip netns exec netns0 ping -c 1 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.055 ms

--- 172.18.0.20 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.055/0.055/0.055/0.000 ms
[root@local10 ~]# ip netns exec netns1 ping -c 1 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.044 ms

--- 172.18.0.10 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.044/0.044/0.044/0.000 ms

1.3. 虚拟网关

到现在,两个容器之间可以互相通信。但容器跟宿主系统还没能通信。为此,我们可以给网桥br0设置IP地址。还记得前面说可以把虚拟网桥看成一块有多个网口的网卡吗?

[root@local10 ~]# ip addr add 172.18.0.1/16 dev br0
[root@local10 ~]# ip addr show br0
6: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 8a:53:23:7a:d8:e0 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::4061:1dff:fe23:8617/64 scope link
       valid_lft forever preferred_lft forever

宿主机和容器就可以互通了

# ping -c 1 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.083 ms
--- 172.18.0.10 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.083/0.083/0.083/0.000 ms

# ping -c 1 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.091 ms
--- 172.18.0.20 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.091/0.091/0.091/0.000 ms

好了,现在的网络拓扑变成了这样,几个网络空间都可以相互通信。

容器可以访问宿主机了,那能访问公网吗?现在还不行。

ip netns exec netns0 ping -c 1 8.8.8.8
ping: connect: Network is unreachable

我们可以先看一下容器内的路由表

# ip netns exec netns0 ip route
172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.10

只有一条关于172.18.0.0/16网段的路由。在容器内不知道该如何访问8.8.8.8。 显然,我们需要认一条默认路由规则,下一跳只能设成172.18.0.1,这是根空间的 br0。要想访问外网,只能借助根空间。

# ip route add default via 172.18.0.1

现在的路由表变成了这样

# ip netns exec netns0 ip route
default via 172.18.0.1 dev veth0
172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.10

但依然没法访问(跟上次不一样,这样是没有回包)。

ip netns exec netns0 ping -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

因为设了默认路由。当我们在容器内访问8.8.8.8时,系统会把数据包转发给172.18.0.1,也就是br0。因为br0在根空间,所以br0收到的包需要根空间处理。可是,根空间也不知道如何访问8.8.8.8,它唯一能做的是把包转发给自己的默认路由。但是,这种转发行为默认是不开启的。需要手工开启:

# echo 1 > /proc/sys/net/ipv4/ip_forward

这样就完事了吗?还不行,这里涉及到一个地址转换的问题。

从 br0 收到的包的源地址都是172.18.0.0/16网段的,显然这个网段跟宿主机的物理网卡(假设是 eth0,假设其IP地址是192.168.0.100/16)不在一个网段。更关鍵的问题是,如果宿主机直接将来自br0的数据包转发给它的默认路由(假设是192.168.0.1),那么192.168.0.1会收到一个目的地址是8.8.8.8而源地址是172.18.0.10的数据包。192.168.0.1根本不知道192.168.0.100上还有一个小网络。所以192.168.0.1一般会丢弃这个包。

怎么办呢?我们就需要让这个包看起来是从192.168.0.100发出来的。也就是说,我们要把这个数据包的源地址改写成192.168.0.100,这就是所谓的地址转换NAT。幸运的是iptables为我们提供了这样的功能:

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

iptables 比较复杂,它有很多表,第个表可以有不同的规则,用来控制各种网络数据。其中 nat 这张表就是用来控制地址转换规则的。iptables在转发数据包的时候又分好多链(可以简单理解成一个队列)。内核如果确定了一个包要转发给那个目标地址(确定的过程就是路由的过程),就会将这个包放到 POSTROUTING 这条链上。我们刚才的命名就是给这条链上源地址的网段是172.18.0.0/16(通过-s指定)而且目标地址不需要通过br0转发(通过 ! -o br0 指定)的数据包,统一扔到MASQUERADE这条链上。内核会对MASQUERADE上的数据包做地址转换。

好了,现在容器可以访问外网了。

# ip netns exec netns0 ping -c 1 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=120 time=0.355 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.355/0.355/0.355/0.000 ms

这里的外网笼统的概念就是其他主机,容器的跨主机通信就是建立该基础之上的。也可以使用网络方案例如 flannel

1.4. 总结

如果你装了 docker,你会发现系统会多一个 docker0 的设备,就是我们前面提到的 br0。

如果你用 docker 起了某个服务,docker 就会创建对应的网格空间。但是,你用ip netns list是看不到的。为什么呢?因为 docker 创建的网络空间的入口都在/var/run/docker/netns/,估计是故意不让iproute2看到,免得大家玩坏。

那怎么看 docker 创建的名称空间呢?答案是使用lsns,一看名字就知道是 list namespace 的缩写。linux支持很多空间,我们这次只看网络相关的

# lsns -t net
        NS TYPE NPROCS   PID USER     NETNSID NSFS                           COMMAND
4026531992 net     130     1 root  unassigned                                /sbin/init
4026532556 net       1  5156 11211          2 /run/docker/netns/a4ef8472617c memcached

看到了吧,第一个就是我们的根空间,由 systemd 创建。

第二个就是 docker 创建的一个空间。那怎么进去呢?看到第七列的路径了吧。使用nsenter --net=/run/docker/netns/a4ef8472617c就可以进入这个网络空间了。

好了,今天就说这么多吧。其实 docker 还支持其他类型的网络。这些就靠大家自己去探索吧。

1.5. docker 中与宿主机通信

如果你装了 docker,你会发现容器间ping不通,那是因为 docker 会设一条防火墙规则,只允许 docker0 这个网桥的设备相互通信。

-P FORWARD DROP
-A FORWARD -i docker0 -o docker0 -j ACCEPT

最简单的处理办法是允许所有网桥转发 ```

2. iptables -P FORWARD ACCEPT

````

2.1. 参考

https://zhuanlan.zhihu.com/p/346440595

Copyright © 温玉 2021 | 浙ICP备2020032454号 all right reserved,powered by Gitbook该文件修订时间: 2023-06-19 08:59:50

results matching ""

    No results matching ""