我需要从我的工作电脑连回家里的机器。这台工作电脑锁死了能装什么软件、能访问什么网络,所以唯一现实的入口就是一个能在浏览器里跑的网页客户端,而且必须是我能免费自建的那种。RustDesk 好用,但它的网页客户端前几年挪到了付费套餐后面,免费版有几个限制让人没法忍。国内的替代品更糟。RayLink 和 ToDesk 的网页客户端都要订阅,而且报价是那种看到第二个零就关掉的报价。企业版什么都有,但你不允许买一个席位。
所以我就找一款能自建、又有正经网页客户端的工具。HopToDesk 满足这两条。它是 RustDesk 的一个 fork,服务器端更简单(只有 HopSignal 一个二进制),网页客户端可以自己托管,桌面客户端照常指向自己的 rendezvous 服务器。没有 key,没有账号,不会向第三方上传遥测。
这篇是我折腾了一个周末之后最终留下的配置。
HopToDesk 到底是什么
HopToDesk 是从前几个版本从 RustDesk fork 出来的。桌面客户端长得还是 RustDesk 的样子,协议形态也一样,线格式没怎么改。对自建用户来说最大的区别在服务器端。RustDesk 的 hbbs / hbbr 双进程在 HopToDesk 这里被拆成了两块原生桌面端要用的角色,再加上一块只有网页客户端才需要的:HopSignal 这个二进制只负责 rendezvous(让两端互相找到对方),coturn TURN 服务器管原生桌面客户端点对点打不通时的中继。网页客户端那一侧要再加第三块——一个自定义的中继桥——因为网页客户端不说 TURN 也不做 ICE,而 coturn 怎么调都吐不出网页客户端真正在说的那个协议。coturn 是独立的第三方项目,跟 HopToDesk 没有从属关系。中继桥同样不是 HopToDesk 提供的,是我自己让 AI 写出来的,也是官方自建指南里缺失的那一块。
安装 HopSignal
服务器是一个二进制。在 Linux 上:
curl -L -o /tmp/hopsignal https://github.com/hoptodesk/HopSignal/releases/latest/download/hopsignal-linux-x86_64
chmod +x /tmp/hopsignal
sudo mv /tmp/hopsignal /usr/local/bin/hopsignal
1.0.0 版本验证可用的参数:
--ADDR 监听地址 (默认 :21116)
--LOGFILE 日志文件路径
--FEDLIST 联邦节点列表
--FEDADDR 联邦节点地址
--FEDPWD 联邦节点共享密钥
单机自用只需要 --ADDR。我用一个 systemd 单元把它跑起来。信号用的 WebSocket 端点内部监听 9000 端口。relay 这一块是另一回事:装的是 coturn,TURN 协议由它处理,跟 HopSignal 没有任何关系。HopSignal 本身保持无状态:不要数据库,不要共享卷,别的什么都不用。
加上 coturn(只管原生客户端)
TURN 这一侧用的是 coturn,一个独立维护的第三方开源 TURN 服务器,跟 HopToDesk 没有从属关系。Debian/Ubuntu、Fedora、Arch 等主流发行版的官方源里都直接有,apt install coturn(或者其它发行版的等价命令)就够了,不需要拉镜像,也不需要自己编译。如果你想用容器跑也行,官方的镜像也有。
标准端口:3478 跑明文 TURN(UDP 和 TCP 都行),5349 跑 TURN over TLS。配一个 realm、一段长随机密钥、和一组凭证。HopToDesk 唯一的要求是 realm、host、凭证跟 api.json(下一节)里的条目对得上。
coturn 管不了的一件事:网页客户端。网页客户端的 bundle 里一个 RTCPeerConnection 都没有。它不说 TURN,不做 ICE,不开 UDP socket,因为浏览器把这些全锁在 WebRTC 后面,而这个 bundle 压根没用 WebRTC。coturn 是原生桌面端能跑起来的关键——没有它,点对点打不通的时候桌面客户端就没有退路。网页客户端这边,它用不上。
网页客户端,以及 HopToDesk 没给你的那一块
这里是我花时间最多才想明白的坑。HopToDesk 官方的自建指南覆盖了信号服务器和 TURN,这对原生桌面应用来说够了,但网页客户端一个字都没提。官方托管的 web.hoptodesk.com 网页客户端把自家基础设施写死在了代码里:信号走 signal2.hoptodesk.com:443,中继走 turn.hoptodesk.com:21118。21118 这种非标准端口正是公司防火墙最爱封的,所以托管网页客户端在我的工作网络里就死在 “Could not establish a relay connection” 上。这些内容任何自建指南里都没有,因为官方压根不存在“可自建的网页客户端”这个东西。
网页客户端实际说的协议,从它的 JavaScript 里反推出来是这样的:
- 浏览器开
wss://<web-host>/relay/?v=2,这是中继通道。 - 对端回一段 JSON:
{"relay_addr": "<host>:21119"}。 - 浏览器告诉信号服务器
RelayConnection {protocol:"one-to-one", endpoint:<id>, addr:<relay_addr>}。 - Mac 的原生客户端从信号 WebSocket 收到
RelayConnection,向<host>:21119拨一条普通 TCP。 - Mac 一连上,后续所有会话字节就开始流动:浏览器这边是 WebSocket 二进制帧,Mac 那边是普通 TCP,中间加一层长度前缀(BytesCodec)编解。
所以中继这一侧其实是一个自定义的 WebSocket 转 TCP 的拼接器,说的是 HopToDesk 自己的 v=2 协议。它不是 TURN,不是 WebSocket-TURN,也不是任何能靠改 coturn 配置弄出来的东西。官方服务器上跑着一个,但没人公开过实现,自建指南对此只字不提。
我让 AI 写了一个。成果在 github.com/david-adam/self-host-hoptodesk:一个 Python 3 的桥(relay_bridge.py),浏览器侧说 v=2 协议,原生客户端侧说长度前缀 TCP,把两边粘起来。浏览器侧听 8443(Caddy 把 /relay* 反代到这里),Mac 侧 TCP 拨入听 21119,同一时间只支持一个网页会话,对我来说够用了。仓库里还有 patch_web_client.py,用来改官方网页客户端 bundle 里写死的域名(2025 年之前的可读版和现在的压缩版都支持),让 bundle 指向你自己的信号域名和 /relay 路径;还有一键部署到 VPS 的 setup_web_client.sh:桥、改好的 bundle、Caddyfile、Let’s Encrypt 证书、systemd 单元一次配齐。
Caddy 配置大致长这样:
hop.{$DOMAIN} {
root * /var/www/web
file_server
handle_path /relay* {
reverse_proxy localhost:8443 {
transport http { versions 1.1 }
}
}
}
那个 versions 1.1 是有讲究的:这个桥只支持 HTTP/1.1,Caddy 默认会尝试向上游用 HTTP/2,会把 WebSocket 升级搞坏。
Cloudflare 在前面只挂 DNS(灰云,不开代理),不做 TLS 终结,所以证书链里没有 Cloudflare 中间证书。每个连过来的 TLS 客户端看到的就是 certbot 发出来的那张 Let’s Encrypt 证书。浏览器那边的网页客户端走 443 没有任何问题,任何现代浏览器都认得现在的 LE 链。
证书怎么签、怎么续签,看你主机环境。DNS-01 最省事,前提是 Cloudflare 托管你的 DNS,而且你愿意给 certbot 一个 API token。HTTP-01 也可以,只要 Caddy 在 challenge 触发那会儿在 80 端口上听一会儿。
让 Mac 客户端连上来
Mac 客户端的配置在 ~/Library/Preferences/com.hoptodesk.HopToDesk/config.toml。TOML 这套写法比较老,现在的文档大多建议改用 api.json。这一套配置对应的 api.json 长这样:
{
"turnservers": [
{
"protocol": "turn",
"host": "turn.your-domain.com",
"port": "3478",
"username": "you",
"password": "long-shared-secret"
},
{
"protocol": "turn-tls",
"host": "turn.your-domain.com",
"port": "5349",
"username": "you",
"password": "long-shared-secret"
}
],
"rendezvous": { "host": "hop.your-domain.com", "port": "80" },
"rendezvousssl": { "host": "hop.your-domain.com", "port": "443" },
"winversion": "1.0.0",
"macversion": "1.0.0",
"linuxversion":"1.0.0"
}
五个部分:TURN 备份服务器(明文和 TLS 两份,严格的防火墙经常只放一个),端口 80 的明文 rendezvous,端口 443 的 TLS rendezvous,以及三个平台的客户端版本号。
rendezvous 和 rendezvousssl 指向同一个域名,只是协议不同:rendezvous 是 80 端口的明文 ws://,rendezvousssl 是 443 端口的 wss://。桌面客户端把这两个拼成一个主机列表,按顺序拨,先 wss 后 ws,哪个先通就用哪个。Mac 上 wss 握手失败(下一节),80 端口的明文 ws 那条就顶上,用户根本察觉不到。两个我都留着,因为 wss 这条路在所有其他平台上是正常工作的;在 Mac 上留着它也没坏处。
如果你只填 rendezvousssl,走 Mac → Caddy:443 这条路,Mac 客户端会卡在一个永远完成不了的握手上。HopToDesk Mac 客户端用的是 native-tls 0.2.11,在 macOS 上走的是 Apple Secure Transport。certbot 现在默认发出来的那条 Let’s Encrypt 链里,有几个中间证书 Apple 这套栈目前认得不如 NSS 或者 BoringSSL 那么干脆,握手就挂在那。这个跟 Cloudflare 没关系:就算 Cloudflare 只走 DNS、certbot 直接签在 Caddy 上,Mac 客户端照样在 wss:// 上失败。
能跑起来的路径是把 Mac 客户端留在明文 HTTP 上,让 Caddy 转发:
Mac 客户端 → hop.your-domain.com:80 → Caddy:80 → HopSignal:9000 (信号 WebSocket)
浏览器网页客户端照常走 443,因为浏览器处理 TLS 没问题。两条路,一个 Caddy,都能用。
还在让我别扭的地方
- Mac 客户端的设置是拆开的。首页那个齿轮按钮只放连接相关的开关(Choose Network、Proxy Settings、Direct IP access、Firewall Mode、Send Server Name、LAN Discovery)。设固定密码要去别的菜单,而且要设过一次密码之后那块才会显示出来。第一次找了挺久。
- 没有统一的网页 UI 来管理设备列表。每台机器显示自己的自动生成 ID,就这么多。要起名字、要分组,都得自己想办法。
- 网页客户端默认没有音频,剪贴板也不行。能用的就是一块屏幕、一个键盘、一个鼠标。我平时做的事情大部分用这个就够了,但要靠它干活之前最好心里有数。
折腾这些值不值
对我来说:免费、不按席位收费、没有“升级到企业版”的弹窗。网页客户端本来就是这次折腾的核心原因。驱动这一切的那台工作电脑,正是最能从浏览器入口里受益的那种锁死机器。自建意味着所有流量都走我自己 Caddy 上的 443,那是我工作网络里唯一不封的端口。
RustDesk 生态更成熟,也更多人盯着看,如果你只需要原生客户端互通,继续用托管版 RustDesk 或者它的自建版大概更省心。HopToDesk 是当你真正需要的就是网页客户端时的答案;只是要清楚官方自建指南到这一步就断了,桥那一块得自己留出一个晚上来写。
出处:HopSignal 二进制来自 hoptodesk.com/self-host.html,中继桥和网页客户端补丁工具来自 github.com/david-adam/self-host-hoptodesk。如果桥需要为更新版本的网页客户端 bundle 做调整、或者 Mac 客户端的 TLS 问题在上游被修了,我会回来更新这篇。