SSH X11 Forwarding 远程开发方案

和一些刨根问底

我有一个老旧的 Windows10 笔记本,它的性能不足以支持2025年的开发需要,但我偶尔需要在离开家里那台强力主机的情况下(比如在老家或办公室时)运行开发环境。于是我买了一台强力的无头主机(mechrevo imini pro, R78845H, 32G RAM)并安装了Debian(注意,安装包括Gnome图形界面,这可能无意间帮我节省了一些安装 X11 转发所必需的依赖库的步骤),用一根外置网卡(绿联USB3.0外置网卡2.5G网速 )把笔记本和无头主机连了起来,并建立了以笔记本为网关和DHCP服务器的局域网。关于如何折腾建立这网络,内容多到值得另写一篇博客,本文按下不表。这样一来,我便可以使用JetBrains IntelliJ中自带的JetBrains Gateway(通过 ssh)连接到无头主机(下称服务器)的 IntelliJ 后端,进行 Java 开发。

一切都很好,直到我编写 minecraft-access 这个 Minecraft mod。Minecraft 是个游戏,而且这个 mod 是客户端 mod,因此调试运行 mod 需要运行游戏,而这游戏需要……打开一个窗口。服务器没有外接显示器,如果直接通过 JB Gateway 在服务器中调试运行程序,程序会抛出以下异常并退出:

java.lang.IllegalStateException: Failed to initialize GLFW, errors: GLFW error during init: [0x1000E]Failed to detect any supported platform
    at knot//com.mojang.blaze3d.platform.GLX._initGlfw(GLX.java:78)
    at knot//com.mojang.blaze3d.systems.RenderSystem.initBackendSystem(RenderSystem.java:460)
    at knot//net.minecraft.client.MinecraftClient.<init>(MinecraftClient.java:501)
    at knot//net.minecraft.client.main.Main.main(Main.java:250)

GLFW 是一个 OpenGL 工具库,Minecarft 使用 LWJGL (Lightweight Java Game Library) 处理其界面、音效、输入,而这个库的底层应用正是 GLFW。搜索后得知,该报错是指 GLFW 找不到一个可匹配的窗口系统

X11/X Window 正是一个窗口系统,它能解决我的问题。经过搜索(这篇文章写得不错)我了解到 X11 协议的术语含义与我们所熟知的概念是相反的:X client 是渲染界面且支持与 X server 通信的程序。在这个场景中,X client 需要运行在服务器里。X server 是运行在连接了输入输出设备的电脑上,并向 X client 转发输入数据、接收界面数据的程序。在这个场景中,它需要运行在我的老笔记本上。虽然界面的渲染由 X client 承担,但渲染好的界面数据仍需要 X server 将其展示在显示器上。

另外值得一提的是,X11 方案在设计之初没有充分考虑安全性,因此现有的安全方案便是通过万能的 ssh 转发 X11 流量。《UNIX and Linux System Administration Handbook, 4th Edition》书中提到:“通过 ssh 转发的 X11 界面并不流畅,即使在局域网中也是,如果存在网络延迟,情况会变得更糟”。它是对的,虽然我最终找到了成功方案,但游戏界面比较卡顿,我不得不缩小窗口来减少延迟,称得上“勉强能用”,组合按键和双击等复杂操作比较难触发。

#TLDR

考虑到最终效果并不好,我不建议你使用这套 X11 Forwarding 方案,按照这个讨论试试 VNC 或者老版本(未收费)的 Nomachine’s NX 吧。不过如果你坚持,这是所有步骤:

  1. 服务器 /etc/ssh/sshd_config 中配置 X11Forwarding 的值为 yessudo systemctl restart ssh 重启 sshd 进程。
  2. 笔记本用户 C:\Users\<user>\.ssh\config(Windows)或 ~/.ssh/ssh_config(Linux)中做以下配置并重启电脑或 ssh 服务。
  3. 笔记本安装 MobaXterm,打开 MobaXterm 并连接到服务器,保持这个连接,检查该连接的 DISPLAY 环境变量的值,不出意外的话为 localhost:10.0
  4. 笔记本使用 JB Gateway 连接服务器上的 IntelliJ 后端,在项目的 Run/Debug Configuration 中增加环境变量 DISPLAY=<MobaXterm 连接的 DISPLAY 值>
  5. 使用 IntelliJ 调试运行程序,界面应显示在笔记本侧。
1
2
3
4
5
6
7
# /etc/ssh/sshd_config in server
X11Forwarding yes

# C:\Users\<user>\.ssh\config in laptop
Host <remote domain/ip>
    HostName <remote domain/ip>
    ForwardX11 yes

#服务器端

/etc/ssh/sshd_config 中配置 X11Forwarding 的值为 yes,同时保持 X11UseLocalhostX11DisplayOffset为默认值,根据手册了解到,以下配置的作用是:

1
2
3
X11Forwarding yes # 允许 sshd 转发 X11 流量
# X11UseLocalhost yes # sshd 将把 X server 的流量出口绑定到 localhost
# X11DisplayOffset 10 # 设定 display number 为10

这些配置使得 sshd 进程会将通过 ssh 连接的远程 X server 的流量转发到 localhost:10.0。在服务器上运行的程序,若环境变量配置为 DISPLAY=localhost:10.0,则可以接收到 sshd 进程转发的 X server 的数据。关于什么是 DISPLAY 环境变量和 display number,见 https://askubuntu.com/a/432257https://linux.die.net/man/7/x。配置完记得 sudo systemctl restart ssh 重启一下 sshd 进程。

#笔记本端

