如果您经常与容器打交道,那么您可能已经多次发布了端口。发布的典型需求是这样的:您正在本地开发一个web应用程序,但在一个容器中,您希望使用笔记本电脑的浏览器测试它。接下来要做的是运行命令 docker run -p 8080:80 app,然后在浏览器中打开 localhost:8080。小菜一碟

但是你有没有想过当你要求Docker发布一个端口时会发生什么?

在本文中,我将尝试连接端口发布(Docker显然创造了这个术语)和一种更传统的网络技术(称为端口转发)之间的点。我还将了解不同的“单主机”容器运行时(Docker Engine、Docker Desktop、containerd、nerdclt和Lima),以比较端口发布实现和功能。

一如既往,最终目标是加深对该技术的理解,并接近成为容器的超级用户!

什么时端口转发或端口映射

        当我们说端口时,通常指的是由一对IP地址和端口号定义的某个[互联网]socket地址。

socket用于表示网络数据交换的参与者。因此,端口转发或端口映射只是一种奇特的方式,可以说寻址到一个套接字的数据被中介(例如,通过网络路由器或代理程序)重定向到另一个套接字。如下图:

1panel的容器终端连接不上 容器 端口_1panel的容器终端连接不上

 从技术上讲,端口转发是网络地址转换(NAT)的一种形式。

有两种常见的重定向网络数据的方法(实现端口转发):

  • 通过偷偷修改数据包的目的地址。
  • 通过在客户端和服务器之间显式地放置代理。、

在第一种情况下,最初发往某个<addr1:port1>的数据包在途中被修改(或翻译),然后被传送到<addr2:port2>。通常,这是由一个Linux内核组件完成的,比如配置了一堆iptables规则的netfilter。

在第二种情况下,面向客户端的socket实际上由L4+代理进程维护,该进程读取传入数据并将其重新发送到最终目的地。 例如,没有包被实际翻译,只有有效载荷数据被代理。

虽然这两种方法之间存在细微差别,但大多数时候,它们是可互换的:

1panel的容器终端连接不上 容器 端口_网络_02

 容器和端口转发

通常,容器有自己的网络堆栈和IP地址。能够访问该IP地址允许您调用容器内部运行的任何服务(假设它们侦听容器的公共接口)。

例如,使用Linux上的Docker Engine(不要与自2022年5月以来也支持Linux的Docker Desktop混淆),您可以创建一个nginx容器并从curl它:

1panel的容器终端连接不上 容器 端口_1panel的容器终端连接不上_03

如果你有足够的勇气在主系统上运行容器,你甚至可以在你喜欢的浏览器中打开上面的地址。这是一种方便的功能,我在执行一些特殊任务时经常依赖它。但它真的有用吗?

嗯,我知道通过IP访问Docker容器至少有两个显著的“不便”:

  • 容器IP是动态分配的,容器的重新启动(或重新创建)可能会导致其IP地址发生变化。
  • 默认情况下,容器IP只能从主机内部路由,而不能从外部访问。

这就需要端口发布派上用场的地方!

在其简单的形式中,用 docker run --publish 8080:80 nginx 命令创建一个常规的端口转发,从主机的0.0.0.0:8080到容器的$CONT_IP:80。

由于映射的两端都驻留在一个Linux主机上,因此这样的转发可以通过几个iptables规则来实现:

 

1panel的容器终端连接不上 容器 端口_网络_04

因此,在Docker Engine的情况下,端口发布和良好的旧内核空间端口转发之间似乎没有太大区别。Docker在顶部添加的唯一“额外”功能是在容器IP地址更改的情况下自动更新映射的目标部分。

1panel的容器终端连接不上 容器 端口_运维_05

您可能知道docker run-publish允许显式指定主机的接口,但您是否也知道docker支持8080-8090语法的端口范围?

                      docker run --publish 127.0.0.1:8080-8090:80 nginx                                             

端口发布和不同的网络驱动程序

上面一节中的实验是使用默认(网桥)网络驱动程序完成的。显然,基于网桥的网络支持并受益于端口发布。然而,Docker还有其他几种网络类型。用Docker自己的话说:

容器使用的网络类型,无论是网桥、覆盖层、macvlan网络还是自定义网络插件,在容器内部都是透明的。从容器的角度来看,它有一个带有IP地址、网关、路由表、DNS服务和其他网络细节的网络接口(假设容器没有使用无网络驱动程序)。

