手动实现docker容器bridge网络模型
# 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
2
3
4
5
6
7
8
9
10
11
- 创建 3 个 Network Namespace
使用 ip netns 命令可以对 Network Namesapce 进行操作,所以使用该命令创建 net0、net1 两个 Network Namespace
[root@localhost ~]# ip netns add net0
[root@localhost ~]# ip netns add net1
2
- 创建两对 veth pair
首先创建两对 veth pair 虚拟网卡,它们是一一对应的,veth0 和 veth1,veth2 和 veth3
[root@localhost ~]# ip link add veth0 type veth peer name veth1
[root@localhost ~]# ip link add veth2 type veth peer name veth3
2
查看创建出来的 4 个虚拟网卡,@前面的是该网卡的名称,后面的是该网卡的另一端。
[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
2
3
4
5
6
7
8
9
10
- 将 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
2
3
4
5
6
7
8
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
2
3
4
5
可以通过命令 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
2
3
也可以通过 brctl 命令查看
[root@localhost ~]# brctl show
[root@localhost ~]# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.7eb5122dfdd4 no veth1
veth3
2
3
4
5
# 测试
- 首先监听 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
2
3
4
5
6
7
8
9
10
11
12
13
可以看到先是 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 了。
[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
2
3
4
5
6
7
8
9
# 宿主机访问容器
- 再次监听 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
2
3
4
5
6
7
8
9
10
11
12
13
可以看到是 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
...
2
3
4
# 容器内访问外部
- 配置 Network Namespace 中将 bridge 作为默认网关
当我们在 net0 内部访问外部 ip 时,会发现网络不可达。
[root@localhost ~]# ip netns exec net0 ping 10.65.132.187
connect: Network is unreachable
2
这个是因为没有匹配上路由表上的原因
[root@localhost ~]# ip netns exec net0 ip r
10.0.0.0/24 dev veth0 proto kernel scope link src 10.0.0.1
2
我们加上默认网关,也就是在没有匹配上其他路由时,走该网关
[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
2
3
4
5
- 配置宿主机 iptables 的 SNAT 规则
我们再次在容器中访问外部 ip 时,发现网络还不可达,但是我们监听 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
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
2
3
4
5
我们再看宿主机上的路由,是有配置默认路由的。
[root@localhost ~]# ip r
default via 10.61.74.1 dev ens18 proto static metric 100
2
所以我再监听一下宿主机的网卡 ens18 看看:
[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
2
3
4
5
发现也是有数据包的,但是原地址是 net0 的 ip 地址 10.0.0.1,外部是不认识的,所以需要配置 iptables,将源 IP 改为宿主机的 IP。
# 创建规则
[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
2
3
4
5
6
7
上面设置 ipatbels 的命令的含义是,在 POSTROUTING 链的 nat 表中添加一条规则,当数据包的源 IP 网段为 10.0.0.0/24 时,并且不是网卡 br0 发送的,就执行 MASQUERADE 动作,该动作就是源地址改为宿主机地址的转换动作。
现在就可以 ping 通外部 IP 了,查看 ens18 网卡的数据包,源地址已经修改成宿主机网卡 ens18 的地址了。
[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
2
3
4
5
那外部 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
2
3
4
5
6
创建规则的命令的意思是,在 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