设置“容器”网络命名空间创建命名空间
在命名空间内运行 Web 服务器
连接到本地命名空间
连接到主机连接 veth 对
分配 IP
配置 veth 对
连接到外部世界启用 IP 转发
设置伪装规则
配置 DNS 解析器
临时
永久
设置容器路由
将主机配置为路由器
结果
清理
如果你在过去几年中从事技术领域的工作,你可能至少听说过容器。容器无疑是近年来最好的创新之一,并且已经席卷了整个网络世界。如果你也像大多数人一样,包括我在内,我们通常只知道它使用 cgroups 和命名空间来实现这种分离。直到最近,我对网络产生了极大的兴趣,并开始揭开构建容器网络堆栈的所有层次。在这里,我将分享我在学习容器网络过程中收集到的一些知识。
在开始之前,有几点需要注意。由于这是理解容器网络的第一步,我选择了使用 Docker 作为示例。Kubernetes 所做的很多事情基本相同,但有一些差异。了解这些差异可能是下一步的好选择,我可能会进一步探索。此外,由于我们只处理网络,我们将只进行网络命名空间。这意味着尽管在不同的网络命名空间中运行进程,它们将共享相同的文件。这一点稍后会更有意义。同样,我们将在这里进行的大多数网络相关更改都是使用ip 命令完成的。如果你没有这个工具,可以在大多数 Linux 发行版中使用iproute2 包来获取它。
正如我之前所说,我们将仔细研究 Docker 的容器网络实现,这里是 Docker 网络的官方设计文档链接,https://github.com/moby/moby/blob/master/libnetwork/docs/network.md
注意:我目前在一个 Arch 主机机器内的 Fedora 虚拟机中运行。我的 Fedora 盒子没有安装 Docker。这一点很重要,因为 Docker 默认会启用一些 sysctl 设置。这些设置配置了内核如何处理网络,因此可能会出现一些变化。如果你遇到任何问题,请告诉我。现在,所有这些都解决了,让我们终于进入有趣的部分。
设置“容器”网络命名空间
在我们的无容器世界中,单个网络命名空间本身将代表 Docker 中的一个实际容器或 Kubernetes 中的一个 Pod。对于 Kubernetes 用户来说,这可能看起来很奇怪,因为 Kubernetes 中的一个 Pod 可以包含多个容器。但对于他们,我想提醒你,尽管一个 Pod 可以有多个容器,但它们都共享相同的网络命名空间。这就是为什么 Pod 内的容器可以使用localhost 相互通信。
创建命名空间
我们使用ip 工具创建这样的命名空间,如下所示:
fedora@localhost:~$ sudo ip netns add container-1
fedora@localhost:~$ ip netns list
注意:对于那些运行 Docker 并兴奋地想查看运行容器的命名空间的人,上述命令不会列出它们。我知道,我试过了,直到我发现 Docker 将它们的网络命名空间存储在 /var/docker/netns 目录下,而 iproute2 将其存储在 /var/netns 目录下。
在命名空间内运行 Web 服务器
我们的下一步是在这个命名空间内运行一个进程。虽然我们可以在命名空间内运行任何进程,但我们目前想运行一个 Web 服务。这将允许我们测试网络连接性。为此,你可以使用任何你选择的 Web 服务,只要确保它们暴露了一个你可以curl 以获取响应的端点。
我选择编写一个非常简单的golang Web 应用程序。我将主要跳过应用程序本身的细节,只在这里放一些相关的部分。
port := flag.String("p", "8000", "port to listen")
flag.Parse()
type Response struct {
Header http.Header `json:"header"`
Body string `json:"body"`
}
// http.Handle("GET /echo", http.HandlerFunc(GetEchoHandler))
func GetEchoHandler(w http.ResponseWriter, r *http.Request) {
resp := Response{
Header: r.Header,
Body: "Echo response",
}
logger.Info("GET /echo response")
WriteJson(w, resp)
}
我有一个命令行选项可以更改端口,默认值设置为8000。还有一个GET /echo 端点,它只返回一个包含头信息和消息体的json 有效载荷。
事实上,如果你在系统中安装了 Python,你也可以直接从 shell 运行一个 Python Web 服务器,而无需编写一行代码。
fedora@localhost:~$ python -m http.server # 默认在端口 8000 运行
fedora@localhost:~$ python -m http.server 8080 # 将端口号更改为 8080
对于剩下的部分,我将使用我的echo-server,但请记住,你可以用上面的Python Web 服务器代替。首先运行:
fedora@localhost:~$ sudo ip netns exec container-1 ./echo-server
然后,你应该会看到你的应用程序启动。为了验证我们确实在命名空间内运行应用程序,你可以运行:
fedora@localhost:~$ sudo ip netns pid container-1
这将输出 Web 服务器的pid。
连接到本地命名空间
如果你是一个实验者,你可能已经注意到你实际上无法在 localhost 上访问你的 Web 服务器。
fedora@localhost:~$ sudo ip netns exec container-1 curl localhost:8000/echo
curl: (7) Failed to connect to localhost port 8000 after 0 ms: Couldn't connect to server
答案可以通过运行以下命令找到:
fedora@localhost:~$ sudo ip netns exec container-1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
我们的环回接口是关闭的。所以我们首先启动环回接口,然后再次尝试curl:
fedora@localhost:~$ sudo ip netns exec container-1 ip link set lo up
fedora@localhost:~$ sudo ip netns exec container-1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host proto kernel_lo
valid_lft forever preferred_lft forever
fedora@localhost:~$ sudo ip netns exec container-1 curl -s localhost:8000/echo | jq
{
"header": {
"Accept": [
"*/*"
],
"User-Agent": [
"curl/8.6.0"
]
},
"body": "Echo response"
}
这次我们成功了。
通过这个,你在这个命名空间内启动的任何进程现在都可以使用 localhost 相互通信。
连接到主机
我们现在希望能够连接到主机命名空间本身。
如果我们想在现实生活中连接两台物理设备,我们可能会选择路由器、交换机或其他设备。我们稍后会谈到这些。但在最基本的形式中,我们可以简单地用一根网线连接两台设备的以太网端口,然后(当然需要一些配置)就可以相互通信了。
同样,在我们的虚拟世界中,我们有一个虚拟以太网设备,veth。类似于实际的以太网线有两个端点,我们的veth 网络设备也有一对。因此,我们可以想象将veth 接口的一端连接到我们的主机默认命名空间,另一端连接到我们的container-1 命名空间。这个veth 网络接口只是我们可以在 Linux 系统中创建和使用的许多其他网络接口之一。你可以在这篇RedHat 文档[2] 中了解更多关于我们可以创建的其他设备。
连接veth 对
我们稍后会讨论更多这些设备,但现在,让我们创建一个veth 对。
fedora@localhost:~$ sudo ip link add veth1 type veth peer name vethc1
fedora@localhost:~$ sudo ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:a5:e0:0e brd ff:ff:ff:ff:ff:ff
inet 192.168.122.240/24 brd 192.168.122.255 scope global dynamic noprefixroute enp1s0
valid_lft 2547sec preferred_lft 2547sec
inet6 fe80::5054:ff:fea5:e00e/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: vethc1@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 3e:15:d3:43:3b:31 brd ff:ff:ff:ff:ff:ff
4: veth1@vethc1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 92:b2:24:76:93:41 brd ff:ff:ff:ff:ff:ff
通过这个,我们现在有了一个veth 对。它们目前连接到默认命名空间。我们现在希望将其中一对发送到我们的容器命名空间。
fedora@localhost:~$ sudo ip link set veth1 netns container-1
fedora@localhost:~$ sudo ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:a5:e0:0e brd ff:ff:ff:ff:ff:ff
inet 192.168.122.240/24 brd 192.168.122.255 scope global dynamic noprefixroute enp1s0
valid_lft 2377sec preferred_lft 2377sec
inet6 fe80::5054:ff:fea5:e00e/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: vethc1@if4: <BROADCAST,MULTICAST>
这是我将使用的 IP 配置:
我们现在只需要为veth 对分配 IP,然后启动接口。
fedora@localhost:~$ sudo ip a add 172.18.0.1/24 dev vethc1
fedora@localhost:~$ sudo ip link set vethc1 up
fedora@localhost:~$ sudo ip netns exec container-1 ip a add 172.18.0.2/24 dev veth1
fedora@localhost:~$ sudo ip netns exec container-1 ip link set veth1 up
fedora@localhost:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 52:54:00:a5:e0:0e brd ff:ff:ff:ff:ff:ff
inet 192.168.122.240/24 brd 192.168.122.255 scope global dynamic noprefixroute enp1s0
valid_lft 3011sec preferred_lft 3011sec
inet6 fe80::5054:ff:fea5:e00e/64 scope link noprefixroute
valid_lft forever preferred_lft forever
3: vethc1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 3e:15:d3:43:3b:31 brd ff:ff:ff:ff:ff:ff link-netns container-1
inet 172.18.0.1/24 scope global vethc1
valid_lft forever preferred_lft forever
inet6 fe80::3c15:d3ff:fe43:3b31/64 scope link tentative proto kernel_ll
valid_lft forever preferred_lft forever
fedora@localhost:~$ sudo ip netns exec container-1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host proto kernel_lo
valid_lft forever preferred_lft forever
4: veth1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 92:b2:24:76:93:41 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.18.0.2/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::90b2:24ff:fe76:9341/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
通过这个,你现在应该能够ping 命名空间了。
fedora@localhost:~$ ping 172.18.0.2 -c 3
PING 172.18.0.2 (172.18.0.2) 56(84) bytes of data.
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.022 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.032 ms
64 bytes from 172.18.0.2: icmp_seq=3 ttl=64 time=0.026 ms
--- 172.18.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2046ms
rtt min/avg/max/mdev = 0.022/0.026/0.032/0.004 ms
fedora@localhost:~$ sudo ip netns exec container-1 ping 172.18.0.1 -c 3
PING 172.18.0.1 (172.18.0.1) 56(84) bytes of data.
64 bytes from 172.18.0.1: icmp_seq=1 ttl=64 time=0.041 ms
64 bytes from 172.18.0.1: icmp_seq=2 ttl=64 time=0.022 ms
64 bytes from 172.18.0.1: icmp_seq=3 ttl=64 time=0.029 ms
--- 172.18.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2030ms
rtt min/avg/max/mdev = 0.022/0.030/0.041/0.007 ms
那么从主机curl 呢?
fedora@localhost:~$ curl -s 172.18.0.2:8000/echo | jq
{
"header": {
"Accept": [
"*/*"
],
"User-Agent": [
"curl/8.6.0"
]
},
"body": "Echo response"
}
太棒了,现在我们有了主机和容器之间的网络连接。
有趣的事实:如果你在主机上运行 Docker 或 Kubernetes,你将能够在系统上看到每个容器的 veth 对。尽管你会看到一个区别,即没有任何 veth 设备分配了 IP。当我们启动多个容器时,我们会回到这一点。
连接到外部世界
如果你尝试访问主机外部,你会发现这是不可能的。
fedora@localhost:~$ sudo ip netns exec container-1 ping example.com
ping: connect: Network is unreachable
答案可以通过检查当前的路由规则找到。
fedora@localhost:~$ sudo ip netns exec container-1 ip r
172.18.0.0/24 dev veth1 proto kernel scope link src 172.18.0.2
设置容器路由
还记得我们如何手动为容器分配静态 IP 吗?现在我们还需要手动设置 IP 路由。
fedora@localhost:~$ sudo ip netns exec container-1 ip r add default via 172.18.0.1 dev veth1 src 172.18.0.2
fedora@localhost:~$ sudo ip netns exec container-1 ip r
default via 172.18.0.1 dev veth1 src 172.18.0.2
172.18.0.0/24 dev veth1 proto kernel scope link src 172.18.0.2
解释一下我们的ip r[oute] add 在做什么,我们正在添加一个默认路由(也称为网关路由),通过172.18.0.1,这是我们的主机地址。这条路由通过veth1 接口。任何通过这个接口使用这条路由离开的数据包都必须具有172.18.0.2 的源 IP。默认路由是设备不知道如何路由接收到的 IP 时必须采取的路由。网关连接到更多的网络接口,希望它们知道如何路由数据包,但如果它们也不知道,它们就会将数据包发送到它们的网关,直到更高级别的网络可以。
当然,这还不够。我们的 Linux 机器首先是网络端点,然后才是路由器。因此,内核设置为如果接收到不属于它的数据包,它只会丢弃它。我们现在必须配置我们的主机系统作为路由器。为什么这很重要,因为我们的容器现在在一个只能通过我们的主机系统路由的虚拟网络中。所以我们必须配置我们的主机作为路由器,以便我们的容器正常工作。
将主机配置为路由器
启用 IP 转发
我们首先允许我们的 Linux 机器进行ip 转发。这是告诉 Linux 内核在接收到不属于该设备的数据包时该做什么的设置。
fedora@localhost:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
有多种方法可以启用(设置为 1)此设置。
临时
fedora@localhost:~$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
永久
fedora@localhost:~$ echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/10-ip_forward.conf
注意:你可以通过 sysctl -p 加载配置文件,或者重启。如果你选择重启,你将不得不从头开始,因为我们目前所做的所有配置都是临时的。
设置伪装规则
设置 IP 转发规则后,接下来要考虑的是 NAT。如果你对 NAT 不太了解,基本思想是我们的主机所在的任何网络都知道如何到达我们的主机,但不知道主机内部的虚拟网络。每个路由器都有这个 IP 伪装的步骤,它将源 IP 更改为来自路由器的 IP,这样当回复返回时,它会返回到路由器,路由器可以将其转发回原始主机。这是整个互联网工作的协议。
为此,我们最终使用iptables。
fedora@localhost:~$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/24 ! -o vethc1 -j MASQUERADE
如果你不经常使用iptables,这个语法看起来像魔法。但简而言之,我们正在向nat 表添加一条规则。这是存储内核中任何 NAT 规则的表。同样,我们正在挂接到POSTROUTING 链。数据包在内核中通过这个链传递,当所有路由决策都已经做出时,如果你在这个阶段更改源或目标 IP,它不会改变数据包下一步的去向。然后我们应用过滤器,以便只有源 IP 在172.18.0.0/24 CIDR 范围内的数据包被转换,并且它们不应该通过vethc1 接口。
当我们开始添加多个容器时,添加后一个过滤器的理由就变得有意义了。最后,我们有由-j 定义的跳转规则。这基本上告诉数据包在通过所有过滤器后应该去链中的哪个位置。在我们的例子中,我们将其设置为默认的MASQUERADE 链,该链负责 IP 伪装。
配置 DNS 解析器
添加规则后,我们的容器基本上已经准备好与外部世界通信了。然而,根据resolv.conf 的设置,我们可能仍然会遇到问题。
还记得我们只进行了网络命名空间,而没有使用任何其他命名空间吗?好吧,如果你检查你的主机resolv.conf,它也是我们的容器使用的同一个resolv.conf。根据你的系统如何设置 DNS 解析,它可能仍然无法将域名解析为 IP 地址。
nameserver 127.0.0.53 options edns0 trust-ad search .这是我的resolv.conf,你可以看到它指向在127.0.0.53 中搜索,如果你知道你的 localhost CIDR(127.0.0.0/8),这是一个环回 IP。我们在容器内没有运行任何进程来解析 localhost 中的 DNS 名称。我们可以通过将命名空间设置为指向你自己的 DNS 服务器来解决这个问题。我将在我的示例中使用 Cloudflare DNS,即1.1.1.1。
fedora@localhost:~$ sudo mkdir -p /etc/netns/container-1
fedora@localhost:~$ echo "nameserver 1.1.1.1" | sudo tee -a /etc/netns/container-1/resolv.conf
nameserver 1.1.1.1
幸运的是,ip-netns 配置为使用这个约定来映射resolv.conf,如man 页面[3] 所示。
结果
将所有这些放在一起:
fedora@localhost:~$ sudo ip netns exec container-1 ping example.com -c3
PING example.com (93.184.215.14) 56(84) bytes of data.
64 bytes from 93.184.215.14: icmp_seq=1 ttl=44 time=101 ms
64 bytes from 93.184.215.14: icmp_seq=2 ttl=44 time=154 ms
64 bytes from 93.184.215.14: icmp_seq=3 ttl=44 time=102 ms
--- example.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 100.828/119.025/154.437/25.042 ms
fedora@localhost:~$ sudo ip netns exec container-1 dig example.com
; <<>> DiG 9.18.26 <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 33889
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 3403 IN A 93.184.215.14
;; Query time: 8 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
;; WHEN: Wed Jul 24 15:30:05 CEST 2024
;; MSG SIZE rcvd: 56
最后,我们在系统中运行了一个“容器”。它在端口8000 上提供了一个 Web 服务器,可以从“容器”系统和我们的主机系统访问。它还可以使用我们的主机系统连接到互联网。
让我们对当前系统进行一个高层次的概述。
但我们才刚刚开始,请继续阅读系列的第二部分,了解如何处理多个“容器”。
清理
要清理我们所做的所有更改,你可以回溯并逐一删除更改,或者直接重启你的系统。
如果这篇文章帮助到你,你能给它一个 👏 并考虑关注我以获取更多技巧和窍门,我将非常感激。你的支持对我来说意义重大!
品质保证
多年的生产力软件专家
专业实力
资深技术支持项目实施团队
安全无忧
多位认证安全工程师
多元服务
软件提供方案整合,项目咨询实施
购软平台-找企业级软件,上购软平台。平台提供更齐全的软件产品、更专业的技术服务,同时提供行业资讯、软件使用教程和技巧。购软平台打造企业级数字产品综合应用服务平台。用户体验和数字类产品的专业化服务是我们不断追求的目标。购软平台您身边的企业级数字产品优秀服务商。