在通过 SSH 连接的服务器上可靠地运行 Docker 容器中的 X 应用程序,而无需“--net 主机”

Posted

技术标签:

【中文标题】在通过 SSH 连接的服务器上可靠地运行 Docker 容器中的 X 应用程序,而无需“--net 主机”【英文标题】:Run X application in a Docker container reliably on a server connected via SSH without "--net host" 【发布时间】:2018-06-22 10:25:12 【问题描述】:

如果没有 Docker 容器,使用 SSH X11 转发 (ssh -X) 在远程服务器上运行 X11 程序很简单。当应用程序在服务器上的 Docker 容器内运行时,我试图让同样的事情正常工作。当使用 -X 选项通过 SSH 连接到服务器时,会设置 X11 隧道,并且环境变量“$DISPLAY”会自动设置为通常为“localhost:10.0”或类似的值。如果我只是尝试在 Docker 中运行 X 应用程序,我会收到以下错误:

Error: GDK_BACKEND does not match available displays

我的第一个想法是使用“-e”选项将 $DISPLAY 实际传递到容器中,如下所示:

docker run -ti -e DISPLAY=$DISPLAY name_of_docker_image

这有帮助,但不能解决问题。错误信息变为:

Unable to init server: Broadway display type not supported: localhost:10.0
Error: cannot open display: localhost:10.0

在网上搜索后,我发现我可以做一些 xauth 魔法来修复身份验证。我添加了以下内容:

SOCK=/tmp/.X11-unix
XAUTH=/tmp/.docker.xauth
xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge -
chmod 777 $XAUTH
docker run -ti -e DISPLAY=$DISPLAY -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH \ 
  -e XAUTHORITY=$XAUTH name_of_docker_image

但是,这仅在将“--net host”添加到 docker 命令时才有效:

docker run -ti -e DISPLAY=$DISPLAY -v $XSOCK:$XSOCK -v $XAUTH:$XAUTH \ 
  -e XAUTHORITY=$XAUTH --net host name_of_docker_image

这是不可取的,因为它使整个主机网络对容器可见。

为了让它在没有“--net 主机”的 docker 中的远程服务器上完全运行,现在缺少什么?

【问题讨论】:

【参考方案1】:

如果您设置X11UseLocalhost = no,您甚至允许外部流量到达 X11 套接字。即定向到机器外部IP的流量可以到达SSHD X11转发。还有两种可能适用的安全机制(防火墙、X11 身份验证)。不过,如果您正在处理像这种情况下的用户甚至应用程序特定问题,我更愿意单独留下 系统全局设置


这是在 sshd 配置中更改 X11UseLocalhost 的替代方法:

                                           + docker container net ns +
                                           |                         |
           172.17.0.1                      |   172.17.0.2            |
        +- docker0 --------- veth123@if5 --|-- eth0@if6              |
        |  (bridge)          (veth pair)   |   (veth pair)           |
        |                                  |                         |
        |  127.0.0.1                       +-------------------------+
routing +- lo
        |  (loopback)
        |
        |  192.168.1.2
        +- ens33
           (physical host interface)

使用默认的X11UseLocalhost yes,sshd 在根网络命名空间上侦听127.0.0.1。我们需要从 docker 网络命名空间内部获取 X11 流量到根网络 ns 中的环回接口。 veth 对连接到docker0 网桥,因此两端可以在没有任何路由的情况下与 172.17.0.1 通信。根网ns中的三个接口(@98​​7654327@、loens33)可以通过路由进行通信。

我们要实现以下目标:

                                           + docker container net ns +
                                           |                         |
           172.17.0.1                      |   172.17.0.2            |
        +- docker0 --------< veth123@if5 --|-< eth0@if6 -----< xeyes |
        |  (bridge)          (veth pair)   |   (veth pair)           |
        v                                  |                         |
        |  127.0.0.1                       +-------------------------+
routing +- lo >------- sshd -+
           (loopback)        |
                             v
           192.168.1.2       |
           ens33 ------<-----+
           (physical host interface)

我们可以让 X11 应用程序直接与172.17.0.1 对话以“逃离”docker net ns。这是通过适当设置DISPLAY 来实现的:export DISPLAY=172.17.0.1:10:

                                           + docker container net ns+
                                           |                         |
           172.17.0.1                      |   172.17.0.2            |
           docker0 --------- veth123@if5 --|-- eth0@if6 -----< xeyes |
           (bridge)          (veth pair)   |   (veth pair)           |
                                           |                         |
           127.0.0.1                       +-------------------------+
           lo
           (loopback)
         
           192.168.1.2
           ens33
           (physical host interface)

现在,我们在根网 ns 中添加一个 iptables 规则,从 172.17.0.1 路由到 127.0.0.1:

