【问题标题】:Run X application in a Docker container reliably on a server connected via SSH without "--net host"在通过 SSH 连接的服务器上可靠地运行 Docker 容器中的 X 应用程序,而无需“--net 主机”
【发布时间】: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 中的远程服务器上完全运行,现在缺少什么?

【问题讨论】:

    标签: docker ssh x11 x11-forwarding


    【解决方案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 - 空输出
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多