Windows系统没有自带的 X server,现有方案有 XmingVcXsrvMobaXterm,我选择了 MobaXterm。安装启动后,该程序将自动架设 X server,地址为 <每个网络适配器的IP>:0.0

#cmd

先用 Windows cmd 试试,-v 打印 debug 日志,-X 允许经过鉴权的 X11 转发-b 指定要连接的网卡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
C:\>ssh -v -X -b <外置网卡对应网络适配器的IP> -p <ssh port> user@remote
OpenSSH_for_Windows_9.5p1, LibreSSL 3.8.2
debug1: ...
...
Enter passphrase for key 'C:\Users\winuser/.ssh/pri_key':
Authenticated to remote ([服务器IP]:port) using "publickey".
...
debug1: Remote: /home/user/.ssh/authorized_keys:2: key options: agent-forwarding port-forwarding pty user-rc x11-forwarding
debug1: Remote: /home/user/.ssh/authorized_keys:2: key options: agent-forwarding port-forwarding pty user-rc x11-forwarding
debug1: X11 forwarding requested but DISPLAY not set
Linux remote 6.1.0-30-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.124-1 (2025-01-12) x86_64
Last login: ...

user in remote in ~
% xeyes
Error: Can't open display:

user in remote in ~
1 % echo $DISPLAY

user in remote in ~
%

失败了,发现这个远程 shell 没有设置 DISPLAY 环境变量,那我们手动设置一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
user in remote in ~
% export DISPLAY=localhost:10.0

user in remote in ~
% echo $DISPLAY
localhost:10.0

user in remote in ~
% xeyes
Error: Can't open display: localhost:10.0

还是不行,搜索后发现,是 X server,即笔记本这边的 cmd 没有配置 DISPLAY 环境变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
C:\>set DISPLAY=localhost:0.0
C:\>echo %DISPLAY%
localhost:0.0
C:\>ssh ...
...
debug1: No xauth program.
Warning: untrusted X11 forwarding setup failed: xauth key data not generated
...
(export DISPLAY=localhost:10.0)
...
user in remote in ~
% xeyes
Error: Can't open display: localhost:10.0

仍然不行,但是出现了 “debug1: No xauth program” 这行日志,说明我们至少向前一步了。搜索得知, xauth 程序需要存在于 X server 这一端, cmd 里没有也没办法自行安装该程序。还好 ssh 还提供了 -Y 参数,允许未经鉴权的 X11 转发(注意,这对笔记本侧来说有安全风险):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
C:\>ssh -v -Y ...
...
debug1: No xauth program.
**Warning: No xauth data; using fake authentication data for X11 forwarding.**
...
user in remote in ~
% xeyes
debug1: client_input_channel_open: ctype x11 rchan 2 win 65536 max 16384
debug1: client_request_x11: request from 127.0.0.1 41424
debug1: channel 1: new x11 [x11] (inactive timeout: 0)
debug1: confirm x11

成功了!

A screenshot showing the xeyes window and the cmd window

#MobaXterm

在 MobaXterm 的本地终端上检查 DISPLAY 环境变量,发现其值已经设置到正确的值:

1
2
/home/mobaxterm> echo $DISPLAY
127.0.0.1:0.0

通过 MobaXterm ssh 连接到服务器,发现 DISPLAY 也已设置,xeyes 运行成功:

1
2
3
4
5
6
% echo $DISPLAY
localhost:11.0

user in remote in ~
% xeyes
(生成界面)

注意这里 shell 中的 DISPLAY 变量的 display number 被设置为11,这是因为我的 cmd 终端还没退出 ssh,退出后再重新通过 MobaXterm 连接服务器,DISPLAY 会变回 localhost:10.0,这就是 sshd_configX11DisplayOffset 配置项的作用,偏移从10开始。不过 MobaXterm 的本地终端上并没有安装 xauth (xauth: command not found),不清楚其内部是如何实现 X11 认证的。

#IntelliJ (over JB Gateway)

我找不到修改 JB Gateway 的 ssh 以传递 -X -Y 参数的方法,还好可以通过修改 ssh 配置的方式达到相同目的,修改 C:\Users\<user>\.ssh\config 文件如下,并重启电脑(重启 ssh 服务也可以):

1
2
3
Host <remote domain/ip>
    HostName <remote domain/ip>
    ForwardX11 yes

使用 JB Gateway 连接服务器上的 IntelliJ 后端,在项目的 Run/Debug Configuration 中增加环境变量 DISPLAY=localhost:10.0(它将在服务器上生效),点击运行并忐忑等待——失败了,出现了和文章开头一样的 GLFW 错误。在 IntelliJ 内的终端(连接到服务器)配置 DISPLAY=localhost:10.0 并运行 xeyes 也失败了。

根据上面的尝试,我们可以判断大概是 IntelliJ 这边没有配置 DISPLAY 环境变量,来试试:

1
C:\Program Files\JetBrains\IntelliJ IDEA 2024.2.3\bin>set DISPLAY=localhost:0.0 && idea64.exe

不行,相同的错误。经过尝试,我发现了一个不知道是否算作 workaround 的方法——借助已证明成功转发 X11 的 MobaXterm ssh 连接展示界面。用 MobaXterm 打开并保持到服务器的连接,将其 DISPLAY 环境变量的值(如localhost:10.0)配置到 Run/Debug Configuration 上,调试运行……成功了!

A screenshot showing the MobaXterm, Minecraft game window and IntelliJ run/debug configuration

就是这样,没什么要说的了,感谢阅读,希望这篇文章能帮到你。如果你知道更好的方案,请给我留言,如果我忍耐不了这方案的延迟,会尝试换方案。

updatedupdated2025-01-222025-01-22