iptables \
  --table nat \
  --insert PREROUTING \
  --proto tcp \
  --destination 172.17.0.1 \
  --dport 6010 \
  --jump DNAT \
  --to-destination 127.0.0.1:6010

sysctl net.ipv4.conf.docker0.route_localnet=1

也许您可以通过仅路由来自该容器(veth 端)的流量来改进这一点。另外,老实说,我不太确定为什么需要route_localnet127/8 似乎是一个奇怪的数据包源/目标,因此默认情况下禁用路由。您可能还可以将流量从 docker net ns 内的环回接口重新路由到 veth 对,然后从那里重新路由到根网络 ns 中的环回接口。

使用上面给出的命令,我们最终得到:

                                           + docker container net ns +
                                           |                         |
           172.17.0.1                      |   172.17.0.2            |
        +- docker0 --------< veth123@if5 --|-< eth0@if6 -----< xeyes |
        |  (bridge)          (veth pair)   |   (veth pair)           |
        v                                  |                         |
        |  127.0.0.1                       +-------------------------+
routing +- lo
           (loopback)

           192.168.1.2
           ens33
           (physical host interface)

但是,现在我们正尝试以172.17.0.1:10 的身份访问 X11 服务器。这不会在 x 授权文件 (~/.Xauthority) 中找到条目,通常类似于 &lt;hostname&gt;:10。使用 Ruben 的建议在 docker 容器中添加一个可见的新条目:

xauth add 172.17.0.1:10 . <cookie>

其中&lt;cookie&gt; 是 SSH X11 转发设置的 cookie,例如通过xauth list

您可能还必须在防火墙中允许进入172.17.0.1:6010 的流量。


您还可以从 docker 容器网络命名空间内的主机启动应用程序:

sudo nsenter --target=<pid of process in container> --net su - $USER <app>

没有su,您将以root 身份运行。当然,你也可以使用另一个容器,共享网络命名空间:

sudo docker run --network=container:<other container name/id> ...

上面显示的 X11 转发机制适用于整个网络命名空间(实际上,适用于连接到 docker0 网桥的所有内容)。因此,它适用于容器网络命名空间内的任何应用程序。

【讨论】:

【参考方案2】:

就我而言,我坐在“远程”并连接到“docker_host”上的“docker_container”:

远程 --> docker_host --> docker_container

为了使用 VScode 更轻松地调试脚本,我将 SSHD 安装到“docker_container”中,报告端口 22,映射到“docker_host”上的另一个端口(比如 1234)。

所以我可以通过 ssh(来自“远程”)直接连接正在运行的容器:

ssh -Y -p 1234 appuser@docker_host.local

(其中appuser 是“docker_container”中的用户名。我现在在本地子网中工作,所以我可以通过 .local 映射引用我的服务器。对于外部 IP,只需确保您的路由器映射到这个端口到这台机器。)

这会通过 ssh 直接从我的“远程”创建到“docker_container”的连接。

远程 --> (ssh) --> docker_container

在“docker_container”里面,我安装了sshd sudo apt-get install openssh-server(您可以将其添加到您的 Dockerfile 以在构建时安装)。

要允许 X11 转发工作,请编辑 /etc/ssh/sshd_config 文件:

X11Forwarding yes
X11UseLocalhost no

然后重新启动容器内的 ssh。您应该从执行到容器中的 shell 执行此操作,从“docker_host”,而不是当您通过 ssh 连接到“docker_container”时:(docker exec -ti docker_container bash)

重启sshd: sudo service ssh restart

当您通过 ssh 连接到“docker_container”时,请检查 $DISPLAY 环境变量。它应该说类似

appuser@3f75a98d67e6:~/data$ echo $DISPLAY
3f75a98d67e6:10.0

通过 ssh 从“docker_container”中执行您最喜欢的 X11 图形程序进行测试(如 cv2.imshow())

【讨论】:

当您的 GUI 应用程序和 X11 服务器在同一个容器中运行时,如何实现相同的功能。比方说,在我的 GUI 应用程序中,如果我输入 xeye,那么我可以看到 xeye 在通过 localhost 端口 6080 和 noVNC 作为客户端连接的 x11 服务器中弹出。所以,我的问题是,我怎样才能通过留在我的 GUI 应用程序中显示与 xeye 相同的结果?我的 GUI 应用程序是 Jupyter lab 我有点困惑。 jupyter lab 不是基于网络的平台吗?这意味着您实际上是在本地本机系统上的浏览器中查看 Jupyter 实验室的结果。容器可能正在运行 Jupyter 应用程序,但您对这个 (GUI) 的真正观察是在您的本机显示器上(无论是 VNC、VM 还是物理显示器)。如果是这种情况,那么连接必须从 VNC 查看容器/VM/本地机器到转发 X11 显示的容器。 ssh -Y 函数会将显示寻址到调用它的平台 感谢您的回复。是的,你是对的 Jupyter 实验室是基于 Web 的,但我要运行的应用程序与 Jupyter 框架不兼容。因此,我创建了 Xserver 并通过 noVNC 在 jupyterlab 和 X-server 之间建立了链接。所以,每当我在我的 jupyterlab 上编写 X-server-app 时,它就会自动运行。在 X-server 中运行应用程序。但我的问题是因为所有库和一切都存在,而不是在 X-server 中打开结果。如何在 jupyterlab 本身上弹出输出。【参考方案3】:

