RustDesk 开源远程桌面软件系统,自建服务器部署 (审计)、发布和使用教程
条评论前言
RustDesk 是一个强大的开源远程桌面软件,是中国开发者的作品,它使用 Rust 编程语言构建,提供安全、高效、跨平台的远程访问体验。可以说是目前全球最火的开源远程桌面软件了,GitHub 收藏数量达到了惊人的 92k!
教程功能
NaN. 简单介绍 RustDesk ,对 RustDesk 原理进行简单说明
NaN. 提供 linux 原生安装和 docker 安装两种方式自建 RustDesk 服务器
NaN. 第三方开源 web-api 服务(RustDesk 开源版本没有权限用户相关管理权限控制,pro 版本才有)
NaN. 如何发布 RustDesk 服务端(主要为开放端口说明)
NaN. 客户端简单使用 RustDesk
视频比较细,时长较长,已经分段完成,各位小伙伴可以选择分段观看。
与 TeamViewer、ToDesk 、向日葵等专有远程访问解决方案相比,RustDesk 作为一个开源软件,提供了几个显著的优势:
NaN. RustDesk 完全免费使用,没有任何隐藏费用或订阅计划。
NaN. 由于其开源特性,RustDesk 的代码是透明的,可以由社区审计,从而提供更高的安全性和可信度。
NaN. 由于 RustDesk 允许自建服务器,隐私性更高
NaN. RustDesk 自建服务器硬件需求非常低官方说法如下:
1
2
硬件要求非常低;基本云服务器的最低配置就足够了,CPU和内存要求极低。您也可以使用树莓派或类似设备。关于网络规模,如果TCP打洞直连失败,将消耗中继流量。中继连接的流量根据分辨率设置和屏幕更新在30 K/s到3 M/s(1920x1080屏幕)之间。如果仅用于办公需求,流量约为100 K/s。
由于被诈骗分子频繁使用,该项目现已暂停国内服务。即国内无法通过公共服务器连接
技术架构对比
维度 | TeamViewer | RustDesk |
---|---|---|
传输协议 | 私有协议(优化带宽和延迟) | WebRTC + 自定义协议(依赖 P2P/UDP 打洞) |
中继服务器 | 全球分布式商业服务器(强制中转) | 可选自建中继服务器(开源实现) |
连接方式 | 优先 P2P,失败时自动中转 | 强制尝试 P2P,失败后走自建 / 官方中继 |
代码可控性 | 闭源 | 完全开源(MIT 许可) |
功能差异对比
功能 | TeamViewer | RustDesk |
---|---|---|
多平台支持 | ✔️ (Win/macOS/Linux/iOS/Android/Web) | ✔️ (全平台 + 浏览器客户端) |
多显示器切换 | ✔️ (无缝切换) | ✔️ |
文件传输 | ✔️ (拖拽 / 管理界面) | ✔️ (基础拖拽) |
会议功能 | ✔️ (最多 300 人,企业版) | ❌ (仅 1 对 1 控制) |
远程打印 | ✔️ (虚拟驱动) | ❌ |
无人值守访问 | ✔️ (设备分组 / 批量部署) | ✔️ (需手动设置密码) |
命令行控制 | ✔️ (TV CLI) | ✔️ (命令行参数) |
移动设备控制 | ✔️ (Android/iOS 反向控制) | ✔️ (Android/iOS 反向控制) |
RustDesk 架构概述
RustDesk 采用了经典的客户端 - 服务器模型,其中涉及三个主要组件:RustDesk 客户端、RustDesk 服务器和 ID Server。
NaN. 客户端 - 服务器模型
在 RustDesk 的架构中,客户端是运行在用户设备 (如笔记本电脑、平板电脑或智能手机) 上的应用程序。它提供了一个图形界面,允许用户发起远程访问请求并与远程计算机进行交互。另一方面,服务器组件运行在要远程访问的目标计算机上。它负责监听来自客户端的连接请求,并在建立连接后向客户端发送屏幕更新和接收输入事件。
NaN. ID Server 的角色
ID Server 在 RustDesk 的生态系统中扮演着重要的角色。它的主要职责是促进客户端和服务器之间的初始连接建立。**当 RustDesk 服务器启动时,它会连接到 ID Server 并注册自己,提供如服务器 ID 和公网 IP 地址等信息**。类似地,当客户端想要连接到特定的 RustDesk 服务器时,它会向 ID Server 查询目标服务器的连接信息。
**ID Server 维护了一个已注册的 RustDesk 服务器目录,并充当客户端和服务器之间的中介,帮助它们建立直接的点对点 (P2P) 连接**。一旦客户端从 ID Server 获得了服务器的连接信息,它就可以尝试直接连接到服务器,而无需进一步通过 ID Server 中继数据。
NaN. Relay Server 的角色
在某些网络环境下,RustDesk 客户端和服务器可能**无法直接建立 P2P 连接**,例如当它们位于 NAT (网络地址转换) 或防火墙后时。为了克服这一挑战,RustDesk 引入了 Relay Server。
**如果客户端无法直接连接到服务器,它会向 ID Server 请求一个 Relay Server。然后,客户端和服务器都连接到指定的 Relay Server,并通过它来中继所有的网络通信**。Relay Server 在这种情况下充当客户端和服务器之间的桥梁,转发来自一方的数据包到另一方。
值得注意的是,即使通过 Relay Server 进行通信,RustDesk 也会维护端到端加密,确保中继服务器无法访问明文数据。Relay Server 只是盲目地转发加密的数据包,而不能查看或修改其内容。
RustDesk 部署教程
linux 原生部署
服务器配置说明:
配置项 | 参数 |
---|---|
cpu | 1C |
内存 | 1G |
系统 | Ubuntu 24.04 任意 linux 系统 |
下载 Rustdesk Server
方法 2 直接前往官方页面下载
https://github.com/rustdesk/rustdesk-server/releases
方法 3 服务器直接或者代理下载
docker github 代理地址:https://www.cnproxy.top/
使用 githuab 代理或者直接下载
1 | wget https://ghproxy.cnproxy.top/https://github.com/rustdesk/rustdesk-se |
解压 Rustdesk Server
1 | apt install unzip |
解压完成后会出现 hbbr,hbbs,rustdesk-utils 三个文件
hbbr:
hbbr
是 RustDesk Relay Server,即 RustDesk 中继服务器,当客户端之间无法直接建立 P2P 连接时,会通过hbbr
中继服务器进行数据传输hbbr
允许无法直接建立 P2P 连接的客户端通过中继服务器进行通信。
hbbs:
hbbs
代表 RustDesk ID / Rendezvous Server,即 RustDesk ID 注册服务器。它用于分配和注册 ID,并且是 RustDesk 的中介服务器(Broker Server),用于管理和协调客户端连接。hbbs
帮助客户端找到并建立 P2P 连接,负责维护客户端的在线状态,并处理连接请求。当客户端 A 希望连接客户端 B 时,它会向hbbs
发送请求,hbbs
会帮助它们建立连接。
rustdesk-utils:
rustdesk-utils
是 RustDesk 的命令行工具,提供了一些管理和操作 RustDesk 服务器端的工具和命令。
配置系统服务且加入开机自启动
执行 vim hbbr.service 配置系统级服务
1 | [Unit] |
执行 vim hbbs.service 配置系统级服务
1 | [Unit] |
将服务添加到系统服务,且加入开机自启动
1 | //将当前路径服务以软连接加入系统服务中 |
总结
解压完成后 hbbr 与 hbbs 可以移动位置,但是两个文件需放在同一目录中。
首次需要先启动 hbbs 在启动 hbbr
docker 部署教程
安装 docker
见本站 docker 安装教程 https://halo.blog360.sbs/archives/dockeran-zhuang-jiao-cheng
运行容器
docker 直接运行
1 | docker run --restart=always --name hbbs -v /opt/rustdesk:/root -td --net=host rustdesk/rustdesk-server hbbs -r [服务器公网IP或域名] |
docker compose 运行
1 | version: '3' |
总结
- 注意配置公网访问中继节点
rustdesk-web-API-server 部署
首先我们为什么要部署 rustdesk-API-server,我们需要指定 rustdesk 开源版本与 pro 版本的区别,其核心区别主要有下:
功能 | 开源自托管版 | Pro 自托管版 |
---|---|---|
Web 管理控制台 | ❌ 无 | ✅ 完整图形化界面(设备 / 用户 / 权限管理) |
API 接口 | ❌ 无 | ✅ RESTful API(自动化集成) |
审计日志 | ❌ 仅基础连接日志 | ✅ 完整操作审计(登录 / 会话 / 文件传输) |
设备分组 / 标签 | ❌ 手动管理 | ✅ 可视化批量管理 |
企业级安全 | ❌ 基础密码验证 | ✅ TOTP 两步验证 (2FA) |
会议功能 | ❌ 仅一对一连接 | ✅ 支持多人会议(≤10 人) |
安卓隐私黑屏 | ❌ 无 | ✅ 被控时屏幕自动黑屏 |
商业授权 | ❌ AGPLv3 (有传染性) | ✅ 商业许可(规避开源风险) |
官方技术支持 | ❌ 仅社区支持 | ✅ 订阅用户专属支持 |
对应企业而言,前五条还是比较重要的,而 pro 版本的订阅价格还比较贵,我们可以通过自行部署其他第三方 rustdesk-web-API 来解决,当然第三方肯定无法和官方的 pro 比较,如果权限要求比较细也可以购买 pro 版本支持一下
那么我们本次部署所使用的第三方 rustdesk-web-API 可以提供哪些功能呢?基本如下
功能 | 详情 |
---|---|
Web 管理控制台 | 提供了一个完整图形化界面(设备 / 用户 / 权限管理) |
API 接口 | RESTful API 并且提供了 swagger 接口,可以自行定制开发 |
设备分组 / 标签 | 可视化批量管理 |
安全提升 | 可强制要求登录后才允许使用远程 |
项目地址:https://github.com/lejianwen/rustdesk-api
安装 docker
见本站 docker 安装教程
docker compose 运行
添加 docker-compose.yml 文件
1 | version: '3' # 必须声明版本(推荐 ≥3.5) |
完整参数配置
变量名 | 说明 | 示例 |
---|---|---|
TZ | 时区 | Asia/Shanghai |
RUSTDESK_API_LANG | 语言 |
|
RUSTDESK_API_APP_WEB_CLIENT | 是否启用 web-client; 1: 启用, 0: 不启用; 默认启用 | 1 |
RUSTDESK_API_APP_REGISTER | 是否开启注册; |
|
RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见 swagger 文档; |
|
RUSTDESK_API_APP_TOKEN_EXPIRE | token 有效时长 |
|
RUSTDESK_API_APP_DISABLE_PWD_LOGIN | 是否禁用密码登录; |
|
RUSTDESK_API_APP_REGISTER_STATUS | 注册用户默认状态; 1 启用,2 禁用, 默认 1 |
|
RUSTDESK_API_APP_CAPTCHA_THRESHOLD | 验证码触发次数; -1 不启用, 0 一直启用, >0 登录错误次数后启用 ; 默认 |
|
RUSTDESK_API_APP_BAN_THRESHOLD | 封禁 IP 触发次数; 0 不启用, >0 登录错误次数后封禁 IP; 默认 |
|
-----ADMIN 配置 ----- | ---------- | ---------- |
RUSTDESK_API_ADMIN_TITLE | 后台标题 |
|
RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用 | |
RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。 会覆盖 |
|
-----GIN 配置 ----- | ---------- | ---------- |
RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理 IP 列表,以 | 192.168.1.2,192.168.1.3 |
-----GORM 配置 ----- | ---------- | --------------------------- |
RUSTDESK_API_GORM_TYPE | 数据库类型 sqlite 或者 mysql,默认 sqlite | sqlite |
RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版 API, 1: 启用, 0: 不启用; 默认启用 | 1 |
-----MYSQL 配置 ----- | ---------- | ---------- |
RUSTDESK_API_MYSQL_USERNAME | mysql 用户名 | root |
RUSTDESK_API_MYSQL_PASSWORD | mysql 密码 | 111111 |
RUSTDESK_API_MYSQL_ADDR | mysql 地址 | 192.168.1.66:3306 |
RUSTDESK_API_MYSQL_DBNAME | mysql 数据库名 | rustdesk |
-----RUSTDESK 配置 ----- | ---------- | ---------- |
RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk 的 id 服务器地址 | 192.168.1.66:21116 |
RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk 的 relay 服务器地址 | 192.168.1.66:21117 |
RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk 的 api 服务器地址 | |
RUSTDESK_API_RUSTDESK_KEY | Rustdesk 的 key | 123456789 |
RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk 存放 key 的文件 |
|
RUSTDESK_API_RUSTDESK_WEBCLIENT MAGICQUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; |
|
RUSTDESK_API_RUSTDESK_WS_HOST | 自定义 Websocket Host |
|
----PROXY 配置 ----- | ---------- | ---------- |
RUSTDESK_API_PROXY_ENABLE | 是否启用代理: |
|
RUSTDESK_API_PROXY_HOST | 代理地址 |
|
----JWT 配置 ---- | -------- | -------- |
RUSTDESK_API_JWT_KEY | 自定义 JWT KEY, 为空则不启用 JWT 如果没使用 | |
RUSTDESK_API_JWT_EXPIRE_DURATION | JWT 有效时间 |
|
查看密码
执行
1 | docker logs -f docker容器ID |
查看密码
总结
注意这个服务包已经包含了 hbbr 与 hbbs,可以不用再部署了。
默认超管密码将会在容器日志中输出。
默认容器已经禁用了注册功能。
RustDesk 发布教程
在发布之前,我们需要了解 rustdesk 各个端口的作用:
了解端口,确认发布端口
hbbs 服务所需端口
协议 | 端口号 | 说明 |
---|---|---|
TCP | 21114 | 用于 Web 控制台(可选) |
TCP | 21115 | 用于 NAT 类型测试 |
TCP/UDP | 21116 | 必须同时启用 TCP 和 UDP,用于 ID 注册、心跳服务(UDP)以及 TCP 打洞、连接服务(TCP) |
TCP | 21118 | 用于支持 Web 客户端(可选) |
hbbr 服务所需端口
协议 | 端口号 | 说明 |
---|---|---|
TCP | 21117 | 用于中继服务 |
TCP | 21119 | 用于支持 Web 客户端(可选) |
那么可以根据一下需求来
RustDesk 官方开源版本
端口 | 是否必须 |
---|---|
21116 tcp/udp | 是 |
21117 tcp | 建议开放,用于在 p2p 失败后中继 |
21115 tcp | 建议不开放 |
rustdesk-web-API-server 第三方带服务版本
端口 | 是否必须 |
---|---|
21116 tcp/udp | 是 |
21114(443 or 80) tcp | 开放 将 web 使用 443 或者 80 发布 |
21117 tcp | 建议开放,用于在 p2p 失败后中继 |
21115 tcp | 建议不开放 |
21119 tcp | 建议不开放 |
21118 | 建议不开放 |
使用 nginx 代理发布端口
非 web 端口 nginx stream 发布 ·
NaN. 修改 nignx 配置文件,设置 stream 模块
1
2
3
4
5
6
7
8
9
10
11
stream {
include /etc/nginx/stream.d/*;
}
```
NaN. 添加 stream 代理端口
server {
listen 21116;
proxy_pass 172.30.0.2:21116;
proxy_connect_timeout 5s;
}
# UDP端口代理(P2P打洞)
server {
listen 21116 udp reuseport;
proxy_pass 172.30.0.2:21116;
proxy_timeout 3s;
}
server {
listen 21117;
proxy_pass 172.30.0.2:21117;
}
1 |
|
server {
listen 80;
server_name test.blog360.sbs;
location / {
client_max_body_size 50m;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://172.30.0.2:21114/;
index index.html index.htm A1-index.html;
}
}
1 |
|
ln -s /etc/nginx/sites-available/你的域名 /etc/nginx/sites-enabled/你的域名
1 |
|
sudo apt update
sudo apt install certbot python3-certbot-nginx
sudo yum install epel-release
sudo yum install certbot python3-certbot-nginx
1 |
|
sudo certbot
1 |
|
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import java.util.HashMap;
import java.util.Map;
public class WeComUserSync {
public static final String QY_HOST = "企业微信url路径";
public static final String QY_CORP_ID = "企业id";
public static final String QY_CORP_SECRET = "企业微信密钥";
public static final String RUST_DESK_HOST = “http://192.168.73.129:21114/api“;
public static final String RUST_DESK_TOKEN = “430b712ba7ef99da742c9c15bc5ae4c0”;
public static void syncUsers() {
String weComAccessToken = getWeComAccessToken();
JSONArray wecomUsers = getWeComUsers(weComAccessToken);
for (Object user : wecomUsers) {
JSONObject wecomUser = (JSONObject) user;
JSONObject targetUser = convertToTargetFormat(wecomUser);
createUser(targetUser);
}
}
public static String getWeComAccessToken() {
Map<String, Object> params = new HashMap<>();
params.put("corpid", QY_CORP_ID);
params.put("corpsecret", QY_CORP_SECRET);
HttpResponse response = HttpRequest.get(QY_HOST +"/cgi-bin/gettoken")
.form(params)
.execute();
JSONObject result = JSONUtil.parseObj(response.body());
if (result.getInt("errcode") != 0) {
throw new RuntimeException("获取access_token失败: " + result.getStr("errmsg"));
}
return result.getStr(“access_token”);
}
public static JSONArray getWeComUsers(String weComAccessToken) {
String departmentId = “1”;
String fetchChild = “1”;
String url = String.format(QY_HOST +"/cgi-bin/user/list?access_token=%s&department_id=%s&fetch_child=%s",
weComAccessToken, departmentId, fetchChild);
HttpResponse response = HttpRequest.get(url).execute();
String body = response.body();
JSONObject result = JSONUtil.parseObj(body);
if (result.getInt("errcode") != 0) {
throw new RuntimeException("企业微信API调用失败: " + result.getStr("errmsg"));
}
return result.getJSONArray(“userlist”);
}
public static JSONObject convertToTargetFormat(JSONObject wecomUser) {
Map<String, Object> map = new HashMap<>();
map.put("group_id", 2);
map.put("is_admin", false);
map.put("nickname", wecomUser.getStr("name"));
map.put("status", 1);
map.put("username", wecomUser.getStr("userid"));
return JSONUtil.parseObj(map);
}
public static void createUser(JSONObject userData) {
HttpResponse response = HttpRequest.post(RUST_DESK_HOST +”/admin/user/create”)
.body(userData.toString())
.header(“api-token”, RUST_DESK_TOKEN)
.contentType(“application/json”)
.execute();
if (response.getStatus() != 200) {
System.out.println("用户创建失败: HTTP " + response.getStatus());
}
JSONObject result = JSONUtil.parseObj(response.body());
System.out.println(“添加响应结果:”+result);
}
public static void updateAllUsersPassword() {
int page = 1;
int pageSize = 100;
int totalPages = 1;
while (page <= totalPages) {
JSONObject userListResponse = getUserList(page, pageSize);
if (userListResponse == null || userListResponse.getInt("code") != 0) {
System.err.println("获取用户列表失败,页码: " + page);
page++;
continue;
}
JSONObject data = userListResponse.getJSONObject(“data”);
JSONArray users = data.getJSONArray(“list”);
totalPages = (int) Math.ceil((double) data.getInt("total") / pageSize);
for (Object userObj : users) {
JSONObject user = (JSONObject) userObj;
updateUserPassword(user);
}
page++;
}
}
public static JSONObject getUserList(int page, int pageSize) {
String url = RUST_DESK_HOST + “/admin/user/list”;
try {
HttpResponse response = HttpRequest.get(url)
.header(“api-token”, RUST_DESK_TOKEN)
.form(“page”, page)
.form(“page_size”, pageSize)
.execute();
String body = response.body();
return JSONUtil.parseObj(body);
} catch (Exception e) {
System.err.println(“获取用户列表异常: “ + e.getMessage());
return null;
}
}
public static void updateUserPassword(JSONObject user) {
int userId = user.getInt(“id”);
String username = user.getStr(“username”);
String newPassword = username + “!@#123”;
String url = RUST_DESK_HOST + “/admin/user/changePwd”;
try {
if (userId >1) {
JSONObject requestBody = new JSONObject();
requestBody.set(“id”, userId);
requestBody.set(“password”, newPassword);
HttpResponse response = HttpRequest.post(url)
.body(requestBody.toString())
.header(“api-token”, RUST_DESK_TOKEN)
.contentType(“application/json”)
.execute();
JSONObject result = JSONUtil.parseObj(response.body());
if (result.getInt(“code”) == 0) {
System.out.println(“用户密码更新成功: “ + username);
} else {
System.err.println(“用户密码更新失败: “ + username +
“ | 错误信息: “ + result.getStr(“message”));
}
}
} catch (Exception e) {
System.err.println(“更新密码异常: “ + username + “ | “ + e.getMessage());
}
}
public static void main(String[] args) {
updateAllUsersPassword();
}
}
1 |
|
curl —location —request POST ‘http://192.168.73.129:21114/api/admin/oauth/create‘ \
—header ‘api-token: 60aca81100daccf883c66f32b45d7b72’ \
—header ‘Content-Type: application/json’ \
—data ‘{
“id”: 0,
“op”: “”,
“oauth_type”: “github”,
“issuer”: “”,
“client_id”: “2221”,
“client_secret”: “1123”,
“redirect_url”: “”,
“scopes”: “”,
“auto_register”: true,
“pkce_enable”: false,
“pkce_method”: “S256”
}’
```
完成后页面展示
第三方接入与 OIDC 接入
根据 up 主的查阅 OIDC 算比较前沿的,国内支持厂商并不多,好像企业微信在 23 年已经内测了,如果内部业务系统支持可以对接,或者查询所使用的系统看有没有对接的可能,或者直接简单开发对接 api 实现自定义 Oauth2 登录
其他权限管理
这里的话各位小伙伴可以自行探索,根据自己的需求来使用地址簿、标签、分组等等