在通过SSH连接的服务器上,可靠地在Docker容器中运行X应用程序,而不需要使用"--net host"选项。

44

如果没有 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

然而,只有在docker命令中添加--net host才能使其正常工作:

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

使用"--net host"会使容器能够看到整个主机网络,这并不理想。

如果不使用"--net host",为了完全在远程Docker服务器上运行它,还需要什么?

4个回答

58

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

任何X应用程序都会使用$DISPLAY中的主机名(通常为“localhost”)并使用TCP进行连接。然后将其隧道返回到SSH客户端。在使用Docker的“--net host”时,“localhost”对于Docker容器和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 服务器,为了防止这种情况发生,需要采取措施。

我希望这样做可以使大多数场景下工作正常。


3
可以使用更易于理解的命令替代含有 "xauth nlist" 的晦涩命令: xauth -f /tmp/.docker.xauth add 172.17.0.1:$X11PORT . $MAGIC_COOKIE其中 $MAGIC_COOKIE 可以通过以下命令找到:xauth list $DISPLAY | awk '{print $3}' - rubund
当在Docker中使用“--net host”时,“localhost”对于Docker容器和Docker主机来说是相同的,因此它可以正常工作。但是对于我来说并不是这样,我使用了--net host,但仍然出现以下错误:X11: Failed to open display localhost:11.0 - agirault

4
如果您设定 X11UseLocalhost = no,那么您允许甚至是外部流量到达X11 socket。也就是说,指向该主机的外部IP的流量可以到达SSHD X11转发。尽管还有两个安全机制可能适用(防火墙、X11认证),但如果您正在处理用户或甚至是应用程序特定问题的情况下,我更喜欢保留系统全局设置而不进行调整。
以下是一种替代方法,可以在不更改sshd配置中的X11UseLocalhost的情况下从容器获取X11图形,并通过X11转发将其从服务器传输到客户端。
                                           + 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流量传递到根网络命名空间中的环回接口。veth对连接到docker0桥,因此双端都可以直接与172.17.0.1通信,无需进行路由。根网络命名空间中的三个接口(docker0loens33)可以通过路由相互通信。
我们想要实现以下目标:
                                           + 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 >--ssh x11 fwd-+
           (loopback)        |
                             v
           192.168.1.2       |
<-- ssh -- ens33 ------<-----+
           (physical host interface)

我们可以让X11应用程序直接与172.17.0.1通信,以“逃脱”Docker网络命名空间。这可以通过适当设置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)

现在,我们在主机上添加一个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

注意我们使用的端口是 6010,这是SSHD执行X11转发的默认端口:它使用显示器编号10,该编号加到了“基础”端口6000上。在建立SSH连接后,您可以通过检查由SSH启动的shell中的DISPLAY环境变量来检查要使用的显示器编号。
也许您可以通过仅路由容器(veth端)中的流量来改进转发规则。另外,我不太确定为什么需要使用route_localnet,说实话。因为127/8是一个奇怪的数据包源/目的地,默认情况下被禁用路由。您可能还可以将docker net ns内的环回接口的流量重新路由到veth对,再从那里路由到root net 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)

当您启用X11转发时,剩余的连接由SSHD建立。请注意,在容器内尝试启动X11应用程序之前,必须先建立连接,因为该应用程序会立即尝试访问X11服务器。

还有一件事情需要注意:身份验证。我们现在正在尝试作为容器内的172.17.0.1:10访问X11服务器。然而,容器中没有任何X11身份验证,或者如果您绑定挂载主目录(在容器外通常是<hostname>:10)则不正确。使用Ruben的建议添加一个在docker容器内可见的新条目:

# inside container
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桥接器的所有内容)。因此,它将适用于容器网络命名空间中的任何应用程序。


这个解决方案对任何人有用吗?我无法在Ubuntu 22.04上使其工作。使用“route_localnet”的DNAT解决方案不起作用。 - rustyx
嗨@rustyx,我刚刚验证了它在Ubuntu 22.04上可以工作(在容器内外都使用)。你的症状是什么? - dyp

