一个窗口如何跨越三台机器
这是「远程 GUI 与跳板机链路」系列的篇①。
本篇的第一性问题:我在办公机上敲一行命令,程序跑在几百公里外的 Linux 服务器上,
中间还隔着一台跳板机 —— 那个窗口是怎么跑到我面前这块屏幕上的,
以及凭什么允许它画在我的屏幕上。回链总述:本篇负责总述里的推论一(角色必须反转)和推论二(协议里必须有授权)。
这两条不是 X11 的设计偏好,是「传命令」这个选择的必然结果 ——
为什么必然,在总述的 2.3。本篇能解释故障表里的哪几行:
DISPLAY为空 /cannot open display,
以及sudo之后 GUI 打不开。全文四部分:一 角色(谁是 server)→ 二 通道(
ssh -X干了什么)→
三 授权(cookie 凭什么算数)→ 四 多跳(链路断在哪、怎么查)。
第一部分 角色:拿着屏幕的那端才是 server
1.1 先把结论摆出来
我面前这块屏幕是 X server。跑在服务器上的那个 GUI 程序是 X client。
几乎每个人第一次听到都要愣一下,因为它和「server = 机房里那台大机器」的日常直觉正好相反。
但这不是命名失误。它是「传命令」模型推出来的:命令要有人接收并执行,
谁执行?拿着显卡、键盘、鼠标的那一端。在网络编程的通用语汇里,
提供服务、被连接的那端叫 server —— 屏幕提供的是「显示服务」,程序是来消费这个服务的。
1.2 1984 年那间机房
X11 诞生在分时终端的年代:一台大机器,一屋子瘦终端。终端没有算力去做别的,
而绘图命令比位图小得多,那时候的网络也扛不住传位图。
所以有一句话值得记住:X11 从第一天起就是网络协议。
「远程显示」不是后来加的功能,是它的原生形态。你在本机用它,
只是走了 Unix domain socket 的一个特例。
这句话看着像掌故,其实是本篇后面所有内容的地基 ——
正因为协议本来就假设两端在不同机器上,所以后面那些机制(DISPLAY、cookie、转发)
才不是「补丁」,而是原本就在的东西。
1.3 同一套角色,换了一根管子
图 1 同一套角色,换了一根管子 —— 本机使用才是那个特例
左右两边的角色**完全一样**。变的只有中间那根管子:本机是 Unix domain socket, 远程是一条 SSH channel。X client 发出去的字节,两边一模一样 —— 它根本不知道自己是不是在跨网络。1.4 立刻自洽的三个现象
理解了角色反转,下面三件事就不用背了:
- 为什么
DISPLAY这个变量设在服务器上,指的却是我这边?
因为它是 X client 用来找 X server 的地址,而 X server 在我这边。 - 为什么我本地要先装一个 VcXsrv 或 X410?
因为我这台 Windows 机器上本来没有 X server,得先把这个「服务」开起来。 - 为什么关掉本地那个小图标,远端程序立刻就崩了?
因为 server 没了。client 的连接断了,它无处可画。
第二部分 通道:ssh -X 是怎么骗过 GUI 程序的
2.1 服务器那头发生的四件事
关键在于:GUI 程序只认 DISPLAY 这一个入口。它读到什么地址就连什么地址,
不会去验证那头是不是真的 X server。于是整件事的实现只需要一个动作 ——
在 DISPLAY 指的那个地址上,摆一个假的 X server。
图 2 ssh -X 做的事:在 DISPLAY 指的那个地址上,摆一个假的 X server
摊开说,你敲下 `ssh -X` 之后,服务器上的 sshd 悄悄做了四件事:- 挑一个空闲的 display 号,通常从
10开始; - 在
localhost:6010上开始监听(display 号 N 对应端口6000 + N); - 调
xauth往~/.Xauthority里写一条针对这个 display 的 cookie; - 把登录 shell 的
DISPLAY设成localhost:10.0。
之后 GUI 程序启动,读 DISPLAY,老老实实连上 localhost:6010。
sshd 收下这些字节,塞进 SSH 的一个 X11 channel,
沿已经建立好的那条连接往回走;办公机上的 ssh 客户端接住,转投给本地真正的 X server。
注意「沿已经建立好的那条连接」这半句 —— 这就是总述 3.1 讲的方向反转:
连接是我发起的(办公机 → 跳板机 → 服务器),X11 字节流却是反着回来的
(服务器 → 跳板机 → 办公机),两者共用同一条连接。
服务器从来没有主动连过我,这也是为什么办公机在 NAT 后面、没有公网 IP 也照样能用。
登上去之后,看一眼就知道这四件事做没做成:
$ echo $DISPLAY
localhost:10.0
有值就是通道搭起来了;空的就是没搭起来,后面第四部分专门讲这个。
2.2 三件套:两个开关 + 一个不起眼的命令
要让上面四件事发生,需要三样东西同时到位,缺一不可:
| 在哪 | 是什么 | 不满足会怎样 |
|---|---|---|
服务器 /etc/ssh/sshd_config |
X11Forwarding yes |
sshd 直接拒绝转发请求 |
客户端(命令行 -X 或 ssh_config 的 ForwardX11) |
打开转发 | 压根没发起请求 |
服务器上装了 xauth |
一个命令行小工具 | cookie 写不进去,转发建不起来 |
前两个是开关,好查。第三个最容易漏:xauth 通常在 xorg-x11-xauth
(或 xauth)这个包里,最小化安装的服务器镜像经常不带它。
没有它,sshd 第 3 步就做不成,DISPLAY 也就设不上 —— 而人第一反应总是去查 sshd_config。
服务器上验证一句话就够:
$ command -v xauth
/usr/bin/xauth
2.3 X11UseLocalhost:容器里跑 GUI 的那个坑
sshd_config 里还有一个开关,默认值是 yes,意思是转发出来的那个假 X server
只绑 127.0.0.1:
X11UseLocalhost yes # 只监听 127.0.0.1:6010(默认)
X11UseLocalhost no # 监听 0.0.0.0:6010
默认值是对的 —— 只绑回环意味着同一台机器上的别人也连不上你这个 display,安全。
坑在于:如果 GUI 程序不在 sshd 所在的那个网络命名空间里,
比如你 ssh 到宿主机之后在容器里跑程序,那么容器里的 127.0.0.1
是容器自己的回环,根本不是宿主机那个监听端口。
于是 DISPLAY=localhost:10.0 在容器里指向一个不存在的东西,报 cannot open display。
这个坑的价值不在于「怎么绕过去」(改成 no 是把监听暴露到全网卡,一般不该干),
而在于它验证了本篇的模型:DISPLAY 是一个地址,地址的解析发生在
X client 所处的网络环境里。想清楚这一点,这类问题就不用试错了。
2.4 怎么亲眼看见这条通道
想确认客户端确实发起了 X11 转发请求,加 -v:
$ ssh -v -X user@server-a
...
debug1: Requesting X11 forwarding with authentication spoofing.
authentication spoofing 这个词组本身就是对本节的注解 ——
ssh 自己把它做的事情叫「认证冒名」。
如果服务器那边没开转发,你会看到这一句,它是会打出来的:
X11 forwarding request failed on channel 0
如果本地 xauth 出了问题,会看到这一句:
Warning: untrusted X11 forwarding setup failed: xauth key data not generated
记住这两句长什么样,因为最难查的情况恰恰是这两句都没有 —— 第四部分会讲到。
第三部分 授权:cookie 凭什么能证明是我
3.1 为什么必须有这道门
如果谁都能连上我的 X server 发命令,那网络上任何人都能在我屏幕上画东西 ——
而且远不止画东西(篇③会展开到底能干多过分)。
所以协议里必然要有一道门。注意这个推论的方向:
cookie 不是 X11 的一个附加特性,它是「传命令」模型的必然产物。
你选了让远端给你的屏幕发指令,你就必须回答一个问题:
「凭什么相信来连我的,是我自己启动的那个程序?」
3.2 MIT-MAGIC-COOKIE-1
X11 的答案朴素得很:一个随机数。
~/.Xauthority 里按 display 分别存放着一串随机十六进制值。
程序连上来时把它出示一遍,对得上就放行,对不上就拒绝。
$ xauth list
server-a/unix:10 MIT-MAGIC-COOKIE-1 a3f2…
注意左边那个 server-a/unix:10 —— cookie 是按 display 存的,不是按用户存的。
一台机器上开了三个 ssh -X 会话,这里就会有三条记录,互不通用。
它的全部安全性来自「只有该知道的人知道」。所以两句话概括它的脾气:
泄漏 = 权限泄漏,丢失 = 你自己也进不去。
3.3 三个经典故障,同一个原因
由 3.2 那一句「按 display 存在家目录的一个文件里」,可以直接推出三个故障,不用背:
| 现象 | 为什么 |
|---|---|
sudo 之后 GUI 打不开 |
HOME 变成了 /root,去新家目录找 ~/.Xauthority,那里没有这条记录 |
| 家目录在 NFS 上,或者磁盘满了 | cookie 根本写不进去,转发建不起来 |
| 换了个终端窗口就打不开了 | 那是另一个会话、另一个 display 号,对应的是另一条记录 |
第一个最常见。它的正确解法不是「关掉 cookie」,而是把凭据带过去 ——
比如 sudo -E(保留环境变量)配合把 XAUTHORITY 指向原来的文件,
或者干脆用 xauth 把那条记录导到目标用户的家目录下。
3.4 xhost + 是拆门锁,不是修钥匙
网上搜这类报错,第一条答案往往是 xhost +。它确实能让报错消失,
所以值得说清楚它到底干了什么:
xhost + 的意思是「取消基于主机的访问控制」——
在这之后,网络上任何能连到你 display 的人,都可以连上来。
不需要 cookie,不需要任何凭据。
这不是「修好了钥匙」,是「把门锁卸了」。而上面 3.3 那三个故障,
根因全都是凭据没被正确地带到,一个都不是「锁坏了」。
要临时放行,用 xhost +SI:localuser:<用户名> 这种带限定的形式,
把范围收到一个具体用户,而不是敞开给整个网络。
第四部分 多跳:一条翻译链,断在哪都不报错
4.1 每一跳都是重新翻译,不是转交
到这里模型已经齐了,但真实链路是两跳:办公机 → 跳板机 → 服务器。
多一跳之后,很多人的心智模型是「cookie 从我这儿一路传到服务器」。不是的。
图 3 多跳不是把 cookie 转交下去,是每一跳重新翻译一次
每一跳的 sshd 都**独立地**走一遍第二部分那四件事: 自己挑 display 号、自己监听、自己调 `xauth` 生成一条**新的** cookie。 所以跳板机上的 cookie B 和我办公机上的 cookie A 没有任何关系, 服务器上的 cookie C 又是另一个。这是一条翻译链,不是一条隧道。理解这个差别,下面两件事就顺了:
- 为什么每一跳都得单独加
-X(少加一次,链子就从那里断); - 为什么两跳之后
DISPLAY还是localhost:10.0——
那个10是这一跳本地的编号,和上一跳的10毫无关系,纯属巧合地相同。
4.2 堡垒机:最容易漏掉的第四个变量
画出来是三台机器,排查的时候得按四个变量算 —— 跳板机上那套堡垒机软件本身是第四个。
很多堡垒机为了做会话审计,只放行 shell channel,其他 channel 一律丢弃。
X11 channel 就这么被吃掉了 —— 而且这一种失败,2.4 里那两句报错一句都不会出现。
从客户端看,请求发出去了,也没被拒绝,只是那条 channel 的数据进了黑洞。
你能看到的全部信息,就是 DISPLAY 是空的。
它值得单独拎出来,是因为这是排查时最容易漏掉的一环:
ssh -X 的参数、服务器的 sshd_config、本地的 X server,
三样都查过了,全对 —— 问题出在中间那台谁都没去看的机器上。
4.3 排查顺序:逐跳 echo,第一个变空的地方就是断点
既然报错不可靠,就别从报错入手。按代价从低到高:
- 在每一跳上
echo $DISPLAY。 从跳板机开始,再到服务器。
第一个变空的地方,就是断点所在的那一跳。 - 断点那一跳,确认客户端参数。 上一跳登进来的时候加
-X了吗? - 断点那一跳,
command -v xauth。 没有就是它。 - 断点那一跳,看
sshd_config的X11Forwarding。
同时ssh -v看有没有X11 forwarding request failed on channel 0。 - 前四步全对、且断点在跳板机 —— 那基本就是 4.2 那个变量,去问管堡垒机的人。
这个顺序的逻辑很简单:先定位在哪一跳,再定位是哪一环。
反过来做(先把所有配置查一遍)会把工作量乘以跳数。
4.4 可以自己反推的练习
盖住答案,先想一遍。这几个都能用本篇的模型直接推出来:
- 我在服务器上
su成另一个用户,GUI 就打不开了。为什么?
(提示:3.2 那句「cookie 按 display 存在家目录里」。) - 同一台服务器我开了两个终端,A 窗口里能开 GUI,B 窗口里不能。
两个窗口的echo $DISPLAY会一样吗? - 我在服务器上跑了个后台进程,然后退出登录。
过一会儿重新ssh -X上去,kill -CONT唤醒它,它的窗口会回来吗?为什么?
(这题的完整答案在篇③,但你现在应该已经能猜到方向了。) - 本地 X server 没开,远端执行 GUI 程序,报错会是「连接被拒绝」还是「超时」?
(提示:2.1 里 X client 连的是哪个地址。) - 如果我把
X11UseLocalhost改成no,2.3 那个容器的问题解决了吗?
代价是什么?
第 5 题值得多想一层:它解决的是地址可达性,
而它的代价是把一个只靠 cookie 保护的入口暴露到全网卡上。
这就滑到篇③要讲的话题了 —— 借出一个 display,到底等于交出了什么。

浙公网安备 33010602011771号