Wayland 无人值守远程桌面 — Sunshine 方案完整指南

环境

节点 系统 桌面 GPU 角色
家里电脑 Ubuntu 26.04 GNOME (Wayland) NVIDIA RTX 4060 Ti 被控端
雨云服务器 Ubuntu Server 无 GUI SSH 中转
公司电脑 Ubuntu GNOME 控制端

为什么选 Sunshine 而不是 GNOME RDP

GNOME RDP Sunshine
编码 CPU 软件编码,延迟高 GPU 硬编码 (NVENC/VA-API),延迟低
凭据 依赖 gnome-keyring,自动登录后丢失,每次重启需手动重设 独立存储,通过 --creds 命令行注入
锁屏 锁屏后 PipeWire 截屏管道切断,远程黑屏 PipeWire 管道不受锁屏影响
画质 软件编码,高分辨率吃力 HEVC/AV1 硬编码,2560×1440 流畅
重启表现 keyring 锁定 → 凭据失效 → 连不上 systemd + ExecStartPre 注入凭据,开机自恢复

Sunshine 核心概念(必读)

端口分工

1
2
3
4
5
6
47985  — 内部探测端口,不需要隧道
47989 — Moonlight 控制端口(TCP)
47990 — 串流 HTTPS 端口(TCP),核心
47991 — Web UI 管理端口(HTTPS,配对用)
47998-48010 — 视频/音频 RTSP 数据流(TCP,force_tcp=true 时)
48011 — RTSP 信令端口(TCP)

全部 TCP 端口(force_tcp = true),均可通过 SSH 隧道转发。

凭据存储与 503 错误

Sunshine 的 Web UI 用户名密码和 Moonlight 配对令牌都存储在 sunshine_state.json 中。

  • 如果手动删除 sunshine_state.json → 凭据丢失 → Moonlight 收到 503
  • 如果 Sunshine 进程重启时有旧进程未退出 → 端口冲突 → 部分功能失败 → 503

解决:systemd 服务中使用 ExecStartPre 在启动前(1)杀掉残留进程(2)自动注入凭据。

Systemd 竞态条件

1
2
3
4
5
6
systemd restart sunshine.service
→ 发 SIGTERM 给旧进程
→ 旧 Sunshine 正在释放 GPU、关闭端口(需要 2-3 秒)
→ 新 Sunshine 同时启动
→ 尝试绑定 48011
→ 旧进程还占着 → "地址已在使用" → 崩溃

解决ExecStartPre=/bin/bash -c 'pkill -9 sunshine 2>/dev/null; sleep 2; true'


整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────┐                 ┌──────────────────────┐                 ┌─────────────────────┐
│ 家里 GNOME │ │ 雨云服务器 │ │ 公司 GNOME │
│ │ autossh -R │ │ ssh -L │ │
│ Sunshine ◄─────────┼───────────────► │ localhost ◄──────────┼────────────────│ Moonlight │
│ 47989 47990 47991 │ SSH 隧道内 │ 11 个端口 │ SSH 隧道内 │ localhost │
│ 47998-48011 │ │ │ │ │
└─────────────────────┘ └──────────┬───────────┘ └─────────────────────┘

NAT 映射
<服务器公网IP>:<服务器SSH端口>

公网只暴露 1 个 SSH 端口。
所有 Sunshine/Moonlight 端口在 SSH 隧道内部传输,NAT 用量 = 1/10。

第一部分:家里电脑配置

以下所有操作在家里的 Ubuntu 终端中执行。


1.0 SSH 密钥免密登录(必须先做)

systemd 后台服务没有终端输入密码,必须配置密钥免密登录。

1
2
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
ssh-copy-id -p <服务器SSH端口> <服务器用户名>@<服务器公网IP>

验证:

1
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "echo ok"

看到 ok 即完成。


1.1 自动登录

Wayland 下 Sunshine 需要用户登录桌面会话后才能访问 PipeWire 截屏管道。

1
sudo vim /etc/gdm3/custom.conf

确保 [daemon] 段下只有:

1
2
3
[daemon]
AutomaticLoginEnable = true
AutomaticLogin = <本地用户名>

