在通过 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中的三个接口(@987654327@、lo
和ens33
)可以通过路由进行通信。
我们要实现以下目标:
+ 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_localnet
。 127/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
) 中找到条目,通常类似于 <hostname>:10
。使用 Ruben 的建议在 docker 容器中添加一个可见的新条目:
xauth add 172.17.0.1:10 . <cookie>
其中<cookie>
是 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 可以吗? @Rubenxauth list $DISPLAY
- 空输出以上是关于在通过 SSH 连接的服务器上可靠地运行 Docker 容器中的 X 应用程序,而无需“--net 主机”的主要内容,如果未能解决你的问题,请参考以下文章