在通过SSH连接的服务器上可靠地在Docker容器中运行X应用程序而不使用“--net host”

时间:2018-01-12 22:43:34

标签: docker ssh x11 x11-forwarding

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

Error: GDK_BACKEND does not match available displays

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

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 host”的docker中的远程服务器上运行,现在缺少什么?

3 个答案:

答案 0 :(得分:16)

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

任何X应用程序都使用$ DISPLAY中的主机名,通常是" localhost"并使用TCP连接。然后将其隧道传送回SSH客户端。使用" - net host"对于Docker," localhost"对于Docker容器和Docker主机是相同的,因此它可以正常工作。

当未指定" - net host"时,Docker正在使用默认的网桥模式。 这意味着" localhost"表示容器内的其他东西比主机,并且容器内的X应用程序将无法通过引用" localhost"来查看X服务器。所以为了解决这个问题,我们必须更换" localhost"使用主机的实际IP地址。这通常是" 172.17.0.1"或类似的。检查" ip addr"对于" docker0"接口

这可以通过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

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

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服务器。

我希望这可以使它适用于大多数情况。

答案 1 :(得分:1)

对于我来说,我坐在“远程”并连接到“ 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。您应该从从“ 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())

答案 2 :(得分:0)

如果设置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。我们需要将X11流量从docker网络名称空间内部传递到根网络ns中的回送接口。第ve对连接到docker0网桥,因此两端都可以与172.17.0.1通话,而无需任何路由。根网络ns中的三个接口(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 >------- 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)

现在,我们添加一个iptables规则以在根网ns中从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

也许您可以通过仅路由来自此容器(第7端)的流量来对此进行改进。另外,老实说,我不太确定为什么需要route_localnet。看来127/8是数据包的一个奇怪的源/目的地,因此默认情况下禁用路由。您可能还可以将流量从docker net ns内部的loopback接口重新路由到veth对,再从那里路由到root net ns中的loopback接口。

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

                                           + 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。根据鲁宾的建议,在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网桥的所有内容)。因此,它将适用于容器网络名称空间内的任何应用程序。