⚠️ 如果文件已有 [daemon] 段,手动编辑,不要用 tee -a 追加(重复段会互相覆写)。


1.2 关闭锁屏与息屏

1
2
3
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.desktop.session idle-delay 0
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing'

1.3 安装 Sunshine(Flatpak 版)

Ubuntu 26.04 与 Sunshine 24.04 deb 包 ICS 库版本不兼容(libicuuc.so.70 缺失)。Flatpak 版自带所有依赖,不受系统版本影响。

1
2
3
sudo apt install -y flatpak
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y flathub dev.lizardbyte.app.Sunshine

授予 Wayland 和 GPU 访问权限:

1
flatpak override --user --socket=wayland --device=dri dev.lizardbyte.app.Sunshine
权限 作用
--socket=wayland 访问 Wayland compositor,用于 PipeWire 截屏
--device=dri 访问 GPU 渲染节点(NVENC 编码必需)

1.4 配置 Sunshine

1
2
3
4
5
6
7
8
9
cat > ~/.var/app/dev.lizardbyte.app.Sunshine/config/sunshine/sunshine.conf << 'EOF'
adapter_name = /dev/dri/renderD128
encoder = nvenc
output_name = 0
port = 47990
upnp = disabled
force_tcp = true
platform = wayland
EOF

配置详解:

参数 为什么这样设
adapter_name /dev/dri/renderD128 Wayland 下 KMS/DRM 抓帧的渲染节点。NVIDIA 闭源驱动 + Wayland 下 KMS 模式可能失败(503),改用 PipeWire(见 platform
encoder nvenc RTX 4060 Ti 的硬件编码器,比 CPU 软件编码快 10 倍
output_name 0 使用第一个显示器
port 47990 串流默认端口,保持默认即可
upnp disabled 禁用 UPnP 端口映射。我们走 SSH 隧道,不需要路由器开洞
force_tcp true 强制 TCP 模式。SSH 隧道只转发 TCP,UDP 无法穿透。开启后所有视频/音频流走 TCP,延迟略高但可靠
platform wayland 使用 PipeWire 管道而不是 KMS/DRM。NVIDIA 闭源驱动 + Wayland + KMS 组合已知兼容性问题(”failed to initialize video capture”),PipeWire 更稳定

1.5 Sunshine systemd 服务(含凭据注入 + 竞态修复)

这是整个方案最关键的配置。systemd 服务在启动 Sunshine 之前,会自动清理残留进程并注入凭据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cat > ~/.config/systemd/user/sunshine.service << 'EOF'
[Unit]
Description=Sunshine (Flatpak) - Moonlight 串流服务
After=network-online.target
Wants=network-online.target

[Service]
# ── 启动前清理 ──
# 杀掉残留 Sunshine 进程,等 2 秒确保端口释放
# 避免 "address already in use" 竞态崩溃
ExecStartPre=/bin/bash -c 'pkill -9 sunshine 2>/dev/null; sleep 2; true'

# ── 注入凭据 ──
# 每次启动前自动设置 Web UI 用户名密码
# 解决了 sunshine_state.json 被删除后凭据丢失的问题
ExecStartPre=/usr/bin/flatpak run --command=sunshine dev.lizardbyte.app.Sunshine --creds <你的用户名> '<你的密码>'

# ── 启动 Sunshine ──
ExecStart=/usr/bin/flatpak run dev.lizardbyte.app.Sunshine
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
systemctl --user enable --now sunshine.service

# 验证
sleep 4
systemctl --user status sunshine.service | head -5
ss -tlnp | grep sunshine

预期看到 4-5 条 LISTEN 端口(47985, 47990, 47991, 48011 等)。

ExecStartPre 详解:

作用 解决的问题
pkill -9 sunshine; sleep 2 彻底清场再启动 Systemd restart 竞态:旧进程未退出时新进程启动,端口冲突崩溃
flatpak run --creds 每次启动注入凭据 删除 sunshine_state.json 重置配对后凭据丢失 → 503

1.6 SSH 反向隧道 systemd 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
sudo apt install -y autossh

mkdir -p ~/.config/systemd/user

cat > ~/.config/systemd/user/rdp-tunnel.service << 'DELIM'
[Unit]
Description=Sunshine 反向隧道到雨云服务器
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/autossh \
-M 0 -N -p <服务器SSH端口> \
-R 47989:localhost:47989 \
-R 47990:localhost:47990 \
-R 47991:localhost:47991 \
-R 47998:localhost:47998 \
-R 47999:localhost:47999 \
-R 48000:localhost:48000 \
-R 48002:localhost:48002 \
-R 48004:localhost:48004 \
-R 48006:localhost:48006 \
-R 48008:localhost:48008 \
-R 48010:localhost:48010 \
-R 48011:localhost:48011 \
-o ServerAliveInterval=30 \
-o ExitOnForwardFailure=yes \
<服务器用户名>@<服务器公网IP>
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
DELIM

systemctl --user daemon-reload
systemctl --user enable --now rdp-tunnel.service

# 验证
systemctl --user status rdp-tunnel.service | head -3

共 12 个端口,全部在 SSH 隧道内部传输,不增加 NAT 用量。


1.7 家里配置总验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. Sunshine 在跑?
systemctl --user status sunshine.service | head -3

# 2. Sunshine 端口在监听?
ss -tlnp | grep sunshine

# 3. 隧道在跑?
systemctl --user status rdp-tunnel.service | head -3

# 4. 服务器端口全部转发成功?(预期 10+ 条)
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "ss -tlnp | grep 127.0.0.1 | grep -c 47"

# 5. 无人值守重启测试
sudo reboot
# 等 60 秒后从另一台机器 SSH 到服务器验证隧道是否自动恢复

第二部分:服务器配置


2.1 SSH 心跳保活(必须配)

家里电脑异常断电时,TCP 连接不会立刻断开,SSH 子进程变成僵尸占着端口。心跳保活确保 3 分钟内自动回收。

1
2
3
4
5
6
7
8
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "sudo tee -a /etc/ssh/sshd_config" << 'EOF'

# Sunshine 隧道心跳保活
ClientAliveInterval 60
ClientAliveCountMax 3
EOF

ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "sudo systemctl restart sshd"

每 60 秒发心跳,连续 3 次无响应 = 180 秒后断开并释放端口。


2.2 断线恢复全景

场景 家里 服务器 公司 恢复
家里网络抖动(< 30s) 心跳保活,不丢连接 无感知 无感知 ✅ 自动
家里宽带断(> 3min) autossh 断开,10s 后重试 心跳超时,3min 后端口释放 Moonlight 断开 ✅ 网络恢复后 autossh 自动重连,公司重开 Moonlight
家里断电/重启 systemd 同时重启 sunshine + autossh 心跳超时释放端口 Moonlight 断开 ✅ 来电后全自动恢复(凭据由 ExecStartPre 注入)
服务器重启 ssh 全断,autossh 10s 重试 sshd 重新拉起 Moonlight 断开 ✅ 服务器 sshd 启动后 autossh 自动重连
公司网络断开 家里↔服务器 链路正常 隧道正常 Moonlight 断开 🔧 公司网络恢复后重开 ssh -L + Moonlight

2.3 端口监控脚本(可选)

1
2
3
4
5
6
7
8
9
10
11
12
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "sudo tee /usr/local/bin/check-tunnel.sh" << 'SCRIPT'
#!/bin/bash
EXPECTED=10
ACTUAL=$(ss -tlnp | grep -c '127.0.0.1:47\|127.0.0.1:48')
if [ "$ACTUAL" -lt "$EXPECTED" ]; then
echo "[$(date)] WARNING: Tunnel ports degraded: $ACTUAL/$EXPECTED" >> /var/log/tunnel-monitor.log
fi
SCRIPT

ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "sudo chmod +x /usr/local/bin/check-tunnel.sh"
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> \
"(crontab -l 2>/dev/null; echo '* * * * * /usr/local/bin/check-tunnel.sh') | crontab -"

登录服务器后 cat /var/log/tunnel-monitor.log 查看历史异常。


2.4 手动清理残留僵尸

1
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "pkill -f 'sshd.*:47\|sshd.*:48'"

配了心跳保活(2.1)后通常不需要手动操作。


第三部分:公司电脑配置


3.1 安装 Moonlight

1
sudo apt install -y moonlight-qt

3.2 首次配对(只需做一次)

① 确认家里就绪:

1
2
ssh -p <服务器SSH端口> <服务器用户名>@<服务器公网IP> "ss -tlnp | grep -c 47990"
# 预期: 1(说明隧道通了)

② 开正向隧道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssh -N -p <服务器SSH端口> \
-L 47989:127.0.0.1:47989 \
-L 47990:127.0.0.1:47990 \
-L 47991:127.0.0.1:47991 \
-L 47998:127.0.0.1:47998 \
-L 47999:127.0.0.1:47999 \
-L 48000:127.0.0.1:48000 \
-L 48002:127.0.0.1:48002 \
-L 48004:127.0.0.1:48004 \
-L 48006:127.0.0.1:48006 \
-L 48008:127.0.0.1:48008 \
-L 48010:127.0.0.1:48010 \
-L 48011:127.0.0.1:48011 \
<服务器用户名>@<服务器公网IP>

命令会”卡住”——这是正常的,-N 不返回 shell,隧道已建立。保持这个终端窗口开着。

③ 打开 Moonlight:

1
moonlight-qt &

④ 添加主机:

  • 点左上角 + → 地址填 127.0.0.1 → 确定
  • Moonlight 弹出一个 4 位 PIN 码

⑤ 确认配对:

  • 浏览器打开 https://127.0.0.1:47991
  • 首次访问提示证书不安全 → 点「高级」→「继续访问」
  • 登录(1.5 中 --creds 设置的用户名和密码)
  • 点顶部 PIN 标签 → 输入 Moonlight 显示的 4 位 PIN → 点 Send
  • Moonlight 显示配对成功,出现家里电脑桌面图标

⚠️ 注意方向:是 Moonlight → 弹出 PIN → Sunshine Web UI 填入,不是反过来。

⑥ 双击电脑图标 → 连接成功。


3.3 后续每次连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 开隧道
ssh -N -p <服务器SSH端口> \
-L 47989:127.0.0.1:47989 -L 47990:127.0.0.1:47990 \
-L 47991:127.0.0.1:47991 -L 47998:127.0.0.1:47998 \
-L 47999:127.0.0.1:47999 -L 48000:127.0.0.1:48000 \
-L 48002:127.0.0.1:48002 -L 48004:127.0.0.1:48004 \
-L 48006:127.0.0.1:48006 -L 48008:127.0.0.1:48008 \
-L 48010:127.0.0.1:48010 -L 48011:127.0.0.1:48011 \
<服务器用户名>@<服务器公网IP>

# 2. 打开 Moonlight
moonlight-qt &

# 3. 双击家里电脑图标连接

3.4 Moonlight 推荐设置

设置项 位置
视频编解码器 HEVC 高级设置
视频解码器 硬件解码 高级设置
自动检测被阻止的连接 ✅ 勾选 高级设置

第四部分:完整开机链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
开机卡通电
→ BIOS POST → GRUB
→ GDM 自动登录 (<本地用户名>)
→ GNOME Wayland 桌面会话
├── gsettings: 锁屏=off, 息屏=off

┌─── sunshine.service 启动 ───┐
│ ① ExecStartPre: pkill -9 sunshine; sleep 2 ← 清场
│ ② ExecStartPre: --creds <用户> '<密码>' ← 注入凭据
│ ③ ExecStart: flatpak run Sunshine ← 启动
│ ├── PipeWire 管道连接 (platform=wayland)
│ ├── NVENC 编码器就绪 (encoder=nvenc)
│ ├── force_tcp=true (纯 TCP 模式)
│ └── 监听 47985,47989-48011
└──────────────────────────────┘

┌─── rdp-tunnel.service 启动 ──┐
│ autossh -R 12个端口 → 服务器(SSH 密钥免密)
└──────────────────────────────┘

→ 服务器 localhost 端口就位
→ ✅ 公司随时可连

第五部分:故障排查


症状:Moonlight 报 503 错误

503 = Sunshine 服务端无法初始化视频流。三种原因:

原因 日志特征 解决
凭据丢失 Open the Web UI to set your new username and password systemd 中加了 ExecStartPre --creds,重启即可
端口冲突 Couldn't bind RTSP server to port [48011] systemd 中加了 pkill -9; sleep 2,重启即可
视频初始化失败 failed to initialize video capture/encoding 检查 nvidia-smi、确认 platform = wayland、确认显示器电源开着
1
2
3
4
# 快速修复:重启 Sunshine(systemd 会自动清理+注入凭据)
systemctl --user restart sunshine.service
sleep 4
systemctl --user status sunshine.service | head -3

症状:Sunshine 不监听端口 / 无限重启

1
2
# 查看 Sunshine 自身日志(不是 systemd 日志)
tail -50 ~/.var/app/dev.lizardbyte.app.Sunshine/config/sunshine/sunshine.log

常见原因:

日志 解决
Couldn't bind RTSP server to port [48011], 地址已在使用 残留进程占端口 → pkill -9 sunshine; sleep 2; systemctl --user restart sunshine.service
libicuuc.so.70: cannot open shared object file 用了 deb 包不是 Flatpak → 卸载 deb,重装 Flatpak 版
NVENC not found NVIDIA 驱动问题 → nvidia-smi 检查

症状:隧道无限重启 (activating auto-restart)

1
journalctl --user -u rdp-tunnel.service -n 20 --no-pager
日志 原因 解决
Permission denied (publickey,password) SSH 密钥未配置 重做 1.0 的 ssh-copy-id
port forwarding failed 服务器上残留旧隧道 ssh ... "pkill -f 'sshd.*:479|sshd.*:48'"

症状:连上但黑屏 / 画面静止

  1. 家里显示器是否开着电源?(Wayland + PipeWire 需要显示器在活动状态)
  2. 确认编码器:grep encoder ~/.var/app/dev.lizardbyte.app.Sunshine/config/sunshine/sunshine.conf
  3. 调高码率:https://127.0.0.1:47991 → Configuration → Video → Bitrate: 15000-25000 kbps

症状:Moonlight 提示端口被阻止(47998/47999 等)

公司 SSH 隧道没开全。重新执行完整的 12 端口命令(见 3.3)。


附录:命令速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# ═══════════ 家里 ═══════════

# Sunshine 管理
systemctl --user status sunshine.service
systemctl --user restart sunshine.service
journalctl --user -u sunshine.service -f # systemd 日志
tail -50 ~/.var/app/dev.lizardbyte.app.Sunshine/config/sunshine/sunshine.log # Sunshine 自身日志

# 隧道管理
systemctl --user status rdp-tunnel.service
systemctl --user restart rdp-tunnel.service

# 端到端验证
ss -tlnp | grep sunshine
ssh -p <端口> <用户>@<IP> "ss -tlnp | grep 127.0.0.1 | grep 47"


# ═══════════ 公司 ═══════════

# 完整隧道(12 端口,复制粘贴即可,替换 <端口> <用户> <IP>)
ssh -N -p <端口> \
-L 47989:127.0.0.1:47989 -L 47990:127.0.0.1:47990 \
-L 47991:127.0.0.1:47991 -L 47998:127.0.0.1:47998 \
-L 47999:127.0.0.1:47999 -L 48000:127.0.0.1:48000 \
-L 48002:127.0.0.1:48002 -L 48004:127.0.0.1:48004 \
-L 48006:127.0.0.1:48006 -L 48008:127.0.0.1:48008 \
-L 48010:127.0.0.1:48010 -L 48011:127.0.0.1:48011 \
<用户>@<IP>

moonlight-qt & # Moonlight
https://127.0.0.1:47991 # Sunshine Web UI


# ═══════════ 服务器 ═══════════
ss -tlnp | grep 127.0.0.1 | grep 47 # 确认端口转发
pkill -f 'sshd.*:47\|sshd.*:48' # 清理残留隧道
cat /var/log/tunnel-monitor.log # 隧道监控日志

最后更新:2025-07-11
适用:Ubuntu 26.04 / GNOME Wayland / NVIDIA NVENC / Sunshine Flatpak
踩坑:NVIDIA + Wayland KMS 不兼容、systemd 竞态条件、sunshine_state.json 凭据耦合