但网络类型是否会影响端口发布功能?

让我们逐一回顾每种网络类型:

  • none 网络意味着没有外部网络接口(因此没有IP地址),容器中只有环回。没有IP地址-没有端口转发。
  • host网络意味着容器没有自己的网络堆栈。相反,它重用了主机的一个。您仍然可以转发端口,但您必须自己完成(例如,通过手动创建iptables规则),因为从Docker的角度来看,几乎不需要它。
  • container:<other>网络表示当前容器重用另一个容器的网络模式。特别是,它使容器继承已经发布的端口(如果有的话)。但不能在该模式下发布自己的端口。
  • overlay网络似乎是Swarm时代的回声。显然,您可以在那里进行某种端口发布,但要针对“服务”而不是单个容器。对这种模式没有太多的经验,所以不要猜测。
  • ipvlan and macvlan 网络模式不支持端口转发,因为它们可能假设分配给容器的IP地址是可直接路由的,不属于Docker的基本职责。

总而言之,您希望使用端口发布的唯一网络类型是网桥。但这是默认类型,可能是最广泛的类型,因此这种限制不会降低端口发布的价值。

⚠️ 注意,大多数时候,如果底层网络驱动程序不支持端口转发,Docker会默默忽略-p |--publish标志。

容器桌面和端口发布

令人惊讶的是, Docker Desktop在容器网络方面的表现与Docker Engine不同。以下是当我尝试通过MacBook上的IP地址访问容器时发生的情况:

$ docker run -d --name nginx-1 nginx

$ CONT_IP=$(

        docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' nginx-1 

) 

$ ping $CONT_IP

PING 172.17.0.3 (172.17.0.3): 56 data bytes 

Request timeout for icmp_seq 0 

Request timeout for icmp_seq 1 

^C 

--- 172.17.0.3 ping statistics --- 

3 packets transmitted, 0 packets received, 100.0% packet loss

嗯,这有点道理。Docker容器需要Linux内核。因此,Docker Desktop for Mac(可能也适用于Windows和Linux)悄悄启动了一个全面的虚拟机,该虚拟机运行预先安装了Docker Engine的定制Linux发行版。因此,上述172.17.0.3地址是来自仅存在于该虚拟机内部的网桥网络的地址。显然,它不能从主机系统(在我的例子中是macOS)路由。

1panel的容器终端连接不上 容器 端口_服务器_06

 但无论如何,端口发布在Docker Desktop中有效!

$ docker run -d -p 8080:80 --name nginx-1 nginx

$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

下图解释了Docker桌面网络的内部结构。特别是,它揭示了如何进行端口转发:

1panel的容器终端连接不上 容器 端口_服务器_07

图片来源

长话短说,以Docker Desktop为例,它是基于用户空间和传统iptables的端口转发的结合。当您启动一个发布端口的容器时,主机上的代理进程(例如,在macOS上)会使用指定的IP地址和端口打开一个真正的socket。您可以使用lsof精确定位此进程: 

$ docker run -d -p 8080:80 --name nginx-1 nginx

$ sudo lsof -i -P | grep LISTEN | grep :8080
com.docke... 24294   iximiuz   76u  IPv6 0x89404e558d90602b    0t0   TCP *:8080 (LISTEN)

$ ps 24294
  PID   TT  STAT      TIME COMMAND
24294   ??  S     38:09.83 /Applications/Docker.app/Contents/MacOS/com.docker.backend -watchdog -native-api

在VM内部,端口发布的实现似乎与本文Docker Engine一节中描述的没有什么不同。可以使用在VM的网络命名空间中运行的特权容器(--net host标志,因为对于Docker Engine,VM是主机)来验证:

$ docker run --privileged --net host -it --rm alpine
/ $# apk add iptables
/ $# iptables -t nat -L
<typical Docker Engine port forwarding iptables rules>

神秘的vpnkit桥(vpnkit项目的一部分)有助于跨越主机和虚拟机系统之间的边界。

总之,当您在Docker Desktop中发布端口时:

  • com.docker.backend 这个主机系统上的后端进程开始充当用户空间代理,并使用指定的地址打开套接字。
  • socket可以被主机上的任何人(或访问主机网络的人)用来向容器发送数据。
  • 当数据出现在套接字上时,它会被vpnkit网桥转发到VM的外部网络接口。
  • 在VM内部,容器的端口已经使用基于Docker Engine iptables的“标准”技术发布到该接口。

