@iximiuz: 容器网络工作原理:从零开始构建桥接网络。要真正理解Docker和Kubernetes的网络…

X AI KOLs Timeline 工具

摘要

本教程说明如何使用标准的Linux工具(如网络命名空间、veth对、桥接器和NAT)从零开始为容器构建桥接网络,以揭示Docker和Kubernetes网络的奥秘。

容器网络工作原理:从零开始构建桥接网络。要真正理解Docker和Kubernetes网络,你应该从基础开始:仅使用标准Linux命令行工具创建和互连网络命名空间:https://labs.iximiuz.com/tutorials/container-networking-from-scratch…
查看原文
查看缓存全文

缓存时间: 2026/07/16 00:01

容器网络如何工作:从零搭建桥接网络

来源:https://labs.iximiuz.com/tutorials/container-networking-from-scratch

使用容器有时就像变魔术一样。对于了解内部原理的人来说是好事,而对于不了解的人则是灾难。幸运的是,我们已经研究容器化技术内核有一段时间了,甚至成功揭示了:

  • 容器不过是隔离和受限的 Linux 进程(https://iximiuz.com/en/posts/not-every-container-has-an-operating-system-inside/#container-is-just-a-processes)
  • 运行容器并不真正需要镜像(https://iximiuz.com/en/posts/you-dont-need-an-image-to-run-a-container/)
  • 相反,构建镜像可能需要运行容器(https://iximiuz.com/en/posts/you-need-containers-to-build-an-image/)

现在,是时候解决容器网络问题了。更准确地说,是单主机容器网络问题。在本文中,我们将回答以下问题:

  • 如何虚拟化网络资源,让容器以为自己拥有独立的网络环境?
  • 如何让容器成为友好的邻居,并学会彼此通信?
  • 如何从容器内部访问外部世界(如互联网)?
  • 如何从外部世界访问运行在 Linux 主机上的容器?
  • 如何实现类似 Docker 的端口发布?

在回答这些问题的过程中,我们将使用标准 Linux 工具从零搭建一个单主机容器网络。最终会明白,容器网络的魔力源于更基础的 Linux 设施的巧妙组合:

  • 网络命名空间(netns
  • 虚拟以太网设备(veth
  • 虚拟网络交换机(bridge
  • IP 路由和网络地址转换(NAT

让我们开始吧!🚀
提升你的后端技能 —— 加入 30,000 名工程师,直接收学习干货到邮箱

前置条件

假设你具备基本的 bash 脚本和 Linux 命令行知识,但不需要编码技能。本教程的示例在 Ubuntu 22.04 上测试过,很可能也适用于其他现代 Linux 发行版。建议使用隔离且可丢弃的沙盒环境(如右侧提供的环境 👉)来跟随教程。

⚠️注意:直接在计算机主机操作系统上运行以下命令可能有害

主要网络环境组件(设备、路由表、防火墙规则)

Linux 网络环境(即网络上下文)由什么构成?显然,是一组网络设备。还有什么?可能还有一些路由规则。当然,不能忘记 netfilter 钩子,包括由 iptables 规则定义的那些。还有更多组件,但以上内容对本教程已足够。

Linux 网络环境可视化。

我们可以快速编写一个(非全面的)脚本来检查网络环境:

inspect-net-context.sh

#!/usr/bin/env bash
echo "# Network devices"
ip link list
echo -e "\n# Route table"
ip route list
echo -e "\n# iptables rules"
iptables --list-rules

不过,在运行之前,我们先添加一条新的 iptables 链,以便规则集更具辨识度:

💡 本教程中的所有命令都应使用 root 权限运行,或在前面加上 sudo

iptables --new-chain MY_CUSTOM_CHAIN

之后,在我的机器上执行检查脚本会产生以下输出:

如果我们在典型的 Linux 机器上运行该脚本,会得到类似以下的输出:

# Network devices
1: lo: <LOOPBACK> 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
...
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 92:b2:3d:42:ed:22 brd ff:ff:ff:ff:ff:ff
# Route table
default via 172.16.0.1 dev eth0
172.16.0.0/16 dev eth0 proto kernel scope link src 172.16.0.2
# iptables rules
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N MY_CUSTOM_CHAIN

为什么需要检查网络环境?因为我们将很快开始创建容器,并且希望确保每个容器都有自己的网络上下文,与主机和其他容器的环境完全隔离。为此,我们需要熟悉如何列出网络设备、路由表和 iptables 规则。

👨🎓 为简单起见,本教程不创建完整容器(使用所有可能的 Linux 命名空间),而是将虚拟化范围限制为仅网络上下文。因此,下文中的容器和网络命名空间将互换使用。

使用网络命名空间(netns)创建第一个容器

你可能已经听说过,用于创建容器的 Linux 命名空间之一叫 netns网络命名空间。根据 man ip-netns(https://man7.org/linux/man-pages/man8/ip-netns.8.html),“网络命名空间是网络栈的另一个逻辑副本,拥有自己的路由、防火墙规则和网络设备。”

在 Linux 中创建网络命名空间的一种方法是使用 ip netns add 命令(其中 ip 工具来自事实上的标准工具集 iproute2(https://en.wikipedia.org/wiki/Iproute2)):

要检查新命名空间是否已添加到系统,运行以下命令:

如何开始使用刚刚创建的命名空间?另一个方便的 Linux 工具是 nsenter。它 进入 一个或多个指定的命名空间,然后在那里执行给定的程序。例如,以下是在 netns0 命名空间中启动一个新 shell 会话的方法:

nsenter --net=/run/netns/netns0 bash

新创建的 bash 进程现在位于 netns0 命名空间中。如果我们在新的 shell 中运行检查脚本,会得到以下输出:

# Network devices
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
# Route table
# Iptables rules
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

上面的输出清楚地表明,运行在 netns0 命名空间中的 bash 进程拥有完全不同的网络环境 —— 没有路由规则,没有 MY_CUSTOM_CHAIN iptables 链,并且只有一个网络设备 loopback。效果不错!

Linux 网络命名空间可视化。

为了巩固本节知识,尝试解决一个挑战:

使用虚拟以太网设备(veth)将容器连接到主机

如果无法与新的隔离网络环境通信,那它将没什么用。幸运的是,Linux 提供了一种特殊的工具来连接网络命名空间 —— 虚拟以太网设备,即 veth。根据 man veth(https://man7.org/linux/man-pages/man4/veth.4.html),“veth 设备是虚拟以太网设备。它们可以充当网络命名空间之间的隧道,以桥接到另一个命名空间中的物理网络设备,也可以作为独立网络设备使用。”

虚拟以太网设备总是成对出现。如果听起来有点困惑,别担心,看个例子就明白了。

⚠️ 我们上面使用的 nsenter 命令在 netns0 网络命名空间中启动了一个嵌套的 shell 会话。在进入下一步之前,别忘了 exit 退出,或者打开一个新的终端标签页。

从根网络命名空间,我们创建一对虚拟以太网设备:

ip link add veth0 type veth peer name ceth0

这一条命令就创建了一对 互连的 虚拟以太网设备。名称 veth0ceth0 是随意选择的。

1: lo: <LOOPBACK> 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
...
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 92:b2:3d:42:ed:22 brd ff:ff:ff:ff:ff:ff
5: ceth0@veth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether b2:d3:e4:24:c3:f1 brd ff:ff:ff:ff:ff:ff
6: veth0@ceth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 4e:ac:e0:3c:d8:6e brd ff:ff:ff:ff:ff:ff

创建后,veth0ceth0 都位于主机的网络上下文中(即根网络命名空间)。要连接根命名空间和我们之前创建的 netns0 命名空间,我们需要将一个设备保留在根命名空间,并将另一个设备移动到 netns0 中:

ip link set ceth0 netns netns0

让我们确认其中一个设备已从根网络上下文中消失:

1: lo: <LOOPBACK> 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
...
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 92:b2:3d:42:ed:22 brd ff:ff:ff:ff:ff:ff
6: veth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 4e:ac:e0:3c:d8:6e brd ff:ff:ff:ff:ff:ff link-netns netns0

一旦我们启用 veth 设备并分配正确的 IP 地址,任何一个设备上出现的任何数据包都会立即出现在其对端设备上,从而有效连接两个网络命名空间。

从根命名空间开始:

ip addr add 172.18.0.11/16 dev veth0

…然后在 netns0 命名空间中继续:

nsenter --net=/run/netns/netns0 bash

netns0 命名空间中运行的新 shell 会话中:

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
5: ceth0@if6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether da:08:47:8b:8f:c7 brd ff:ff:ff:ff:ff:ff link-netnsid 0

看起来新命名空间中的 loopback 设备是 down 的,所以我们先启用它:

ip link set lo up

现在,回到 ceth0 设备:

ip link set ceth0 up
ip addr add 172.18.0.10/16 dev ceth0

通过 veth 设备连接网络命名空间。

我们准备好进行第一次连通性检查了!🎉 让我们尝试netns0 命名空间 ping veth0 设备:

ping 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.093 ms
64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.075 ms
--- 172.18.0.11 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1028ms
rtt min/avg/max/mdev = 0.075/0.084/0.093/0.009 ms

现在,输入 exit 退出 netns0(或打开一个新的终端标签页),然后尝试从根命名空间 ping ceth0 设备:

ping 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.012 ms
64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.078 ms
--- 172.18.0.10 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1024ms
rtt min/avg/max/mdev = 0.012/0.045/0.078/0.033 ms

成功!我们刚刚让数据包在根命名空间和 netns0 命名空间之间流动了。

但如果尝试从 netns0 命名空间访问其他地址,会成功吗?我们来看看!

4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether ee:36:69:36:fe:bd brd ff:ff:ff:ff:ff:ff
    inet 172.16.0.2/16 brd 172.16.255.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::b0b9:11ff:fe79:613f/64 scope link
       valid_lft forever preferred_lft forever

注意到 172.16.0.2 这个地址 —— 它是主机上另一个网络接口的 IP 地址,我们将用它来检查从 netns0 命名空间的连通性:

nsenter --net=/run/netns/netns0 bash

netns0 命名空间访问主机的网络接口:

ping 172.16.0.2
ping: connect: Network is unreachable

如果尝试访问互联网上的地址呢?

ping 8.8.8.8
ping: connect: Network is unreachable

失败很容易解释。netns0 的路由表中根本没有针对这些数据包的记录。唯一的条目显示了如何到达 172.18.0.0/16 网络:

172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

这个路由是怎么来的?Linux 有多种填充路由表的方式,其中之一是从直接连接的网络接口中提取路由。还记得创建命名空间后 netns0 的路由表是空的吗?但后来我们把 ceth0 设备移到了 netns0,并给它分配了 IP 地址 172.18.0.10/16。由于我们使用的不是简单的 IP 地址,而是地址和网络掩码的组合,网络栈成功从中提取了路由信息。由于这个 派生 的记录,netns0 命名空间中发送到 172.18.0.0/16 网络的每个数据包都会通过 ceth0 设备发送。但其他所有数据包都会被丢弃。

类似地,根命名空间中也添加了一条新路由(你可能需要先输入 exit 退出 netns0):

... omitted lines ...
172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11

至此,我们已经准备好回答第一个问题了。我们现在知道如何虚拟化和互连 Linux 网络环境了。 是时候检验知识了!

重复相同步骤创建第二个容器

容器化的出现是因为人们需要更好的方式来共享计算机资源。与其在一台服务器上只运行一个应用程序,容器允许在同一台服务器上运行多个(彼此隔离的)进程。这样就能最大限度地利用服务器能力。

让我们看看如果使用上面的 veth 技巧在同一个主机上放置多个容器会发生什么。

从根命名空间,添加另一个“容器”:

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

…然后在“容器”内部继续:

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

好了,现在到了我们最喜欢的部分 —— 检查连通性(netns1 命名空间):

ping 172.18.0.21
PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.
--- 172.18.0.21 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1023ms

嗯……我们做了和之前一样的事情,但连通性出了问题。从 netns1 无法到达根命名空间。

ip route list
172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20

甚至有一条路由!那为什么不起作用呢?如果尝试从根命名空间 ping ceth1 设备呢?

ping 172.18.0.20
PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.
From 172.18.0.11 icmp_seq=1 Destination Host Unreachable
From 172.18.0.11 icmp_seq=2 Destination Host Unreachable
--- 172.18.0.20 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1014ms pipe 2

同时,从我们的第一个容器(netns0)我们能够到达新容器的主机端(veth1):

nsenter --net=/run/netns/netns0 bash
ping 172.18.0.21
PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.
64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms
64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms
--- 172.18.0.21 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 33ms
rtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms

但我们仍然无法到达 netns1

ping 172.18.0.20
From 1```

相似文章

每个人都应构建自己的网络栈

Hacker News Top

本文描述了在 DN42 去中心化网络上开发和部署一个名为 DNet 的自定义网络栈,展示了其用于 DNS 等服务,以促进动手实践的网络学习和实现。