
1. 为什么要自己编译 RustDesk而不直接用官方包RustDesk 这个远程控制软件这几年是越来越多人在用了。真要把它部署成“自己说了算”的一套内网穿透体系核心就两件事自己编译一套服务端再把自建 ID 服务器的地址和 key 一起写进客户端。注意不是让你下载官方安装包然后填个 IP 那么简单而是从源码构建出一条完全可控的链路。官方客户端默认连接的是官方 ID 服务器和中继服务器开箱即用这当然方便。但放到实际生产环境里问题马上会浮现出来设备多公网流量绕到第三方中转时延和稳定性不可控管理端要统一分发客户端配置官方包没有这种自由度还有审计和合规上的要求谁连过机器、数据走了哪条链路总得能说清楚吧。这些需求只要沾上一条就免不了要碰“自建服务器 改客户端”这条路线。这里要先讲清一个概念RustDesk 的“ID 服务器”不是你租一台云主机随便跑个进程就完事它背后有握手、身份认证、密钥交换这一整套逻辑。最关键的是那把 key。客户端和服务端之间要互相信任靠的就是这把 key。如果你只是自己下个客户端、填个服务器地址客户端不认识服务端的钥匙连接会直接失败。所以把 key 写进客户端不是锦上添花而是能不能连上的必要条件。这篇文章实际是在讲一条完整的落地方案怎么编译服务端、怎么拿到 key、怎么把 key 和服务器地址一起内嵌到客户端里。不管你是运维、网工还是自己家里有十几台机器想统一管理的折腾型选手只要能跟 Linux 命令行和编辑器打交道跟着步骤走完大概率能成。2. 先搞懂 ID 服务器和 key 的配合关系很多人在这一步踩坑原因是把 RustDesk 想得太简单以为它就是“客户端 服务端”两个程序。实际上它的服务端拆成了两个独立组件key 也不是随便填的明文密码。2.1 两个服务程序hbbs 与 hbbrRustDesk 的服务端由两个二进制组成hbbs和hbbr。hbbs是 ID 注册和信令服务负责维护设备 ID 和 IP 地址之间的映射关系。客户端启动后第一件事就是找 hbbs 报到说“我是这台机器的 ID当前 IP 是 x.x.x.x谁要连我就来找你”。当两台机器要建立会话时也是通过 hbbs 交换通信需要的 SDP 信息。hbbr是中继服务。网络环境千奇百怪两个客户端可能都在对称型 NAT 后面打洞打不过来。这时候数据流量就走 hbbr 中转。你可以把 hbbs 理解成前台总机负责接线hbbr 是备用线路打洞不通就绕它走。这两个组件缺一不可。只部署 hbbs 不部署 hbbr或者反过来都会在真正使用的时候出问题。前者会导致某些网络环境下无法建立连接后者会导致设备无法注册上线。2.2 公钥/私钥体系为什么客户端一定要带 keyRustDesk 的 key 不是简单的登录密码而是基于非对称加密的密钥对。服务端在首次启动时会在数据目录生成一对 Ed25519 密钥得到私钥文件id_ed25519和公钥文件id_ed25519.pub。私钥保存在服务端机器上绝对不能泄露公钥则是要发给所有客户端的“通行证”。客户端连接服务端时会带上一段由公钥参与计算的身份信息。服务端用自己手上的私钥去验证验证通过才允许这个客户端接入。这个过程可以防止有人架一个冒牌服务器来骗客户端因为只有真正持有对应私钥的服务器才能和带正确公钥的客户端完成握手。这就解释了一个常见的困惑为什么我填了服务器地址还是连不上因为客户端默认使用官方内置的公钥和你自建服务器生成的公钥对不上握手时直接被拒。2.3 一次远程会话是怎么建立的我习惯把这整套流程看做一次电话呼叫。第一步设备 A 和设备 B 都先连接到 hbbs注册自己的 ID 和当前网络地址。第二步设备 A 想控制设备 B就通过 hbbs 发起“呼叫”hbbs 告诉设备 B“有人找你”同时把双方的地址信息交换出去。第三步两边的客户端尝试建立点对点连接能直接连通就走 P2P 通道速度快、不占服务器带宽连不通就把数据交给 hbbr 中转。最后等连接建立起来双方再通过之前约定好的 key 做端到端加密确保传输内容只有两端能解开。整个流程里key 不仅参与了第一步的接入认证也参与了加密会话的初始化。如果一开始 key 就对不上后面所有环节都无从谈起。所以自建服务器的第一步不是连客户端而是先把服务端跑起来、把 key 拿到手。3. 编译运行自建 ID 服务器服务端编译相对简单因为 RustDesk 的服务端代码是独立的仓库依赖很少。我以一台 Linux 云主机为例把整个过程拆成四步。3.1 准备编译环境和源码先确保机器上有基础的编译工具链git、curl、build-essential、cmake、pkg-config、libssl-dev。如果你用的是干净的云镜像大概率需要手动装一遍。apt update apt install -y git curl build-essential cmake pkg-config libssl-dev然后是 Rust 工具链。服务端代码基于 Rust 编写用官方rustup安装即可。curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/envRustDesk 服务端仓库就一个直接克隆下来git clone https://github.com/rustdesk/rustdesk-server.git cd rustdesk-server这里有个小建议不要 clone 最新的 main 分支就直接上生产。优先看仓库里的 Tags挑一个稳定版本。远程控制软件对稳定性要求极高追新容易追出兼容性问题。3.2 编译得到 hbbs 和 hbbr在仓库根目录执行cargo build --release编译时间取决于机器性能通常几分钟到一刻钟不等。结束后在target/release目录下能看到hbbs和hbbr两个可执行文件。ls -lh target/release/hbbs target/release/hbbr确认文件已经生成把它们复制到你计划运行服务端的目录。我习惯统一放在/opt/rustdesk-server下mkdir -p /opt/rustdesk-server cp target/release/hbbs target/release/hbbr /opt/rustdesk-server/ cd /opt/rustdesk-server3.3 首次启动、生成 key、拿到公钥首次运行hbbs时它会在当前目录生成密钥对。这一步非常关键因为后面所有客户端要用的 key 就在这里。启动命令./hbbs -r public_ip:21117-r参数后面跟的是 hbbr 中继服务器的地址也就是客户端在 P2P 不通时使用的转发地址。这里的public_ip写这台服务器的公网 IP端口默认是 21117。启动后查看当前目录应该能看到id_ed25519 id_ed25519.pubid_ed25519是私钥务必做好备份也务必不要分发出去。id_ed25519.pub是公钥它的内容就是客户端要用的 key。把公钥内容复制出来它是 base64 编码的一长串字符cat id_ed25519.pub然后再启动hbbr./hbbrhbbr启动时不需要额外参数默认监听 21117 端口。如果你想让它和 hbbs 使用同一个密钥目录保持当前目录不变即可。实际部署中一般还要加一个-k参数指定服务端连接时用的 verify key。这个参数不是必需的但建议加上它可以防止其他地方误启动的服务端使用错误的密钥文件。命令行直接写./hbbs -r public_ip:21117 -k 公钥内容 ./hbbr -k 公钥内容这里有个容易混淆的点-k参数后面的内容就是刚才cat id_ed25519.pub得到的那串 base64 字符串。这意味着服务端启动时明确告诉客户端“我认这把公钥”。如果-k写错或者不写而客户端又带了自建公钥就会频繁出现握手失败。3.4 端口规划与防火墙放行RustDesk 服务端用的端口不多但类型要分清。整理成一张表方便对照端口协议用途21115TCPNAT 类型探测21116TCP UDPhbbs 主通信端口UDP 用于打洞21117TCPhbbr 中继通信21118TCPWeb 客户端连接 hbbs 使用可选如果服务器有防火墙或者云安全组这几个端口都要放行尤其是 21116 的 UDP 包。很多人部署后能连上但画质差、延迟高就是 UDP 没放通导致打洞全部失败所有流量都走了中继。建议直接用systemd托管这两个服务避免进程意外退出。我提供一个简单的服务文件思路以 hbbs 为例[Unit] DescriptionRustDesk hbbs Afternetwork.target [Service] Typesimple WorkingDirectory/opt/rustdesk-server ExecStart/opt/rustdesk-server/hbbs -r public_ip:21117 -k 公钥内容 Restarton-failure [Install] WantedBymulti-user.targethbbr的服务文件照葫芦画瓢就行只不过ExecStart换成/opt/rustdesk-server/hbbr -k 公钥内容。4. 编译客户端把服务器地址和 key 写进去服务端跑起来key 也拿到了接下来是最核心的一步自己编译客户端并把自建服务器的信息写进去。4.1 获取客户端源码和编译环境RustDesk 客户端的主仓库是独立的它在结构上和普通软件不太一样。老的版本直接使用 Rust 写全功能客户端后来引入了 Flutter 作为界面层。GitHub 上大多数情况下你会看到主仓库rustdesk/rustdesk里面的 UI 部分可能是 Sciter 版本也可能是 Flutter 版本具体看分支和 tag。推荐的做法是拉取稳定 taggit clone --depth1 -b 稳定版本号 https://github.com/rustdesk/rustdesk.git cd rustdesk编译前需要确认下面这些依赖Linux 平台上需要 GTK 相关开发包、libxdo-dev、libssl-dev、cmake、clangWindows 平台上需要 Visual Studio Build Tools、LLVM/Clang、Windows SDK无论哪个平台都要有完整的 Rust 工具链这里要特别提一句编译依赖下载的问题。RustDesk 的内部模块会引用一些第三方组件比如 Windows 上可能需要qscintilla这类和 UI 编辑相关的库。第一次编译时网络条件不好会非常痛苦。我的经验是提前把依赖拉全不要在编译过程中挂着代理期待它自动解决所有问题。项目里如果有vendor目录或预编译脚本优先使用。4.2 写 key 和服务器地址的三种方式把自建服务器地址和 key 写进客户端常见的有三种做法按从“临时”到“固化”的顺序排列。第一种环境变量。部分版本的客户端支持通过环境变量注入公钥编译时设置RS_PUB_KEY你的公钥内容 cargo build --release这种做法的好处是源码不用改增删都方便。但缺点也很明显变量是编译时注入的运行时对用户透明除非你自己写文档否则使用者根本不知道这把 key 是哪里来的。第二种启动参数。客户端最终编译出来后运行时可以加参数./rustdesk --server 你的服务器地址:21116 --key 公钥内容这种方式应急很管用但你需要把参数写进桌面快捷方式或封装脚本里。一旦用户重装系统、拷贝快捷方式时漏掉参数直接连不上。第三种也是最推荐的修改源码里的默认常量。客户端源码中一定有一个地方存着默认 ID 服务器地址和默认公钥。以某一版源码为例在src/common.rs或者src/server/config.rs里会有类似这样的定义pub const DEFAULT_ID_SERVER: str rs.rustdesk.com; pub const DEFAULT_PUB_KEY: str 官方公钥字符串;把这两处改成你自己的pub const DEFAULT_ID_SERVER: str you.server.com; pub const DEFAULT_PUB_KEY: str 你的公钥内容;然后在源码根目录重新编译cargo build --release这样产出的客户端打开以后默认就连你自己的服务器key 也自动带好了。用户拿到手就是一个开箱即用的定制版本不需要在界面上做任何额外配置。需要注意不同 tag 的源码文件布局不同但搜索DEFAULT、PUB_KEY、ID_SERVER这几个关键字多花两分钟完全可以定位到。我个人建议先打开源码搜一遍确认当前版本的常量位置再动手改。4.3 编译一个大坑Flutter 客户端和 Rust 客户端的区别如果你拉取的版本是 Flutter 界面版的那么除了 Rust 核心代码还要编译 Flutter 部分。主仓库里通常会带一个flutter目录里面有对应的lib和pubspec.yaml编译步骤要复杂一些先编译 Rust 核心代码生成动态库或静态库再用 Flutter 编译 UI 层把 Rust 库链接进去这种结构下改服务器地址和 key 的位置也变了大概率在 Flutter 层的 Dart 源码里而不是在 Rust 的common.rs里。打开flutter/lib/下面的文件搜索server和key一样能定位到。我的忠告是如果你不是对 Flutter 构建流程很熟优先选 Sciter 版或者纯 Rust 版的老版本。不是说老版本更好而是它构建链路短、干扰项少适合第一次走通全流程。等你把服务端、key、客户端这条链路跑通了再回头折腾 Flutter 版不迟。4.4 编译产物和分发注意事项编译完成后Linux 下在target/release/里能找到可执行文件Windows 下则是一整套 exe 和 DLL。分发时不要只拷一个 exeRustDesk 依赖的动态库也要一并带上。最简单的办法是打成压缩包让使用者解压后直接运行目录里的rustdesk.exe。这里有两个容易忽略的点。第一代码签名。Windows 系统对没有签名的可执行文件非常敏感SmartScreen 会弹警告域环境下甚至可能被直接拦截。如果你只在小范围分发可以暂时忽略但正式铺开使用前最好申请一个代码签名证书。第二版本管理。你自己编译的客户端并没有内置更新通道以后换了 key、换了服务器地址所有存量客户端都要重新分发。所以在编译阶段就把版本号写清楚、把配置文件独立出来会省掉后面大量的解释成本。5. 联调验证与常见问题排查服务端编译完客户端也编译完不代表大功告成。联调这一步才是真正出故事的环节。5.1 怎么判断连接走的是自己的服务器打开客户端在设置页面里确认 ID 服务器地址已经显示为你自己的域名或 IP而不是rs.rustdesk.com。然后让两台机器同时上线随便建立一次远程控制回到服务器上看日志。如果hbbs日志里出现对应的 ID 注册信息说明客户端确实连上了你的 ID 服务器。如果两台机器走的是 P2P日志可能没有中继记录可如果日志里出现了数据转发相关的记录就说明一部分流量走了你的hbbr。最直接的验证方法临时把服务器上的 hbbs 进程停掉再看客户端是不是立刻报“无法连接服务器”。是说明客户端配置生效不是说明它还在偷偷连官方服务器赶紧查源码改漏了哪里。5.2 频繁出现 401 或 unauthorized 类报错如果你在日志或客户端界面里看到类似 401 unauthorized、密钥验证失败、身份认证异常一类的提示十有八九是 key 对不上。这个报错的经典场景是服务端启动时自动生成了一把全新的密钥但你发给客户端的还是上一次的公钥又或者-k参数里填了旧公钥而数据目录里实际放的私钥是新生成的。解决思路很明确让客户端手里的公钥和服务端手里的私钥匹配起来。排查步骤按顺序走在服务端执行cat id_ed25519.pub拿到当前公钥在源码里对比DEFAULT_PUB_KEY是不是和它一致重新编译客户端注意先清掉旧的编译缓存再构建避免用了旧的产物一个容易被忽略的细节是字符串复制问题。base64 公钥很长复制时容易漏掉末尾字符或带上不可见换行符。改完源码后建议仔细核对一遍最好用diff工具和服务器上的公钥文件做对比不要肉眼看。5.3 提示 key 值未知、服务器拒绝握手这也是常见报错。客户端提示 key 值未知通常表示客户端代码里根本没有写默认公钥或者公钥常量为空。此时用户虽然在客户端界面上可以手动填 key但既然你的目标是“写进客户端”说明这个场景不能接受。处理办法很直接回到源码确认DEFAULT_PUB_KEY不是空字符串也不是官方占位符。这里也能顺手检查一下你改的常量是否真的被编译进去在源码里插入一行测试日志把公钥明文打印到启动输出重新编译运行一次。这个笨办法在排查里非常有效能直接把“源码改了但没生效”和“改错位置”两个问题定位出来。5.4 端口不通导致打洞失败服务端进程都在跑key 也完全一致但两台机器连接时明显卡顿画面出来后延迟极高。这时要优先检查防火墙端口。前面已经提到21116 的 UDP 是最容易被遗漏的。云平台的安全组规则往往只放了 TCP忘了 UDP。打洞依赖 UDP走不通就得绕中继中继又在公网另一端延迟自然上去了。检查端口连通性时不要盲目用 telnet 测它只能测 TCP。UDP 端口可以用nc -u或者直接看 hbbs 日志正常情况能看到来自不同公网 IP 的 UDP 包不断地进来。没有 UDP 流量就回去检查安全组和系统防火墙。5.5 编译阶段的三类典型坑第一类缺系统依赖。Linux 下最常见的报错是找不到 GTK、找不到libssl解决办法就是回头看编译前依赖检查那一节缺什么装什么。第二类工具链版本不匹配。RustDesk 源码里一般有rust-toolchain.toml文件它会锁定一个特定的 nightly 版本。如果你本地装的 Rust 工具链太新或太旧编译时会报一些匪夷所思的错误。遇到这种情况不要硬调代码先按项目指定的工具链版本重装问题往往自动消失。第三类第三方库拉取失败。前面提到过的 qscintilla、以及其他 UI 组件下载源在国外国内网络环境经常会拉不完或者超时。我的做法是在编译前先执行一遍cargo fetch把依赖全部拉到本地缓存确认没有缺包后再开始正式编译。如果cargo fetch本身就失败就检查 DNS 和底层网络配置不要试图跳过依赖裸编译。6. 再分享一点实际部署中的心得前前后后我帮朋友和自己搭过好几套 RustDesk 自建服务第一次走通时觉得新鲜用了一段时间之后才明白最难的不是编译而是版本管理和用户习惯。编译只是半天工作量真正要花心思的是让所有人把“打开客户端就直接用”当作默认行为。如果使用者还要打开设置面板填服务器地址、填 key那这套系统的价值就少了一半。所以我的建议是在源码阶段就把默认值写死别留任何给普通用户手动操作的余地。key 的管理一定要建立备份机制。私钥id_ed25519一旦丢失所有客户端都得重新发布。密钥文件不要只放在服务器上本地备份一份或者放到加密的配置管理仓库里。每次变更 key都要把服务端和客户端的搭配关系记录下来不然几个月后回来看只能一脸茫然。最后一个小技巧编译好的客户端版本号不要沿用官方默认在源码里把版本号改成自己的标识。这样排查问题时你一眼能看出这是自己编译的版本还是官方包也方便在多个测试批次之间做区分。别小看这一个小动作它能在你被远程问题折腾到快崩溃的时候帮你省下半小时。