如何使用容器发布端口

Docker严重依赖containerd进行较低级别的容器管理,这已经不是什么秘密了。但担忧的确切分离可能并不总是明确的。例如,发布端口对我来说总是很低级。但事实证明containerd不能自己发布端口!

与 docker run --name nginx -1 nginx:latest  类似,您可以使用

ctr run docker.io/library/nginx:latest nginx-1 

因此,端口发布显然是Docker自己的职责之一。但还是有其他办法。。。

使用强大的nerdctl进行端口转发

nerdctl是containerd的流行CLI客户端。与极简主义的ctr不同,nerdctl尝试丰富功能并兼容Docker(这意味着它模仿Docker CLI命令)。但如果它与Docker兼容,那么它必须实现端口发布!

对nerdctl源代码的快速检查显示(cmd/nnerdctl/run.go,ocihook/ocihook.go),它依赖于OCI运行时生命周期钩子(准确地说是在“CreateRuntime”上)来为即将启动的容器设置一些额外的网络功能。但是nerdctl并不能自己进行实际的网络配置。相反,它使用了helper containerd/go cni包,该包特别允许配置端口映射(很可能还将实际作业委托给相应的端口映射cni插件)。

🤓 注意,上述机制仅用于端口转发等额外配置。容器的网络名称空间及其与网桥网络的连接的实际创建是由containerd自己完成的(尽管可能是通过运行底层容器运行时和/或相同的containerd/go cni助手包)。

一个验证上述发现的小实验表明,portmap CNI插件使用了已经熟悉的iptables技巧来实现端口转发。因此,事实上,nerdctl与Docker Engine在这种情况下的作用非常接近:

$ sudo nerdctl run -d --name nerd-nginx-1 -p 8080:80 nginx

$ sudo nerdctl inspect \
  -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  nerd-nginx-1
10.4.0.4

$ sudo iptables -t nat -L | grep '10.4.0.4'
CNI-6603d34f605da0cf8e0a0934  all  --  10.4.0.4    anywhere             
DNAT       tcp  --  anywhere           anywhere    tcp dpt:http-alt to:10.4.0.4:80

Lima如何实现端口转发

Lima项目试图用nerdctl打包containerd,并使该包在macOS上轻松运行。这应该包括运行虚拟机:

Lima推出了具有自动文件共享和端口转发(类似于WSL2)的Linux虚拟机和containerd。

因此,在架构上,Lima与Docker Desktop有些相似。

$ limactl start

$ nerdctl.lima run -d --name lima-nginx-1 -p 8080:80 nginx
$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

OK,在Lima中发布端口是可以工作的,但它是如何实现的?

$ sudo lsof -i -P | grep LISTEN | grep :8080
ssh    86663    iximiuz   15u  IPv4 0xcf4f91a867ed7809    0t0  TCP localhost:8080 (LISTEN)

$ ps 86663
  PID   TT  STAT      TIME COMMAND
86663   ??  Ss     0:00.02 ssh: /Users/iximiuz/.lima/default/ssh.sock [mux]

我非常喜欢我在那里发现的东西!Lima使用旧的SSH隧道在主机系统(macOS)和VM中的服务之间转发端口。

1panel的容器终端连接不上 容器 端口_docker_08

                                                             图片来源

在VM内部,它只是一个常规容器(由nerdctl启动),其端口发布为0.0.0.0:

$ limactl shell default

$ nerdctl ps -a
CONTAINER ID  IMAGE         COMMAND                 CREATED        STATUS  PORTS                 NAMES
6f2aa0304e4c  nginx:latest  "/docker-entrypoint.…"  5 seconds ago  Up      0.0.0.0:8080->80/tcp  lima-nginx-1

 代替结论

那么,端口发布和端口转发之间有什么真正的区别吗?还是端口发布只是Docker调用容器端口转发的一种方式?好吧,你告诉我😉

无论如何,了解发布容器的端口时实际发生的情况是有帮助的。例如,它允许您预测哪些用例在技术上是可行的,并允许实现一些相当高级的场景,如发布已运行容器的端口或访问侦听容器本地主机的服务。