惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
量子位
博客园 - 三生石上(FineUI控件)
Hacker News - Newest:
Hacker News - Newest: "LLM"
月光博客
月光博客
博客园 - 叶小钗
爱范儿
爱范儿
S
SegmentFault 最新的问题
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Security Archives - TechRepublic
Security Archives - TechRepublic
小众软件
小众软件
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security @ Cisco Blogs
V
Visual Studio Blog
V
V2EX
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
有赞技术团队
有赞技术团队
C
Check Point Blog
T
Troy Hunt's Blog
Google DeepMind News
Google DeepMind News
Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
Engineering at Meta
Engineering at Meta
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Schneier on Security
N
Netflix TechBlog - Medium
Project Zero
Project Zero
Last Week in AI
Last Week in AI
N
News and Events Feed by Topic
Microsoft Azure Blog
Microsoft Azure Blog
NISL@THU
NISL@THU
T
The Exploit Database - CXSecurity.com
AWS News Blog
AWS News Blog
博客园 - 聂微东
S
Securelist
腾讯CDC
O
OpenAI News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
About on SuperTechFans
Microsoft Security Blog
Microsoft Security Blog
B
Blog
博客园 - 【当耐特】
Y
Y Combinator Blog
N
News and Events Feed by Topic
Recorded Future
Recorded Future
Vercel News
Vercel News
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络 理解Linux TunTap设备 快速了解iptables kafka中listener和advertised.listeners的作用 django rest_framework 分页 django后端服务、logstash和flink接入VictoriaMetrics指标监控 python中import原理 docker容器单机网络 mysql之MVCC原理 mysql之日志 使用java开发logstash的filter插件 使用python实现单例模式的三种方式 redis之缓存 redis之分片集群 redis之哨兵机制 redis之主从库同步 redis之持久化 redis之五种基本数据类型 go中如何处理error pod中将代码与运行环境分离 ddt源码分析 python装饰器的使用方法 读书笔记:如何阅读一本书 使用ddt实现unittest的参数化测试 使用kubeadm安装k8s 优化gin表单的错误提示信息 gin中validator模块的源码分析 go简单使用grpc python简单使用grpc k8s之PV、PVC和StorageClass k8s之StatefulSet k8s之DaemonSet k8s之Job和CronJob k8s之ConfigMap和Secret k8s之Service k8s之Pod k8s之Deployment 容器的本质 docker容器 python迭代器与生成器 python元编程 python垃圾回收机制 python上下文管理器 django rest_framework使用jwt django rest_framework异常处理 django rest_framework 自定义文档 django压缩文件下载 django rest_framework使用pytest单元测试 django restframework choice 自定义输出数据 django Filtering 使用 django viewset 和 Router 配合使用时报的错 django model的序列化 django中使用AbStractUser django.core.exceptions.ImproperlyConfigured Application labels aren't unique, duplicates users django 中 media配置 django 外键引用自身和on_delete参数 django 警告 while time zone support is active Flask使用flask_socketio实现websocket flask结合mongo tornado 文件上传 tornado 使用jwt完成用户异步认证 tornado 用户密码 bcrypt加密 tornado 结合wtforms使用表单操作 tornado finish和write区别 tornado 使用peewee-async 完成异步orm数据库操作 pyspark streaming简介 和 消费 kafka示例 使用hue创建ozzie的pyspark action workflow count的性能优化 django rest_framework Authentication django celery 结合使用 网站
手动实现docker容器bridge网络模型
zhengwenfeng · 2023-01-08 · via 郑文峰的博客

# 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 网卡
  1. 在宿主机上 ping 容器内部 ip,是可以 ping 通的
  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