郑文峰的博客 郑文峰的博客
首页
  • python之路
  • go之路
  • 其他
  • redis
  • mysql
  • docker
  • k8s
读书破万卷
周刊
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)

zhengwenfeng

穷则变,变则通,通则久
首页
  • python之路
  • go之路
  • 其他
  • redis
  • mysql
  • docker
  • k8s
读书破万卷
周刊
关于
  • 导航 (opens new window)
  • 代码片段 (opens new window)
  • 收藏
  • 友链
  • 外部页面

    • 开往 (opens new window)
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)
  • docker

    • 容器的本质
    • docker容器
    • 手动实现docker容器bridge网络模型
      • Network Namespace
      • 容器间网络互通
        • veth pair
        • 实践
        • 测试
      • 宿主机访问容器
      • 容器内访问外部
      • 外部访问容器暴露的服务
    • docker容器单机网络
  • k8s

  • 云原生
  • docker
zhengwenfeng
2023-01-08
目录

手动实现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 来模拟容器的网络环境。

  1. 创建 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
1
2
3
4
5
6
7
8
9
10
11
  1. 创建 3 个 Network Namespace

使用 ip netns 命令可以对 Network Namesapce 进行操作,所以使用该命令创建 net0、net1 两个 Network Namespace

[root@localhost ~]# ip netns add net0
[root@localhost ~]# ip netns add net1
1
2
  1. 创建两对 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
1
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
1
2
3
4
5
6
7
8
9
10
  1. 将 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
1
2
3
4
5
6
7
8

ip netns exec 命令可以进入到指定的 Network Namespace 中执行命令

  1. 将 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
1
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
1
2
3

也可以通过 brctl 命令查看

[root@localhost ~]# brctl show
[root@localhost ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.7eb5122dfdd4       no              veth1
                                                        veth3
1
2
3
4
5

# 测试

  1. 首先监听 br0 网卡
[root@localhost ~]# tcpdump -i br0
1
  1. 在 net0 中 ping net1 的 ip
ip netns exec net0 ping -c 3 10.0.0.2
1
  1. 会发现监听 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
1
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
1
2
3
4
5
6
7
8
9

# 宿主机访问容器

  1. 再次监听 br0 网卡
tcpdump -i br0
1
  1. 在宿主机上 ping 容器内部 ip,是可以 ping 通的
ping 10.0.0.1 -n 3
1
  1. 查看监听的 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
1
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?

  1. 通过查看宿主机的路由表,可以看到当我们 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 
...
1
2
3
4

# 容器内访问外部

  1. 配置 Network Namespace 中将 bridge 作为默认网关

当我们在 net0 内部访问外部 ip 时,会发现网络不可达。

[root@localhost ~]# ip netns exec net0 ping 10.65.132.187
connect: Network is unreachable
1
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
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 
1
2
3
4
5
  1. 配置宿主机 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
1
2
3
4
5

我们再看宿主机上的路由,是有配置默认路由的。

[root@localhost ~]# ip r
default via 10.61.74.1 dev ens18 proto static metric 100
1
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
1
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
1
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
1
2
3
4
5

那外部 IP 回包时,只有宿主机的 IP,并没有容器的 IP,那宿主机怎么知道发送容器中呢?

这个是因为内核 netfilter 会追踪记录连接,我们在增加了 SNAT 规则时,系统会自动增加一个隐式的反向规则,这样返回的包会自动将宿主机的 IP 替换为容器 IP。

# 外部访问容器暴露的服务

docker 容器可以通过-p 的方式将容器内部的端口暴露到宿主机中,给外部访问,这个是怎么实现的呢?

  1. 这个是通过 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
1
2
3
4
5
6

创建规则的命令的意思是,在 PREROUTING 链的 nat 表中添加一条规则,数据包的来源网卡不是 br0,数据包协议是 tcp,目的端口是 80,则进行 DNAT,将目的 IP 从宿主机 IP 改为容器 IP。

  1. 在 net0 中开启 80 端口
[root@localhost ~]# nc -lp 80
1
  1. 在外部主机上访问宿主机的 80 端口,是可以访问成功的
[root@k8s-master-07rf9 ~]# telnet 10.61.74.37 80
1
#docker#云原生#容器
上次更新: 2023/01/15, 15:47:48
docker容器
docker容器单机网络

← docker容器 docker容器单机网络→

最近更新
01
django rest_framework 分页
03-20
02
学习周刊-第03期-第09周
03-03
03
学习周刊-第02期-第08周
02-24
更多文章>
Theme by Vdoing | Copyright © 2022-2023 zhengwenfeng | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式