我想通了。当您通过 SSH 连接到计算机并使用 X11 转发时,/tmp/.X11-unix 不用于 X 通信,并且与 $XSOCK 相关的部分是不必要的。

任何 X 应用程序都使用 $DISPLAY 中的主机名,通常是“localhost”并使用 TCP 连接。然后通过隧道返回到 SSH 客户端。在 Docker 中使用“--net host”时,Docker 容器的“localhost”与 Docker 主机相同,因此可以正常工作。

当不指定“--net host”时,Docker 使用默认的桥接网络模式。 这意味着“localhost”意味着容器内的其他东西而不是主机,容器内的X应用程序将无法通过引用“localhost”看到X服务器。因此,为了解决这个问题,必须将“localhost”替换为主机的实际 IP 地址。这通常是“172.17.0.1”或类似的。检查“docker0”接口的“ip addr”。

这可以通过 sed 替换来完成:

DISPLAY=`echo $DISPLAY | sed 's/^[^:]*\(.*\)/172.17.0.1\1/'`

此外,SSH 服务器通常不配置为接受到此 X11 隧道的远程连接。然后必须通过编辑 /etc/ssh/sshd_config(至少在 Debian 中)和设置来更改:

X11UseLocalhost no

然后重启SSH服务器,使用“ssh -X”重新登录服务器。

差不多就是这样,但还有一个复杂的问题。如果 Docker 主机上正在运行任何防火墙,则必须打开与 X11 隧道关联的 TCP 端口。端口号是 $DISPLAY 中 :. 之间的数字加上 6000。

要获取 TCP 端口号,可以运行:

X11PORT=`echo $DISPLAY | sed 's/^[^:]*:\([^\.]\+\).*/\1/'`
TCPPORT=`expr 6000 + $X11PORT`

然后(如果使用 ufw 作为防火墙),为 172.17.0.0 子网中的 Docker 容器打开此端口:

ufw allow from 172.17.0.0/16 to any port $TCPPORT proto tcp

所有命令都可以放在一个脚本中:

XSOCK=/tmp/.X11-unix
XAUTH=/tmp/.docker.xauth
xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | sudo xauth -f $XAUTH nmerge -
sudo chmod 777 $XAUTH
X11PORT=`echo $DISPLAY | sed 's/^[^:]*:\([^\.]\+\).*/\1/'`
TCPPORT=`expr 6000 + $X11PORT`
sudo ufw allow from 172.17.0.0/16 to any port $TCPPORT proto tcp 
DISPLAY=`echo $DISPLAY | sed 's/^[^:]*\(.*\)/172.17.0.1\1/'`
sudo docker run -ti --rm -e DISPLAY=$DISPLAY -v $XAUTH:$XAUTH \
   -e XAUTHORITY=$XAUTH name_of_docker_image

假设您不是 root,因此需要使用 sudo。

代替sudo chmod 777 $XAUTH,你可以运行:

sudo chown my_docker_container_user $XAUTH
sudo chmod 600 $XAUTH

防止服务器上的其他用户在知道您创建 /tmp/.docker.auth 文件的目的时也能够访问 X 服务器。

我希望这应该使它在大多数情况下都能正常工作。

【讨论】:

除了带有“xauth nlist”的神秘行,还可以使用更易于理解的命令: xauth -f /tmp/.docker.xauth add 172.17.0.1:$X11PORT 。 $MAGIC_COOKIE 其中 $MAGIC_COOKIE 可以通过以下方式找到: xauth list $DISPLAY | awk 'print $3' “--net 主机”有什么问题?如果没有“--net host”方法,你需要是超级用户,你需要编辑 sshd_config 文件。不用 sudo 可以吗? @Ruben xauth list $DISPLAY - 空输出

以上是关于在通过 SSH 连接的服务器上可靠地运行 Docker 容器中的 X 应用程序,而无需“--net 主机”的主要内容,如果未能解决你的问题,请参考以下文章

ssh 协议详解

通过 DataGrip 上的 ssh 隧道更快地连接

网络编程ssh,粘包

如何通过关闭连接可靠地确定主体长度(RFC 2616 4.4.5)

通过 SSH 连接 Firebird 数据库

linux 让程序在后台运行的几种可靠方法