
很多人第一次看 RustDesk,会把它理解成“Flutter UI 加一个远程屏幕库”:控制端连上对方,主机端把画面编码推过来,再把鼠标键盘事件传回去。
这个理解能解释表面体验,但解释不了源码里真正复杂的部分。RustDesk 难的不是“显示一张远端屏幕”,而是:两台机器经常都在 NAT 后面,控制端要先找到 host;直连、UDP 打洞、IPv6、同内网地址和 relay fallback 要一起竞争;连接进来以后,还要把键盘、剪贴板、文件、音频、摄像头、terminal、隐私模式这些能力拆成独立权限。
这篇不写泛泛的“远程桌面架构”。我们只沿着源码看三件事:RustDesk 怎么让两端连起来,host 端怎么按需暴露能力,以及危险操作为什么不是登录成功就全部放开。
源码基线:
- • 本地源码目录:
sources/rustdesk - • 分支:
master - • Commit:
fa369365a576cc3a86e4643ba7224f65b48bdf9c - • Commit time:
2026-05-30T15:44:04+08:00 - • Commit subject:
Update Portuguese translations for clarity (#15135) - •
libs/hbb_commonsubmodule:2e9f641101c6bfbd1f4ca42a249bef7c14e52f5b
说明:本文由 AI 辅助完成,可能存在遗漏、理解偏差或版本差异。关键实现请以本文固定的 commit、本地源码和官方文档为准。
先把源码地图摆出来

RustDesk 的源码可以先分成四块看。
第一块是入口层:src/main.rs 和 src/core_main.rs。它们处理 GUI、CLI、server mode、connect mode 等启动路径。入口本身不厚,核心是把不同模式导入同一套 runtime。
第二块是 host runtime:src/server.rs、src/rendezvous_mediator.rs、src/server/connection.rs。这里负责启动本机服务、向 rendezvous server 注册、响应打洞/relay 请求、处理登录和权限。
第三块是控制端:src/client.rs 和 src/client/io_loop.rs。控制端会发 PunchHoleRequest,同时尝试直连、UDP KCP、IPv6、本地地址或 relay;连接建立后,peer stream 和 UI channel 在 io_loop 里汇合。
第四块是共享协议和平台能力:libs/hbb_common/protos/rendezvous.proto、libs/hbb_common/protos/message.proto 定义连接和会话消息;libs/scrap 负责采集,libs/enigo 负责输入注入,libs/clipboard 负责剪贴板。
一个容易踩的坑是只看 src/。RustDesk 的协议、配置和部分公共类型在 libs/hbb_common 里,而且它是 submodule。没有初始化这块,很多连接字段和权限字段会看不完整。
启动:UI 不是全部,host service 才是常驻能力
非 mobile、非 CLI、非 Flutter feature 的入口在 src/main.rs。main 调 core_main::core_main(),如果返回 Some(args) 再启动旧 Sciter UI。
真正的分流在 src/core_main.rs。它会解析 --connect、--file-transfer、--view-camera、--port-forward、--terminal、--server、--no-server 等模式。普通启动时,如果没有明确连接参数,会在后台启动 server;显式 --server 则进入 start_server(true, false)。
src/server.rs 里的 start_server 会做几件 host 侧基础工作:设置 server running 状态,启动 IPC,处理输入按键 timeout,按平台初始化输入/配置同步/Windows broker 清理,然后进入 RendezvousMediator::start_all().await。
所以 RustDesk 更准确的形态是“客户端 UI + 本机常驻 host runtime”的混合体。用户看到的是窗口,远程控制依赖的是本机 server runtime 是否能注册、被发现、接收连接并暴露服务。
连接建立:不是一条路,而是一组候选路径竞速


控制端的主入口在 src/client.rs。
Client::start 包装 _start。_start 会先处理 incoming-only、直连 IP/domain、rendezvous server 选择、IPv6 测试、UDP NAT 准备等前置条件。有 UDP 时,它会竞争两条 _start_inner 路径,而不是只走一个固定分支。
_start_inner 会连接 rendezvous server,构造 PunchHoleRequest。这个消息里不只是对方 id,还包括 conn_type、token、nat_type、udp_port、force_relay、IPv6 socket 地址等字段。换句话说,RustDesk 发起连接时已经把“我想用什么模式、我这边网络条件如何、是否强制 relay”一起告诉了协调方。
host 端的响应在 src/rendezvous_mediator.rs。handle_resp 收到 PunchHole 会 spawn handle_punch_hole,收到 RequestRelay 会 spawn handle_request_relay,收到 FetchLocalAddr 会处理同内网地址。handle_punch_hole 再根据 NAT 类型、same NAT、proxy、force relay、是否禁用 TCP/UDP 来决定直连、UDP hole punching、local addr 还是 relay。
底层辅助逻辑在 src/common.rs。例如 test_nat_type_ 会通过两个 rendezvous 端口比较映射端口,判断 asymmetric 或 symmetric;punch_udp 会以 20ms 起步、最大 200ms 的间隔反复发空包,最长 20 秒;secure_tcp_impl 会用 rendezvous server public key 验证 key exchange,再设置 stream symmetric key。
这就是 RustDesk 连接建立最重要的事实:它不是“先连服务器,再转发屏幕”。rendezvous server 负责协调,controller 和 host 会围绕直连、打洞、本地地址和 relay fallback 做路径选择;最后无论走哪条路,都会进入后面的安全握手和登录流程。
Relay fallback:路径变了,安全边界没变
relay fallback 容易被误解成“直连失败后找个代理转一下”。RustDesk 的源码里,它更像双端会合。
host 收到 RequestRelay 后,handle_request_relay 会去重,然后调用 create_relay。host 先连接 relay server,把 RelayResponse 发回 rendezvous server,再调用 server::create_relay_connection。client 侧则通过 rendezvous server 拿到 RelayResponse,再去 relay server 上用同一个 uuid 会合。
关键点在于:relay 只是 transport 路径。进入 host 后,仍然会走 Connection::start、登录、权限判断和 service subscription。不能把 relay 理解成绕过 host 安全边界的通道。
Host service:授权以后才按需订阅能力

Server 的结构在 src/server.rs:它持有 connections、services 和 id_count。new() 默认注册 audio、display、clipboard、cursor/position/window focus、printer 等服务;terminal 是 per connection,不是全局 service。
服务抽象在 src/server/service.rs。Service trait 只暴露 name、subscribe/unsubscribe、join、option、ok 等通用能力。更关键的是 ServiceTmpl::run:只有 has_subscribes() 时才进入 inner loop;没有订阅时按 hibernate timeout 休眠。
这就解释了 host 端为什么不是一个连接对象里跑所有功能。连接通过认证以后,src/server/connection.rs 会按模式和权限决定订阅哪些服务:
- • 文件传输连接走文件相关分支。
- • terminal 连接初始化 per-connection terminal service。
- • 摄像头模式会增加 camera video service。
- • 普通远控会根据 keyboard、cursor、clipboard、file clipboard、audio 等权限构造
noperms,再调用s.add_connection(...)。
运行中也能调整。update_options 会根据 OptionMessage 动态改图像质量、FPS、supported decoding、音频、剪贴板、cursor/window focus 等订阅。切换 display 时,也会动态增加 video service 并订阅新的 service。
所以 RustDesk 的 host 模型可以概括成一句话:连接对象不直接拥有全部能力,它通过权限和选项去订阅 host services。
视频链路:capture、encode、send、ack 是闭环
视频服务在 src/server/video_service.rs。
new(source, idx) 会为 monitor 或 camera 创建 GenericService,注册 frame notifier 并运行 service loop。capturer 创建时会优先处理 Windows privacy mode;Windows 可走 portable_service DXGI/GDI,非 Windows 走 scrap::Capturer。monitor capturer 还会处理 Wayland、display 列表、MIT-SHM、privacy mode 等平台差异。
run 初始化 capturer、读取 VIDEO_QOS、设置 encoder。如果创建 encoder 失败,会 fallback 到 VP9。主循环捕获 frame,转成 encoder 需要的 YUV,再编码并发送。每帧发送后,VideoFrameController 会等 client ack,最多 3 秒;client ack 会反过来影响 QoS。
控制端在 src/client/io_loop.rs 收到 VideoFrame 后,第一帧会关闭连接成功提示、适配尺寸、发送隐私模式/虚拟显示 toggle,然后按 display 启动 video thread。关键帧直接发送,非关键帧进 queue。音频、剪贴板、文件、权限变化也在同一个 io loop 里被分发。
这块的工程价值不是“用了哪个编码器”,而是 RustDesk 把采集、编码、发送、确认、QoS、display switch、privacy mode 放进一个能订阅、能重启、能被连接状态影响的 service loop。
权限:远程控制不是登录成功就全放开

src/server/connection.rs 是权限边界的核心文件。
Connection::start 初始化时,会构造 challenge hash、CM channel、stream channel、video channel、input channel 等,同时把 keyboard、clipboard、audio、file、restart、recording、block_input、privacy_mode 这些能力通过 Self::permission(..., &control_permissions) 初始化。如果某项权限关闭,会立即发送 PermissionInfo 给 client。
权限来源不是一个地方。is_permission_enabled_locally 会先看 access-mode:full 直接允许,view 直接禁用;否则读取本地 option。permission 如果有 rendezvous ControlPermissions,会把 local option 映射到 rendezvous permission bitmap;bitmap 没设才回落到本地 option。位图解码在 src/common.rs 的 get_control_permission,每项 2 bit,1 = disable,2 = enable。
登录主流程也不是单一密码检查。未授权时,connection 只允许处理 LoginRequest、Auth2fa、TestDelay、CloseReason 等少数消息。LoginRequest 会先按连接模式检查权限:file transfer、view camera、terminal、port forward 都有各自开关。之后还会处理 ID/直连地址匹配、approve mode、无密码、密码错误、最近 session、Linux headless、OS login、connection manager 审批和 2FA。
授权之后,危险能力仍然有 gate。输入事件会检查连接模式;view camera 连接会忽略 mouse/key;重启只在 self.restart 为 true 时执行;分辨率调整要检查 self.keyboard;音频、隐私模式、文件、剪贴板也都有各自状态和订阅更新。
这就是 RustDesk 比“远程桌面投屏工具”更值得读的地方:它把可达性、身份认证、权限位图、本地配置、连接模式和服务订阅放在同一条链上,而不是把“已连接”当成万能许可。
最后怎么读
如果只读一条主线,我建议按这个顺序:
- 1.
src/core_main.rs:确认 GUI、CLI、server mode 怎么进入 runtime。 - 2.
src/server.rs:看start_server、Server::new、add_connection、subscribe。 - 3.
src/rendezvous_mediator.rs:看 host 如何注册、收 punch hole、决定 relay/direct。 - 4.
src/client.rs:看 controller 如何发PunchHoleRequest、竞争连接路径、进入 secure connection。 - 5.
src/server/connection.rs:看认证、2FA、权限、服务订阅和危险 action gate。 - 6.
src/server/video_service.rs:看视频 capture、encode、send、ack、QoS。 - 7.
src/client/io_loop.rs:看 client 如何接收视频、音频、剪贴板、文件和权限变化。 - 8.
libs/hbb_common/protos/*.proto:最后回到协议字段,整理连接类型、权限位和消息边界。
RustDesk 的项目价值,不只是把一台电脑的屏幕送到另一台电脑。它真正清楚地处理了远程桌面里最麻烦的几件事:网络路径不可靠,所以连接建立要多路径协调;host 能力太危险,所以必须按权限和连接模式拆开;视频只是其中一个 service,音频、剪贴板、输入、文件和 terminal 都要被同一套连接边界管理。读懂这三层,再看 RustDesk 的任何功能分支,都会更容易判断它是在解决连接问题、服务订阅问题,还是权限边界问题。
夜雨聆风