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

q9rjltbz  于 2023-02-07  发布在  Docker
关注(0)|答案(4)|浏览(277)

如果没有Docker容器,可以直接使用SSH X11转发在远程服务器上运行X11程序(ssh -X).我曾尝试过在服务器上的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

这是不希望的,因为它使得整个主机网络对于容器可见。
为了让它完全运行在Docker中的远程服务器上而不使用“--net host”,现在缺少了什么?

mitkmikd

mitkmikd1#

我想明白了,当你用SSH连接电脑,使用X11转发的时候,X通信不使用**/tmp/. X11-unix**,$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“或类似地址。检查“docker 0”接口的“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 作为防火墙),为www.example.com子网中的Docker容器打开此端口172.17.0.0:

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

以防止服务器上的其他用户也能够访问X服务器(如果他们知道您创建/tmp/.docker.auth文件的目的)。
我希望这能使它在大多数情况下正常工作。

6ss1mwsb

6ss1mwsb2#

如果您设置X11UseLocalhost = no,您甚至允许外部流量到达X11套接字。也就是说,定向到机器外部IP的流量可以到达SSHD X11转发。仍然有两种安全机制 * 可能 * 适用(防火墙,X11 auth)。不过,如果你在处理一个特定于用户甚至应用程序的问题,我宁愿不去处理 * 系统全局设置 *,就像这个例子一样。
这里有一个替代方法,可以从容器中获取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网络名称空间内部获取到根网络ns中的环回接口的X11流量。veth对连接到docker0网桥,因此两端都可以与www.example.com通信,而无需任何路由。根网络ns中的三个接口172.17.0.1 without any routing. The three interfaces in the root net ns ( docker0 , lo and ens33 ) can communicate via routing.
我们希望实现以下目标:

+ 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 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规则,以便从www.example.com路由到根网络ns中的www.example.com: 172.17.0.1 to 127.0.0.1 in the root net ns:

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,该编号已添加到端口"base" 6000。在建立SSH连接后,您可以通过检查SSH启动的shell中的DISPLAY环境变量来检查要使用的显示编号。
也许您可以通过只路由来自此容器的流量来改进转发规则(veth end)。而且,老实说,我不太确定为什么需要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)

当您使用X11转发建立连接时,SSHD会建立剩余的连接。请注意,您必须在尝试启动容器内的X11应用程序 * 之前 * 建立连接,因为该应用程序会立即尝试连接到X11服务器。
少了一块:我们现在尝试在容器内以172.17.0.1:10的身份访问X11服务器。但是容器没有任何X11身份验证,或者如果你绑定挂载主目录(容器外的主目录通常是<hostname>:10),则没有正确的X11身份验证。按照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桥的所有东西),因此,它适用于容器网络名称空间内的任何应用程序。

qoefvg9y

qoefvg9y3#

在我的例子中,我位于“remote”并连接到“docker_host”上的“docker_container”:
远程--〉停靠站主机--〉停靠站容器
为了使使用VScode调试脚本更容易,我将SSHD安装到“docker_container”中,报告端口22,该端口Map到“docker_host”上的另一个端口(例如1234)。
所以我可以通过ssh直接连接到运行的容器(从“远程”):
ssh -Y -p 1234 appuser@docker_host.local
(其中appuser是“docker_container”内的用户名。我现在正在我的本地子网上工作,所以我可以通过.localMap引用我的服务器。对于外部IP,只要确保您的路由器Map到这个端口到这台机器。)
这将通过ssh创建一个从我的“remote”到“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”的时候:(x 1米5英寸1 x)
重新启动sshd:sudo service ssh restart
当您通过ssh连接到“docker_container”时,请检查$DISPLAY环境变量。

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

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

zrfyljdw

zrfyljdw4#

我使用了一种自动化的方法,可以完全从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"

相关问题