Network Namespace
Namespace 技术是容器虚拟化的重要技术,可以将进程隔离在自己的 Namespace 中。而 Network Namespace 技术是将进程隔离在自己的网络空间中,拥有独立的网络栈,仿佛自己处于独立的网络中。
网络栈是指网卡、回环设备、路由表和 iptables 规则等等。网络栈是指网卡、回环设备、路由表和 iptables 规则等等。
容器间网络互通
容器的 bridge 网络模式会创建属于自己的 Network Namespace 来隔离自己与宿主机,拥有属于自己的网络栈。可以理解为不同的 Network Namespace 是不同的主机,那么它们想要网络通信,该如何实现?
首先想到的当然是交换机和路由器了,因为它们是处于同一个网络中,所以使用交换机即可,而 Linux 提供了 bridge
来充当虚拟交换机的角色,将多个 Network Namespace 接入到 bridge 中,它们就能网络互通了。
那么如何将容器的 Network Namespace 接入到 bridge 呢?
veth pair
veth pair
是成对出现的虚拟网卡,可以把它看做成一个管道,或者一根网线,从一段输入的数据包会从另一端输出,可以通过它来连接容器的 Network Namespace 和 bridge。
使用 veth pair bridge 的网络模型如下:
实践
我们不用安装 docker,直接用 Network Namespace 来模拟容器的网络环境。
- 创建 bridge 设备
这里使用 ip 命令来对 bridge 进行操作,也可以使用 brctl
命令,该命令是在 bridge-utils
包中。
[root@localhost ~]# ip link add br0 type bridge
[root@localhost ~]# ip link set dev br0 up
[root@localhost ~]# ip addr add 10.0.0.3/24 dev br0
[root@localhost ~]# 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: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether fe:fc:fe:af:4b:ea brd ff:ff:ff:ff:ff:ff
60: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 7e:b5:12:2d:fd:d4 brd ff:ff:ff:ff:ff:ff
- 创建 3 个 Network Namespace
使用 ip netns 命令可以对 Network Namesapce 进行操作,所以使用该命令创建 net0、net1 两个 Network Namespace
代码语言:javascript复制[root@localhost ~]# ip netns add net0
[root@localhost ~]# ip netns add net1
- 创建两对 veth pair
首先创建两对 veth pair 虚拟网卡,它们是一一对应的,veth0 和 veth1,veth2 和 veth3
代码语言:javascript复制[root@localhost ~]# ip link add veth0 type veth peer name veth1
[root@localhost ~]# ip link add veth2 type veth peer name veth3
查看创建出来的 4 个虚拟网卡,@前面的是该网卡的名称,后面的是该网卡的另一端。
代码语言:javascript复制[root@localhost ~]# ip a
...
67: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 7e:b5:12:2d:fd:d4 brd ff:ff:ff:ff:ff:ff
68: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 26:22:ef:1a:b3:33 brd ff:ff:ff:ff:ff:ff
69: veth3@veth2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether c6:f2:d1:2d:c7:95 brd ff:ff:ff:ff:ff:ff
70: veth2@veth3: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether ee:4b:0d:17:60:b1 brd ff:ff:ff:ff:ff:ff
- 将 veth pair 的一端接入到 Network Namespace 中,并设置好其 IP 地址
[root@localhost ~]# ip link set dev veth0 netns net0
[root@localhost ~]# ip netns exec net0 ip addr add 10.0.0.1/24 dev veth0
[root@localhost ~]# ip netns exec net0 ip link set dev veth0 up
[root@localhost ~]# ip link set dev veth2 netns net1
[root@localhost ~]# ip netns exec net1 ip addr add 10.0.0.2/24 dev veth2
[root@localhost ~]# ip netns exec net1 ip link set dev veth2 up
ip netns exec 命令可以进入到指定的 Network Namespace 中执行命令
- 将 veth pair 的另一端设置到 bridge 中
[root@localhost ~]# ip link set dev veth1 master br0
[root@localhost ~]# ip link set dev veth3 master br0
[root@localhost ~]# ip link set dev veth1 up
[root@localhost ~]# ip link set dev veth3 up
可以通过命令 bridge link
查看到 veth1、veth3都插入到 br0 中了。
[root@localhost ~]# bridge link
67: veth1 state UP @(null): <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 2
69: veth3 state UP @(null): <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding priority 32 cost 2
也可以通过 brctl 命令查看
代码语言:javascript复制[root@localhost ~]# brctl show
[root@localhost ~]# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.7eb5122dfdd4 no veth1
veth3
测试
- 首先监听 br0 网卡
[root@localhost ~]# tcpdump -i br0
- 在 net0 中 ping net1 的 ip
ip netns exec net0 ping -c 3 10.0.0.2
- 会发现监听 br0 网卡是有流量的:
[root@localhost ~]# tcpdump -i br0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:39:41.251428 ARP, Request who-has 10.0.0.2 tell 10.0.0.1, length 28
02:39:41.251495 ARP, Reply 10.0.0.2 is-at ee:4b:0d:17:60:b1 (oui Unknown), length 28
02:39:41.251502 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 15665, seq 1, length 64
02:39:41.251702 IP 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 15665, seq 1, length 64
02:39:42.251435 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 15665, seq 2, length 64
02:39:42.251554 IP 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 15665, seq 2, length 64
02:39:43.251414 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 15665, seq 3, length 64
02:39:43.251512 IP 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 15665, seq 3, length 64
02:39:46.261377 ARP, Request who-has 10.0.0.1 tell 10.0.0.2, length 28
02:39:46.261400 ARP, Reply 10.0.0.1 is-at 26:22:ef:1a:b3:33 (oui Unknown), length 28
可以看到先是 10.0.0.1 发送的 ARP 获取 10.0.0.2 的 MAC 地址,然后再发送 ICMP 的请求和响应,最后 10.0.0.2 也发送 ARP 获取 10.0.0.1 的 MAC 地址。
因为 bridge 是二层网络设备,所以它是需要通过识别 MAC 地址来进行通信的局域网。
后面我又通过 net0 ping net1 的 ip,发现它没有发送 ARP 了,这个是因为第一次 bridge 已经将 MAC 地址和端口的映射关系已经记录了下来,后面可以直接通过查表,而不用再发送 ARP 了。
代码语言:javascript复制[root@localhost ~]# tcpdump -i br0 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:40:01.267689 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 15991, seq 1, length 64
02:40:01.267948 IP 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 15991, seq 1, length 64
02:40:02.267509 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 15991, seq 2, length 64
02:40:02.267641 IP 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 15991, seq 2, length 64
02:40:03.267458 IP 10.0.0.1 > 10.0.0.2: ICMP echo request, id 15991, seq 3, length 64
02:40:03.267582 IP 10.0.0.2 > 10.0.0.1: ICMP echo reply, id 15991, seq 3, length 64
宿主机访问容器
- 再次监听 br0 网卡
tcpdump -i br0
- 在宿主机上 ping 容器内部 ip,是可以 ping 通的
ping 10.0.0.1 -n 3
- 查看监听的 br0 网卡流量:
[root@localhost ~]# tcpdump -i br0 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:56:03.232293 ARP, Request who-has 10.0.0.1 tell 10.0.0.3, length 28
02:56:03.232342 ARP, Reply 10.0.0.1 is-at 26:22:ef:1a:b3:33, length 28
02:56:03.232379 IP 10.0.0.3 > 10.0.0.1: ICMP echo request, id 3964, seq 1, length 64
02:56:03.232402 IP 10.0.0.1 > 10.0.0.3: ICMP echo reply, id 3964, seq 1, length 64
02:56:04.232494 IP 10.0.0.3 > 10.0.0.1: ICMP echo request, id 3964, seq 2, length 64
02:56:04.232554 IP 10.0.0.1 > 10.0.0.3: ICMP echo reply, id 3964, seq 2, length 64
02:56:05.232517 IP 10.0.0.3 > 10.0.0.1: ICMP echo request, id 3964, seq 3, length 64
02:56:05.232607 IP 10.0.0.1 > 10.0.0.3: ICMP echo reply, id 3964, seq 3, length 64
02:56:08.245354 ARP, Request who-has 10.0.0.3 tell 10.0.0.1, length 28
02:56:08.245412 ARP, Reply 10.0.0.3 is-at 7e:b5:12:2d:fd:d4, length 28
可以看到是 10.0.0.3 访问通了 10.0.0.1 ,我们是通过宿主机访问的容器内部,为什么源 ip 变成 10.0.0.3?
- 通过查看宿主机的路由表,可以看到当我们 ping 10.0.0.1 时,是匹配上了该条路由,然后走 br0 网卡,使用的源 ip 是 br0 的 ip 10.0.0.3
[root@localhost ~]# ip r
...
10.0.0.0/24 dev br0 proto kernel scope link src 10.0.0.3
...
容器内访问外部
- 配置 Network Namespace 中将 bridge 作为默认网关
当我们在 net0 内部访问外部 ip 时,会发现网络不可达。
代码语言:javascript复制[root@localhost ~]# ip netns exec net0 ping 10.65.132.187
connect: Network is unreachable
这个是因为没有匹配上路由表上的原因
代码语言:javascript复制[root@localhost ~]# ip netns exec net0 ip r
10.0.0.0/24 dev veth0 proto kernel scope link src 10.0.0.1
我们加上默认网关,也就是在没有匹配上其他路由时,走该网关
代码语言:javascript复制[root@localhost ~]# ip netns exec net0 ip r add default via 10.0.0.3 dev veth0
[root@localhost ~]# ip netns exec net0 ip r
default via 10.0.0.3 dev veth0
10.0.0.0/24 dev veth0 proto kernel scope link src 10.0.0.1
- 配置宿主机 iptables 的 SNAT 规则
我们再次在容器中访问外部 ip 时,发现网络还不可达,但是我们监听 br0 网卡发现是有数据包的,也就是路由配的没问题
代码语言:javascript复制[root@localhost ~]# tcpdump -i br0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
03:23:23.738010 IP 10.0.0.1 > 10.65.132.187: ICMP echo request, id 11228, seq 1, length 64
03:23:24.737415 IP 10.0.0.1 > 10.65.132.187: ICMP echo request, id 11228, seq 2, length 64
我们再看宿主机上的路由,是有配置默认路由的。
代码语言:javascript复制[root@localhost ~]# ip r
default via 10.61.74.1 dev ens18 proto static metric 100
所以我再监听一下宿主机的网卡 ens18 看看:
代码语言:javascript复制[root@localhost ~]# tcpdump -i ens18 dst host 10.65.132.187
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens18, link-type EN10MB (Ethernet), capture size 262144 bytes
03:24:47.162152 IP 10.0.0.1 > 10.65.132.187: ICMP echo request, id 13281, seq 1, length 64
03:24:48.161585 IP 10.0.0.1 > 10.65.132.187: ICMP echo request, id 13281, seq 2, length 64
发现也是有数据包的,但是原地址是 net0 的 ip 地址 10.0.0.1,外部是不认识的,所以需要配置 iptables,将源 IP 改为宿主机的 IP。
代码语言:javascript复制# 创建规则
[root@localhost ~]# iptables -t nat -A POSTROUTING -s 10.0.0.0/24 ! -o br0 -j MASQUERADE
# 查看规则
[root@localhost ~]# iptables -L POSTROUTING -t nat
...
MASQUERADE all -- 10.0.0.0/24 anywhere
上面设置 ipatbels 的命令的含义是,在 POSTROUTING 链的 nat 表中添加一条规则,当数据包的源 IP 网段为 10.0.0.0/24 时,并且不是网卡 br0 发送的,就执行 MASQUERADE 动作,该动作就是源地址改为宿主机地址的转换动作。
现在就可以 ping 通外部 IP 了,查看 ens18 网卡的数据包,源地址已经修改成宿主机网卡 ens18 的地址了。
代码语言:javascript复制[root@localhost ~]# tcpdump -i ens18 dst host 10.65.132.187 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens18, link-type EN10MB (Ethernet), capture size 262144 bytes
03:41:51.157483 IP 10.61.74.37 > 10.65.132.187: ICMP echo request, id 3972, seq 1, length 64
03:41:52.158483 IP 10.61.74.37 > 10.65.132.187: ICMP echo request, id 3972, seq 2, length 64
那外部 IP 回包时,只有宿主机的 IP,并没有容器的 IP,那宿主机怎么知道发送容器中呢?
这个是因为内核 netfilter
会追踪记录连接,我们在增加了 SNAT 规则时,系统会自动增加一个隐式的反向规则,这样返回的包会自动将宿主机的 IP 替换为容器 IP。
外部访问容器暴露的服务
docker 容器可以通过-p 的方式将容器内部的端口暴露到宿主机中,给外部访问,这个是怎么实现的呢?
- 这个是通过 iptables 的 DNAT 规则实现的
# 创建规则
[root@localhost ~]# iptables -t nat -A PREROUTING ! -i br0 -p tcp -m tcp --dport 80 -j DNAT --to-destination 10.0.0.1:80
# 查看规则
[root@localhost ~]# iptables -L PREROUTING -t nat
DNAT tcp -- anywhere anywhere tcp dpt:http to:10.0.0.1:80
创建规则的命令的意思是,在 PREROUTING 链的 nat 表中添加一条规则,数据包的来源网卡不是 br0,数据包协议是 tcp,目的端口是 80,则进行 DNAT,将目的 IP 从宿主机 IP 改为容器 IP。
- 在 net0 中开启 80 端口
[root@localhost ~]# nc -lp 80
- 在外部主机上访问宿主机的 80 端口,是可以访问成功的
[root@k8s-master-07rf9 ~]# telnet 10.61.74.37 80