HopToDesk

自建 HopToDesk + 网页客户端,因为我试过的每一款都要钱包

October 17, 2026 · 3 分钟阅读

我需要从我的工作电脑连回家里的机器。这台工作电脑锁死了能装什么软件、能访问什么网络,所以唯一现实的入口就是一个能在浏览器里跑的网页客户端,而且必须是我能免费自建的那种。RustDesk 好用,但它的网页客户端前几年挪到了付费套餐后面,免费版有几个限制让人没法忍。国内的替代品更糟。RayLink 和 ToDesk 的网页客户端都要订阅,而且报价是那种看到第二个零就关掉的报价。企业版什么都有,但你不允许买一个席位。

所以我就找一款能自建、又有正经网页客户端的工具。HopToDesk 满足这两条。它是 RustDesk 的一个 fork,服务器端更简单(只有 HopSignal 一个二进制),网页客户端可以自己托管,桌面客户端照常指向自己的 rendezvous 服务器。没有 key,没有账号,不会向第三方上传遥测。

这篇是我折腾了一个周末之后最终留下的配置。

HopToDesk 到底是什么

HopToDesk 是从前几个版本从 RustDesk fork 出来的。桌面客户端长得还是 RustDesk 的样子,协议形态也一样,线格式没怎么改。对自建用户来说最大的区别在服务器端。RustDesk 的 hbbs / hbbr 双进程在 HopToDesk 这里被拆成了两部分:HopSignal 这个二进制只负责 rendezvous(让两端互相找到对方),relay 那侧则交给现成的 coturn TURN 服务器,在点对点打不通的时候承载实际的屏幕和键鼠数据。coturn 是独立的第三方项目,跟 HopToDesk 没有从属关系,只是大家默认都拿它做 TURN 这一层;HopToDesk 的 api.json 字段也是按它的接口来设计的。HopSignal 本身是无状态的,没有 id_ed25519.pub 要在客户端配置里来回拷,也没有 key = "..." 这种字段。安装脚本 hsinstall.sh 不会生成任何密钥对,因为根本没有东西要钉死。

这个思路让我重新学了一下。我之前存的好几份老教程都默认 RustDesk 那套,我花了不少时间在找一个根本不存在的密钥文件,丢人。

安装 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(下一节)里的条目对得上。我自己的主机上是 apt install coturn 直接装的,对外给桌面客户端暴露 3478 和 5349,本机再让 coturn 的 WebSocket TURN 端点听 8443,这样网页客户端(浏览器里只能跑 HTTPS)就能经 Caddy 转发过去。

网页客户端

HopToDesk 在同一个仓库里带了网页客户端。编译一次,把产物放到 /var/www/web。Caddy 同时占用 80 和 443 两个端口,所有路由都交给它,443 上挂的是 certbot 管的 Let’s Encrypt 证书。

Caddy 配置大致长这样:

hop.{$DOMAIN} {
    root * /var/www/web
    file_server
    @relay path /relay*
    reverse_proxy @relay localhost:8443
}

web.{$DOMAIN} {
    root * /var/www/web
    file_server
    @relay path /relay*
    reverse_proxy @relay localhost:8443
}

hop. 和 web. 是两个指向同一台 Caddy 的域名。两个都直接服务静态网页客户端,都把 /relay* 反代到 coturn 在 127.0.0.1:8443 上的 WebSocket TURN 端点。想把哪个告诉浏览器都行。

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,都能用。

还在让我别扭的地方

折腾这些值不值

对我来说:免费、不按席位收费、没有“升级到企业版”的弹窗。网页客户端本来就是这次折腾的核心原因。驱动这一切的那台工作电脑,正是最能从浏览器入口里受益的那种锁死机器。自建意味着我完全不依赖 HopToDesk 的 hosted 中继,这一点很重要,因为 hosted 网页客户端硬编码的中继域名在我工作的网络里就被挡掉了。

RustDesk 生态更成熟,也更多人盯着看,如果你只需要 Windows 跟 Windows 互通、也不需要网页客户端,继续用托管版 RustDesk 或者它的自建版大概更省心。HopToDesk 是当你真正需要的就是网页客户端、而且你宁愿自己跑中继也不想为此付费时的答案。

服务器二进制和安装脚本的出处: github.com/hoptodesk/HopSignal。如果 hsinstall.sh 改了、HopSignal 后面版本的证书加载方式变了、或者 Mac 客户端的 TLS 问题在上游被修了,我会回来更新这篇。

← 返回所有文章