2
在我的情况下,我坐在“远程”位置,并连接到“docker_host”上的“docker_container”:
远程 --> docker_host --> docker_container 为了让使用VScode调试脚本更加容易,我在“docker_container”中安装了SSHD,报告端口22,映射到“docker_host”的另一个端口(比如1234)。
因此,我可以直接通过ssh(从“远程”)连接到正在运行的容器:
ssh -Y -p 1234 appuser@docker_host.local
(其中“appuser”是“docker_container”内的用户名。我现在正在本地子网上工作,因此可以通过.local映射引用我的服务器。对于外部IP,请确保您的路由器将此端口映射到此机器。)
这将创建一个直接从我的“远程”到“docker_container”的ssh连接。
在“docker_container”内部,我使用sudo apt-get install openssh-server安装了sshd(您可以将其添加到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”,那么我可以通过本地主机端口6080连接的x11服务器中看到“xeye”弹出,而客户端为noVNC。因此,我的问题是,如何在我的GUI应用程序中呈现“xeye”的相同结果?我的GUI应用程序是“Jupyter lab”。 - change198
我有点困惑。jupyter lab不是一个基于Web的平台吗?这意味着您实际上是在本地原生系统的浏览器中查看Jupyter lab的结果。容器可能正在运行Jupyter应用程序,但您对此(GUI)的真正观察是在本地显示器上(无论是VNC、VM还是物理显示器)。如果是这种情况,那么连接将必须从VNC查看容器/VM/本地计算机到您的容器,后者正在转发X11显示。ssh -Y函数将把显示地址定向到它所调用的平台。 - RexBarker
谢谢您的回复。是的,您说得对,Jupyter Lab是基于Web的,但我想要运行的应用程序与Jupyter框架不兼容。因此,我创建了Xserver,并通过noVNC在jupyterlab和X-server之间建立了链接。因此,每当我在我的jupyterlab上编写X-server-app时,它会自动在X-server中运行该应用程序。但我的问题是,既然所有的库和一切都存在,那么我如何在jupyterlab本身上弹出输出,而不是在X-server中打开结果呢? - change198
@RexBarker,我已经设置了DISPLAY环境变量。 sshuser@9a64d08b9764:/Volumes/Workspace/work/csroot/private/sensor$ echo $DISPLAY 9a64d08b9764:10.0 但是当我尝试执行我的UI应用程序时,出现以下错误: QStandardPaths:XDG_RUNTIME_DIR未设置,默认为'/tmp/runtime-sshuser' qt.qpa.gl:QXcbConnection:无法初始化GLX X11连接中断:没有错误(代码0) XIO:在X服务器“9a64d08b9764:10.0”上发生致命的IO错误2(没有此文件或目录) 在352个请求(已知处理的352个)之后,事件数为0。 - Vinay Tiwary

0

我使用自动化方法,可以完全在Docker容器内执行。

所需的只是将DISPLAY变量传递给容器,并挂载.Xauthority。 此外,它仅使用DISPLAY变量中的端口,因此它也适用于DISPLAY=localhost:XY.Z的情况。

创建一个名为source-me.sh的文件,其内容如下:

# Find the containers address in /etc/hosts
CONTAINER_IP=$(grep $(hostname) /etc/hosts | awk '{ print $1 }')
# Assume the docker-host IP only differs in the last byte
SUBNET=$(echo $CONTAINER_IP | sed 's/\.[^\.]$//')
DOCKER_HOST_IP=${SUBNET}.1

# Get the port from the DISPLAY variable
DISPLAY_PORT=$(echo $DISPLAY | sed 's/.*://'  | sed 's/\..*//')
# Create the correct display-name
export DISPLAY=$DOCKER_HOST_IP:$DISPLAY_PORT

# Find an existing xauth entry for the same port (DISPLAY_PORT), 
# and copy everything except the dispay-name
# filtering out entries containing /unix: which correspond to "same-machine" connections
ENTRY=$(xauth -n list | grep -v '/unix\:' | grep "\:${DISPLAY_PORT}" | head -n 1 | sed 's/^[^ ]* *//')
# Prepend our display-name
ENTRY="$DOCKER_HOST_IP:$DISPLAY_PORT $ENTRY"
# Add the new xauth entry. 
# Because our .Xauthority file is mounted, a new file 
# named ${HOME}/.Xauthority-n will be created, and a warning 
# is printed on std-err 
xauth add $ENTRY 2> /dev/null
# replace the content of ${HOME}/.Xauthority with that of ${HOME}/.Xauthority-n
# without creating a new i-node.
cat ${HOME}/.Xauthority-n > ${HOME}/.Xauthority

创建以下Dockerfile进行测试:
FROM ubuntu
RUN apt-get update
RUN apt-get install -y xauth
COPY source-me.sh /root/
RUN cat /root/source-me.sh >> /root/.bashrc
 
# xeyes for testing:
RUN apt-get install -y x11-apps

构建和运行:

docker build -t test-x .
docker run -ti \
    -v $HOME/.Xauthority:/root/.Xauthority:rw \
    -e DISPLAY=$DISPLAY \
    test-x \
    bash

在容器内运行以下命令:
xeyes

要以非交互方式运行,必须确保已启动 source-me.sh

docker run \
    -v $HOME/.Xauthority:/root/.Xauthority:rw \
    -e DISPLAY=$DISPLAY \
    test-x \
    bash -c "source source-me.sh ; xeyes"

对我来说,$HOME/.Xauthority 是一个目录...docker run -ti \ -v $HOME/.Xauthority:/root/.Xauthority:rw \并通过 cat 重写:# 不创建新的 i-node。 cat ${HOME}/.Xauthority-n > ${HOME}/.Xauthority没有效果,但是会出现错误 :-( - pavlovma007
请检查您的主机上是否存在$HOME/.Xauthority文件,而不是目录。如果不存在,Docker将假定您正在挂载一个目录,并为您创建它。 - ognum
出现以下错误:cat: /root/.Xauthority-n: 没有那个文件或目录 在bash中尝试运行xeyes时,出现以下错误: 错误:无法打开显示:10.4.0.32.1:10 - maximusg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接