2023-02-16 15:23:31 +08:00
2023-02-08 16:45:30 +08:00
2026-07-23 20:13:48 +08:00
2025-02-07 18:34:58 +08:00
2026-07-24 10:56:56 +08:00
2025-01-15 22:00:33 +08:00
2023-07-06 00:50:11 +08:00
2026-07-23 20:13:48 +08:00
2022-05-12 20:07:45 +08:00
2026-07-24 10:56:56 +08:00
2023-02-16 15:23:31 +08:00
2026-07-24 10:56:56 +08:00
2026-07-24 10:56:56 +08:00
2022-05-12 22:11:06 +08:00
2025-08-31 17:21:02 +08:00
2025-08-31 17:21:02 +08:00
2025-08-31 17:21:02 +08:00
2025-08-31 17:21:02 +08:00

关于此分支

原始仓库:https://github.com/lejianwen/rustdesk-server.git 克隆时间:2026-09-16 镜像仓库:https://gitea7.yangjieshao.work/rustdesk/rustdesk-server.git 子模块 hbb_common:https://gitea7.yangjieshao.work/rustdesk/hbb_common.git


本仓库修改

编译与打包步骤

1. 编译服务器二进制(hbbs / hbbr / rustdesk-utils)

使用持久化 Rust 容器编译:

# 启动编译容器
docker run -d --name rustbuild --network host \
  -v /root/rustdesk/lejianwen/rustdesk-server:/srv -w /srv rust:alpine sleep infinity

# 容器内:换阿里云 apk 源
printf 'https://mirrors.aliyun.com/alpine/v3.24/main\nhttps://mirrors.aliyun.com/alpine/v3.24/community\n' > /etc/apk/repositories

# 安装依赖
apk add build-base pkgconf openssl-dev openssl-libs-static git

# 配置 cargo(rsproxy 源 + CLI git fetch)
# /usr/local/cargo/config.toml:
#   [source.crates-io] replace-with = "rsproxy-sparse"
#   [source.rsproxy-sparse] registry = "sparse+https://rsproxy.cn/index/"
#   [net] git-fetch-with-cli = true

# GitHub 加速(容器内系统级改写)
git config --system url.'https://gh-proxy.com/https://github.com/'.insteadOf 'https://github.com/'

# 编译
docker exec rustbuild cargo build --release

编译产物:target/release/hbbs、target/release/hbbr、target/release/rustdesk-utils

2. 构建 s6 基础镜像

# 拷贝二进制到 rootfs
docker cp rustbuild:/srv/target/release/hbbs /root/rustdesk/build/s6img/rootfs/usr/bin/
docker cp rustbuild:/srv/target/release/hbbr /root/rustdesk/build/s6img/rootfs/usr/bin/
docker cp rustbuild:/srv/target/release/rustdesk-utils /root/rustdesk/build/s6img/rootfs/usr/bin/

# 构建镜像(必须 --no-cache,否则 COPY rootfs 走缓存)
docker build --no-cache \
  -f /root/rustdesk/build/s6img/Dockerfile.local \
  -t rustdesk-server-s6:1.1.17-forapi \
  /root/rustdesk/build/s6img

3. 编译 API 二进制(apimain)

# 启动 Go 编译容器
docker run -d --name gobuild --network host \
  -v /root/rustdesk/build/apibuild:/srv -w /srv golang:1.23-alpine sleep infinity

# 安装 git + gcc(CGO 静态编译需要)
docker exec gobuild apk add --no-cache git gcc musl-dev

# 设置 Go 代理(直连 GitHub 或国内镜像)
docker exec gobuild go env -w GOPROXY=https://goproxy.cn,direct

# 编译(CGO 静态编译)
docker exec gobuild env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=gcc \
  go build -ldflags '-s -w' -o ./amd64/release/apimain ./cmd/apimain.go

4. 构建 admin 前端

# admin 前端来自 lejianwen/rustdesk-api-web
git clone --depth 1 -b master https://gh-proxy.com/https://github.com/lejianwen/rustdesk-api-web
docker run --rm -v $PWD:/srv -w /srv node:20-alpine sh -c \
  "npm config set registry https://registry.npmmirror.com && npm install && npm run build"
cp -a rustdesk-api-web/dist/. /root/rustdesk/build/apibuild/resources/admin/

5. 打完整镜像

# 修改 Dockerfile_full_s6 首行,指向本地 s6 基础镜像
cp /root/rustdesk/build/apibuild/Dockerfile_full_s6 /tmp/Dockerfile_full_s6
sed -i 's|FROM rustdesk/rustdesk-server-s6:latest AS server|FROM rustdesk-server-s6:1.1.17-forapi AS server|' /tmp/Dockerfile_full_s6

# 构建(context 必须含 amd64/release)
docker build -f /tmp/Dockerfile_full_s6 \
  --build-arg BUILDARCH=amd64 \
  -t yangjieshao/rustdesk-api:full-s6-amd64 \
  /root/rustdesk/build/apibuild

6. 验证运行

docker run -d --name rustdesk-test -p 21114-21119:21114-21119 \
  -p 21116:21116/udp \
  -e RELAY=<IP[:21117]> \
  -v <data>:/data -v <appdata>:/app/data \
  yangjieshao/rustdesk-api:full-s6-amd64

curl -s http://127.0.0.1:21114/_admin/ -o /dev/null -w '%{http_code}\n'

构建产物

镜像 说明
rustdesk-server-s6:1.1.17-forapi s6 基础镜像(hbbs + hbbr + rustdesk-utils)
yangjieshao/rustdesk-api:full-s6-amd64 完整镜像(+ apimain + admin 前端,s6 托管)

中继限速配置

hbbr(中继服务器)支持三个限速参数,均通过环境变量设置,单位 Mb/s:

参数 默认值 说明
LIMIT_SPEED 32 单连接限速
TOTAL_BANDWIDTH 1024 中继总带宽上限
SINGLE_BANDWIDTH 128 单连接带宽上限

启动时配置(Docker 环境变量):

docker run ... \
  -e LIMIT_SPEED=100 \
  -e TOTAL_BANDWIDTH=2000 \
  -e SINGLE_BANDWIDTH=500 \
  yangjieshao/rustdesk-api:full-s6-amd64

docker-compose 示例:

version: '3'

services:
  rustdesk:
    image: yangjieshao/rustdesk-api:full-s6-amd64
    ports:
      - 21114:21114
      - 21115:21115
      - 21116:21116
      - 21116:21116/udp
      - 21117:21117
      - 21118:21118
      - 21119:21119
    environment:
      - RELAY=<本机IP:21117>
      - ENCRYPTED_ONLY=1
      - TZ=Asia/Shanghai
      - MUST_LOGIN=Y
      - RUSTDESK_API_JWT_KEY=<your-secret-key>
      # 中继限速(单位 Mb/s,不设则用默认值)
      - LIMIT_SPEED=100
      - TOTAL_BANDWIDTH=2000
      - SINGLE_BANDWIDTH=500
    volumes:
      - /data/rustdesk/server:/data
      - /data/rustdesk/api:/app/data
    restart: unless-stopped

设为无限制时把对应值设为极大数,例如 -e LIMIT_SPEED=100000。

运行时修改(连接控制台):

命令 说明
limit-speed <值> 设置单连接限速
total-bandwidth <值> 设置总带宽
single-bandwidth <值> 设置单连接带宽

⚠️ 设为无限制:代码中 if v > 0. 判断导致设 0 会被忽略。如需无限制,请设一个极大值,例如 -e LIMIT_SPEED=100000(≈100 Gb/s,实际等同无限制)。


1.1.17 合并记录

日期:2026-09-05 关联文档:lejianwen/rustdesk-api README 说明:将官方 rustdesk/rustdesk-server 1.1.17 合并到本仓库(forapi 分支),完整构建记录见 lejianwen/rustdesk-api README。

合并要点

  • 官方 1.1.14(merge-base c650217)→ 1.1.17,共 43 个上游提交
  • 安全:禁用 UDP 打洞反射/放大路径(PunchHoleRequest/PunchHoleSent/LocalAddr 只允许走 TCP/WS)
  • 功能:-b/--bind 绑定指定 IP;控制台监听独立绑到 127.0.0.1;punch-requests(pr) 连接日志控制台命令
  • 修复:在线状态 i32 溢出(运行 24.9~49.7 天后误报在线);中继限速默认值 4Mb/s → 32Mb/s
  • 依赖:hbb_common 子模块推进到 69cea8d、新增 flate2

冲突处理

冲突文件 处理方式
src/rendezvous_server.rs 保留 fork 的 JWT/必须登录/KeyExchange/加密 Sink/API 特性,叠加上游全部修复,共 4 处冲突块手动合并
src/main.rs 双方参数合并:上游的 --bind + DEPRECATED 标注,保留 fork 的 --must-login
libs/hbb_common(子模块) 从 fork 的旧提交直接指到官方 1.1.17 用的 69cea8d
Cargo.toml / Cargo.lock git 自动合并成功

注意点:

  • fork 侧 check_cmd 帮助文本比上游多一行 must-login,合并 punch-requests 时要同步把 format! 占位符从 7 个加到 8 个,否则编译报 argument never used
  • 上游的 peers_online_state 是内联代码,fork 抽成了 helper(handle_online_request 复用),合并时保留 helper 并把 as i32 改 as i64

自连拦截(A 远程连 A)服务器端方案

状态:已实施并通过端到端验证(2026-09-06) 日期:2026-09-05 设计 / 2026-09-06 实施 关联文档:lejianwen/rustdesk-api README

v2 变更:经用户提示,发现 API 的 UserToken 表已存在 token→设备ID 映射, 方案从"JWT 加 claim"改为"映射查询",JWT 结构完全不动。 实施时额外修复:API 签发的 JWT 增加 jti(见 4.4)。

1. 问题描述

用户偶尔会误操作"用 A 电脑远程连接 A 电脑自己"(比如通讯录里点了自己、输错 ID),连接一旦成功,画面里包含自己的画面,形成无限嵌套(镜中镜)。

  • 该问题正统修法在客户端(发起前检查目标 == 自己),但 rustdesk 客户端仓库太大、用户不希望动客户端
  • 因此考虑服务器端拦截:发现"自己连自己"时直接拒绝,并把原因返回给客户端弹窗

2. 服务器端能拦截的位置分析

连接建立的时序(hbbs = ID/信令服务器,hbbr = 中继):

主控端 A ──PunchHoleRequest──▶ hbbs ──PunchHoleResponse──▶ 主控端 A
                                │
                                └──PunchHoleSent──▶ 被控端 B
(之后 A/B 直连打洞,或双方连 hbbr 中继)

关键结论:拦截窗口只有 hbbs 处理 PunchHoleRequest / RequestRelay 这两步。 一旦放行、两端直连建立(本机自连几乎必然直连成功),后续流量完全不经服务器,无法感知也无法切断。

API(Go 服务)不在打洞数据路径上(客户端直连 hbbs 的 21116/21118),所以 API 层没有拦截点;hbbr 也只在打洞失败后才登场。唯一可行的服务端拦截点就是 hbbs。

3. 代码调查结论(证据)

3.1 协议里没有"发起方设备 ID"

libs/hbb_common/protos/rendezvous.proto:

message PunchHoleRequest {
  string id = 1;              // 目标设备 ID(有)
  NatType nat_type = 2;
  string licence_key = 3;
  ConnType conn_type = 4;
  string token = 5;           // ← 发起方的登录 token(fork 已在用)
  ...
}                             // 没有任何"发起方自己 ID"字段

message RequestRelay {
  string id = 1;              // 目标设备 ID
  ...
  string token = 8;           // ← 中继请求也带 token
  ...
}

→ "目标 ID == 发起 ID 就拒"的规则本身正确,但服务器必须先知道发起方是谁。

3.2 token(JWT)里目前只有账号,但 API 数据库里已有 token→设备ID 映射

  • hbbs 侧(src/jwt.rs):Claims { user_id, exp } —— JWT 本身不含设备
  • API 签发侧(rustdesk-api/service/user.go:89):Jwt.GenerateToken(u.Id) 只放 user_id
  • 仅凭 user_id 对比目标设备归属 → 会误伤"同账号下 A 连自己的另一台设备 B"(合法主场景)❌
  • 但:API 的 model/userToken.go UserToken 表已存 Token + DeviceId + DeviceUuid + ExpiredAt, 且 Login()(service/user.go:97)签发时就写入 DeviceId: llog.DeviceId,Logout 时删行。 即 token → 设备ID 的映射已经持久化在 API 的 sqlite 中(v2 方案的基础)。

3.3 映射数据的来源:登录时 API 已经拿到发起方设备 ID

rustdesk-api/http/request/api/user.go:31:

type LoginForm struct {
    AutoLogin  bool              `json:"autoLogin"`
    DeviceInfo DeviceInfoInLogin `json:"deviceInfo"`
    Id         string            `json:"id"`   // ← 客户端自己的 rustdesk ID
    Type       string            `json:"type"`
    Uuid       string            `json:"uuid"`
    Username   string            `json:"username"`
    Password   string            `json:"password,omitempty"`
}

桌面客户端和 web 客户端登录 API 都走这个接口并携带 Id,登录流程将其写入 LoginLog.DeviceId 并落到 UserToken.DeviceId —— 无需客户端任何改动,设备身份已经入库。

3.4 MUST_LOGIN 只影响发起方(已确认)

hbbs 中 MUST_LOGIN 的强制点只有一处:src/rendezvous_server.rs:932(handle_punch_hole_request 内),即只有发起打洞的会话要求 token。

消息 发送方 MUST_LOGIN=Y 时
PunchHoleRequest 发起方 必须带有效 token
RequestRelay 发起方(force_relay 场景) 目前不检查(现状缺口)
RegisterPeer(注册/心跳) 被控端 不检查,不登录可被连
RegisterPk 被控端 不检查
hbbr 中继连接 双方 无校验

→ 被连接设备不需要登录;MUST_LOGIN=Y 恰好只保证"发起方必带 token",与自连拦截(只需要发起方身份)完全匹配。

3.5 错误原因回传客户端有现成通道

现有代码已经用 PunchHoleResponse.other_failure 给客户端弹窗:

msg_out.set_punch_hole_response(PunchHoleResponse {
    other_failure: String::from("Connection failed, please login!"),
    ..Default::default()
});

→ 自连拒绝提示用同一字段,客户端零改动。

4. 推荐方案(v2:映射查询,不改 JWT)

数据流:

客户端登录 ──LoginForm{Id}──▶ API:签发JWT + UserToken{Token, DeviceId}(已有)
主控端发起连接 ──PunchHoleRequest{id, token}──▶ hbbs
    hbbs:验JWT签名/有效期(逻辑不变)
        → 按 token 查 UserToken 得 device_id(内存缓存 → 未命中走 API 内部接口)
        → device_id == ph.id ?
           是 → 回 PunchHoleResponse{other_failure:"不能远程自己"},拒绝
           否 → 正常打洞

改动 1:API(rustdesk-api,Go,改动极小)

  • 确认登录时 LoginLog.DeviceId 取自 LoginForm.Id(rustdesk ID),对不上则补一行赋值
  • 新增内部查询接口(如 GET /api/internal/token-device,入参 token,返回 device_id), 仅允许 localhost/容器内访问(IP 判断 + 共享密钥头),不记日志避免 token 泄露

改动 2:hbbs(lejianwen/rustdesk-server,Rust,约 40 行)

  • handle_punch_hole_request:验签通过后查映射(token 哈希 → device_id, hbbs 内置 HashMap 缓存 + TTL 几分钟;未命中调 API 内部接口), device_id == ph.id → 拒绝并回 other_failure
  • RequestRelay 分支(handle_tcp):补上 token 校验(顺带堵掉 force_relay 绕过 MUST_LOGIN 的口子)+ 同样的自连比对
  • fail-open:API 不可达/查询失败 → 视为"无设备信息",跳过自连检查(不拖累正常连接)

兜底规则(已确认的设计决策):无 token 一律跳过校验

自连校验的触发条件是"请求携带有效 token 且查得到 device_id",与 MUST_LOGIN 取值解耦:

场景 行为
MUST_LOGIN=Y,任意发起方 必有 token → 校验生效,100% 拦截
MUST_LOGIN=N,发起方已登录 带 token → 顺路校验,同样拦截(免费保护)
MUST_LOGIN=N,发起方未登录 无 token → 跳过校验直接放行,保持现状行为(套娃风险被明确接受)

理由:没有 token 就没有设备身份,无从判断"是不是自己连自己";强行用 IP/user_id 猜 只会引入误伤(见第 5 节)。跳过 = 该场景退化为现状,不比现在更差,已是改动范围内的最优解。

改动 3:开启 MUST_LOGIN=Y

  • 保证所有发起连接的会话都带 token → 拦截覆盖率 100%
  • 代价即其既有语义:所有"主动发起连接"的设备必须登录;纯被连设备不受影响

明确不推荐:hbbs 直读 API 的 sqlite

同容器共享 /app/data 技术上可行,但 Go gorm 与 Rust sqlx 双进程写同一 sqlite 有锁风险, 且 hbbs 从此耦合 API 表结构(API 升级改表即崩)——映射读取统一走 HTTP 内部接口。

4.4 实施补充:JWT 签发增加 jti

实施中发现:JWT claims 只有 {user_id, exp},同一账号同一秒内多次登录会产出 字节级相同的 token,UserToken 表中无法区分设备(hbbs 查映射会命中错误设备)。 修复:lib/jwt/jwt.go GenerateToken 的 RegisteredClaims 增加 ID: uuid.NewString()(jti), 每次登录 token 唯一。旧客户端/旧 token 不受影响(Rust 侧 serde 忽略未知字段)。

4.5 实际改动清单(已全部落地)

rustdesk-api(Go)

文件 改动
service/user.go 新增 GetDeviceIdByToken(token):查 UserToken(校验 ExpiredAt)返回 DeviceId
http/controller/api/internal.go 新增内部接口 GET /api/internal/token-device?token=:仅回环 IP + X-Secret 头必须等于 JWT 密钥,否则 403;查不到 404
http/router/api.go 注册路由(置于 RustAuth 中间件之前)
lib/jwt/jwt.go GenerateToken 增加 jti

lejianwen/rustdesk-server(Rust)

文件 改动
src/token_device.rs(新增) 映射查询模块:get_device_id(token),内存缓存(命中 TTL 300s / 未命中 60s,容量上限 4096),minreq 在 spawn_blocking 中调 API,超时 3s,任何失败返回 None(fail-open)
src/lib.rs 注册 mod token_device
src/rendezvous_server.rs handle_punch_hole_request MUST_LOGIN 验签后:token 有效且 get_device_id == ph.id → 返回 PunchHoleResponse{other_failure:"不能远程自己…/You are connecting to yourself"},不打洞
src/rendezvous_server.rs handle_tcp RequestRelay 分支 补上缺失的 MUST_LOGIN token 校验(堵 force_relay 绕过口子)+ 同样的自连比对,命中则丢弃中继请求并记 warn 日志
src/main.rs --must-login 帮助文本标注 default Y
src/rendezvous_server.rs start_with_bind MUST_LOGIN 默认值改为 Y:仅当显式 --must-login=N 或 MUST_LOGIN=N 时关闭
docker-compose.yml 示例 env MUST_LOGIN=N → Y

运行配置:hbbs 与 API 同容器,RUSTDESK_API_JWT_KEY 双方共享(hbbs 的 jwt::SECRET 即 API 的 jwt.key); API 内部地址可用 RUSTDESK_API_INTERNAL_URL 覆盖,默认 http://127.0.0.1:21114。

⚠️ 部署必设 RUSTDESK_API_JWT_KEY(任意字符串)。未设置时 hbbs 的 JWT 密钥为空, token 校验和自连拦截整体静默关闭(fail-open 设计,日志无报错,连接照常放行)。 新版本已在启动日志中显式告警:RUSTDESK_API_JWT_KEY is not set: ... DISABLED; 设置后日志显示 Self-connection blocking: enabled。修改该值后客户端需退出重新登录。

4.6 端到端验证结果(2026-09-06,全部通过)

验证方法:Python 脚本按 hbb_common bytes_codec 帧格式与 rendezvous.proto 手工构造 PunchHoleRequest 直连 hbbs 21116 端口(脚本:/root/rustdesk/build/punch_test.py)。

用例 结果
自连:token 属于设备 11010050,目标 11010050 ✅ 拦截,客户端收到 other_failure: "不能远程自己:发起方与目标是同一台设备 / You are connecting to yourself",hbbs 记 Blocked self-connection attempt warn 日志
误伤检查:token 属于设备 99999999,目标 11010050 ✅ 放行(映射返回 99999999 ≠ 目标),无误拦
无 token(MUST_LOGIN=Y) ✅ 拒绝:Connection failed, please login!
内部接口:正确密钥 + 回环 ✅ 返回 {"device_id":"11010050"}
内部接口:错误密钥 / 非回环来源 ✅ 均 403
MUST_LOGIN 默认值 ✅ 未配置任何 env 时日志显示 MUST_LOGIN=Y

注:验证容器中以 -k "" 跳过密钥交换直连明文路径(打洞处理代码与加密路径完全相同), 正式镜像仍按官方流程自动生成密钥对并对 TCP 连接先发 KeyExchange 阶段 1,由客户端正常完成握手。

5. 为什么其他做法不行 / 不选

做法 结论
拿 user_id 对比目标设备归属 误伤同账号多设备互连(RustDesk 的主场景)❌
拿来源 IP 对比目标注册 IP 误伤整个局域网 A→B(同一 NAT 出口)❌
API/前端层拦截 API 不在打洞路径上,没有拦截点 ❌
连接建立后检测/切断 直连流量不经服务器,无法感知 ❌
hbbs 直读 API 的 sqlite 双进程双 ORM 同库有锁风险,hbbs 耦合 API 表结构 ❌(见 4 节末)
rustdesk-api-web 前端加"目标==自己"提示 可选退路:只覆盖 webclient 点击路径,桌面客户端管不到;本质仍是客户端补丁 ⚠️
客户端修复(正统) 用户排除(客户端仓库太大)⛔
协议增强:PunchHoleRequest 加 my_id 字段 长期正解,proto3 加字段向后兼容,但需要改客户端;远期项,服务端将来可"有字段就校验、无字段跳过"

JWT 加 claim(v1 方案)降为备选:同样可行且无运行时查询,但改动面(改签发+改 Claims 结构) 比映射查询大,且登出不能即时失效设备身份;若将来想去掉对 API 的运行时依赖可切换。

对比项 v2 映射查询(推荐) v1 JWT 加 claim(备选)
JWT 结构 不动 加字段(向后兼容)
API 改动 加 1 个内部接口(映射已存在) 改签发逻辑
hbbs 改动 两处校验 + 查询缓存 两处校验
运行时依赖 查询时 API 参与一次(有缓存,fail-open) 无
登出语义 设备身份即时失效(增强) token 到期前一直有效

6. 覆盖范围与边缘情况

  • MUST_LOGIN=Y:webclient 与桌面客户端发起 → 100% 拦截 (LoginForm.Id → UserToken.DeviceId 提供设备身份)
  • MUST_LOGIN=N:已登录设备发起 → 拦截;未登录设备发起 → 放行(见第 4 节兜底规则, 套娃风险被明确接受,行为与现状一致)
  • MUST_LOGIN=Y 时未登录客户端(无 token)→ 本来就被拒绝("please login"),不存在漏网
  • API 暂时不可达 → fail-open,自连拦截暂时降级,正常连接不受影响
  • 边缘情况:客户端 ID 变更(重装/清数据)到重新登录之间的短窗口内,映射里的旧 device_id 与目标不匹配 → 可能漏拦或误拦一次,重新登录即恢复,影响很小
  • 实施时需抽查验证 UserToken.DeviceId 对各端(桌面/webclient/OAuth 登录)都已正确填充
  • 错误文案建议中英对照,走 other_failure

7. 名词澄清:hbbs 的代码就在本机

"hbbs"不是另一个项目,是 lejianwen/rustdesk-server 仓库编译出的二进制名(Cargo.toml 里 [[bin]] name = "hbbs"),主体代码即 src/rendezvous_server.rs。本机已有该仓库(刚完成 1.1.17 合并)、依赖子模块 hbb_common、完整容器编译链路——改完重新打镜像约十几分钟,无需再拉取任何大仓库(不动的 rustdesk 客户端仓库才是大家伙)。

8. 实施决策记录

  1. ✅ 按 v2 方案实施(API 内部查询接口 + hbbs 两处校验,JWT 结构不动,仅加 jti)
  2. ✅ 错误文案:不能远程自己:发起方与目标是同一台设备 / You are connecting to yourself
  3. ✅ 已顺带补上 RequestRelay 缺失的 token 校验(堵住 MUST_LOGIN 的绕过口子)
  4. ✅ 查不到映射(API 不可达)时 fail-open:只影响自连拦截,不拖累正常连接
  5. ✅ MUST_LOGIN 默认值改为 Y(显式配置 N 可关闭)
  6. ⏳ 远期项(未实施):PunchHoleRequest 增加 my_id 字段(需改客户端,proto3 向后兼容)

原始 README 内容

以下为 lejianwen/rustdesk-server 原始仓库的 README,本仓库在此基础上增加了 自连拦截(A 远程连 A)服务器端方案和 1.1.17 合并支持。

build

  • 解决当客户端登录了API账号时链接超时的问题
  • s6镜像添加了API支持,API开源地址 https://github.com/lejianwen/rustdesk-api
  • 是否必须登录才能链接, MUST_LOGIN 默认为 N,设置为 Y 则必须登录才能链接
  • RUSTDESK_API_JWT_KEY,设置后会通过JWT校验token的合法性
  • 支持client websocket (client >= 1.4.1)

docker镜像地址

 networks:
   rustdesk-net:
     external: false
 services:
   rustdesk:
     ports:
       - 21114:21114
       - 21115:21115
       - 21116:21116
       - 21116:21116/udp
       - 21117:21117
       - 21118:21118
       - 21119:21119
     image: lejianwen/rustdesk-server-s6:latest
     environment:
       - RELAY=<relay_server[:port]>
       - ENCRYPTED_ONLY=1
       - MUST_LOGIN=N
       - TZ=Asia/Shanghai
       - RUSTDESK_API_RUSTDESK_ID_SERVER=<id_server[:21116]>
       - RUSTDESK_API_RUSTDESK_RELAY_SERVER=<relay_server[:21117]>
       - RUSTDESK_API_RUSTDESK_API_SERVER=http://<api_server[:21114]>
       - RUSTDESK_API_KEY_FILE=/data/id_ed25519.pub
       - RUSTDESK_API_JWT_KEY=xxxxxx # jwt key
     volumes:
       - /data/rustdesk/server:/data
       - /data/rustdesk/api:/app/data #将数据库挂载
     networks:
       - rustdesk-net
     restart: unless-stopped
       

API功能截图

Api.png

commnd.png

更多查看 RustDesk Api


自行构建 • Docker • S6-overlay • 密钥 • Debian • 环境参数
[English] | [Deutsch] | [Nederlands] | [繁体中文]

RustDesk Server Program

下载

说明文件

自行搭建属于你的RustDesk服务器,所有的一切都是免费且开源的

如何自行构建

cargo build --release

执行后会在target/release目录下生成三个对应平台的可执行程序

  • hbbs - RustDesk ID/会和服务器
  • hbbr - RustDesk 中继服务器
  • rustdesk-utils - RustDesk 命令行工具

您可以在 releases 页面中找到最新的服务端软件。

如果您需要额外的功能支持,RustDesk 专业版服务器 获取更适合您。

如果您想开发自己的服务器,rustdesk-server-demo 应该会比直接使用这个仓库更简单快捷。

Docker 镜像

Docker镜像会在每次 GitHub 发布新的release版本时自动构建。我们提供两种类型的镜像。

Classic 传统镜像

这个类型的镜像是基于 ubuntu-20.04 进行构建,镜像仅包含两个主要的可执行程序(hbbr 和 hbbs)。它们可以通过以下tag在 Docker Hub 上获得:

架构 image:tag
amd64 lejianwen/rustdesk-server:latest
arm64v8 lejianwen/rustdesk-server:latest-arm64v8

您可以使用以下命令,直接通过 docker run 來启动这些镜像:

docker run --name hbbs --net=host -v "$PWD/data:/root" -d lejianwen/rustdesk-server:latest hbbs -r <relay-server-ip[:port]> 
docker run --name hbbr --net=host -v "$PWD/data:/root" -d lejianwen/rustdesk-server:latest hbbr 

或不使用 --net=host 参数启动, 但这样 P2P 直连功能将无法工作。

对于使用了 SELinux 的系统,您需要将 /root 替换为 /root:z,以保证容器的正常运行。或者,也可以通过添加参数 --security-opt label=disable 来完全禁用 SELinux 容器隔离。

docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 -v "$PWD/data:/root" -d lejianwen/rustdesk-server:latest hbbs -r <relay-server-ip[:port]> 
docker run --name hbbr -p 21117:21117 -p 21119:21119 -v "$PWD/data:/root" -d lejianwen/rustdesk-server:latest hbbr 

relay-server-ip 参数是运行这些容器的服务器的 IP 地址(或 DNS 名称)。如果你不想使用 21117 作为 hbbr 的服务端口,可使用可选参数 port 进行指定。

您也可以使用 docker-compose 进行构建,以下为配置示例:

version: '3'

networks:
  rustdesk-net:
    external: false

services:
  hbbs:
    container_name: hbbs
    ports:
      - 21115:21115
      - 21116:21116
      - 21116:21116/udp
      - 21118:21118
    image: lejianwen/rustdesk-server:latest
    command: hbbs -r rustdesk.example.com:21117
    volumes:
      - ./data:/root
    networks:
      - rustdesk-net
    depends_on:
      - hbbr
    restart: unless-stopped

  hbbr:
    container_name: hbbr
    ports:
      - 21117:21117
      - 21119:21119
    image: lejianwen/rustdesk-server:latest
    command: hbbr
    volumes:
      - ./data:/root
    networks:
      - rustdesk-net
    restart: unless-stopped

编辑第16行来指定你的中继服务器 (默认端口监听在 21117 的那一个)。 如果需要的话,您也可以编辑 volume 信息 (第 18 和 33 行)。

(感谢 @lukebarone 和 @QuiGonLeong 协助提供的 docker-compose 配置示例)

基于 S6-overlay 的镜像

这些镜像是针对 busybox:stable 构建的,并添加了可执行程序(hbbr 和 hbbs)以及 S6-overlay。 它们可以使用以下tag在 Docker hub 上获取:

架構 version image:tag
multiarch latest lejianwen/rustdesk-server-s6:latest
amd64 latest lejianwen/rustdesk-server-s6:latest-amd64
i386 latest lejianwen/rustdesk-server-s6:latest-i386
arm64v8 latest lejianwen/rustdesk-server-s6:latest-arm64v8
armv7 latest lejianwen/rustdesk-server-s6:latest-armv7
multiarch 2 lejianwen/rustdesk-server-s6:2
amd64 2 lejianwen/rustdesk-server-s6:2-amd64
i386 2 lejianwen/rustdesk-server-s6:2-i386
arm64v8 2 lejianwen/rustdesk-server-s6:2-arm64v8
armv7 2 lejianwen/rustdesk-server-s6:2-armv7
multiarch 2.0.0 lejianwen/rustdesk-server-s6:2.0.0
amd64 2.0.0 lejianwen/rustdesk-server-s6:2.0.0-amd64
i386 2.0.0 lejianwen/rustdesk-server-s6:2.0.0-i386
arm64v8 2.0.0 lejianwen/rustdesk-server-s6:2.0.0-arm64v8
armv7 2.0.0 lejianwen/rustdesk-server-s6:2.0.0-armv7

强烈建议您使用major version 或 latest tag 的 multiarch 架构的镜像。

S6-overlay 在此处作为监控程序,用以保证两个进程的运行,因此使用此镜像,您无需运行两个容器。

您可以使用 docker run 命令直接启动镜像,如下:

docker run --name rustdesk-server \ 
  --net=host \
  -e "RELAY=rustdeskrelay.example.com" \
  -e "ENCRYPTED_ONLY=1" \
  -v "$PWD/data:/data" -d lejianwen/rustdesk-server-s6:latest

或刪去 --net=host 参数, 但 P2P 直连功能将无法工作。

docker run --name rustdesk-server \
  -p 21115:21115 -p 21116:21116 -p 21116:21116/udp \
  -p 21117:21117 -p 21118:21118 -p 21119:21119 \
  -e "RELAY=rustdeskrelay.example.com" \
  -e "ENCRYPTED_ONLY=1" \
  -v "$PWD/data:/data" -d lejianwen/rustdesk-server-s6:latest

或着您也可以使用 docker-compose 文件:

version: '3'

services:
  rustdesk-server:
    container_name: rustdesk-server
    ports:
      - 21114:21114
      - 21115:21115
      - 21116:21116
      - 21116:21116/udp
      - 21117:21117
      - 21118:21118
      - 21119:21119
    image: lejianwen/rustdesk-server-s6:latest
    environment:
      - "RELAY=rustdesk.example.com:21117"
      - "ENCRYPTED_ONLY=1"
    volumes:
      - ./data:/data
    restart: unless-stopped

对于此容器镜像,除了在下面的环境变量部分指定的变量之外,您还可以使用以下环境变量

环境变量 是否可选 描述
RELAY 否 运行此容器的宿主机的 IP 地址/ DNS 名称
ENCRYPTED_ONLY 是 如果设置为 "1",将不接受未加密的连接。
KEY_PUB 是 密钥对中的公钥(Public Key)
KEY_PRIV 是 密钥对中的私钥(Private Key)

基于 S6-overlay 镜像的密钥管理

您可以将密钥对保存在 Docker volume 中,但我们建议不要将密钥写入文件系統中;因此,我们提供了一些方案。

在容器启动时,会检查密钥对是否存在(/data/id_ed25519.pub 和 /data/id_ed25519),如果其中一個密钥不存在,则会从环境变量或 Docker Secret 中重新生成它。 然后检查密钥对的可用性:如果公钥和私钥不匹配,容器将停止运行。 如果您未提供密钥,hbbs 将会在默认位置生成一个。

使用 ENV 存储密钥对

您可以使用 Docker 环境变量來存储密钥。如下:

docker run --name rustdesk-server \ 
  --net=host \
  -e "RELAY=rustdeskrelay.example.com" \
  -e "ENCRYPTED_ONLY=1" \
  -e "DB_URL=/db/db_v2.sqlite3" \
  -e "KEY_PRIV=FR2j78IxfwJNR+HjLluQ2Nh7eEryEeIZCwiQDPVe+PaITKyShphHAsPLn7So0OqRs92nGvSRdFJnE2MSyrKTIQ==" \
  -e "KEY_PUB=iEyskoaYRwLDy5+0qNDqkbPdpxr0kXRSZxNjEsqykyE=" \
  -v "$PWD/db:/db" -d lejianwen/rustdesk-server-s6:latest
version: '3'

services:
  rustdesk-server:
    container_name: rustdesk-server
    ports:
      - 21114:21114
      - 21115:21115
      - 21116:21116
      - 21116:21116/udp
      - 21117:21117
      - 21118:21118
      - 21119:21119
    image: lejianwen/rustdesk-server-s6:latest
    environment:
      - "RELAY=rustdesk.example.com:21117"
      - "ENCRYPTED_ONLY=1"
      - "DB_URL=/db/db_v2.sqlite3"
      - "KEY_PRIV=FR2j78IxfwJNR+HjLluQ2Nh7eEryEeIZCwiQDPVe+PaITKyShphHAsPLn7So0OqRs92nGvSRdFJnE2MSyrKTIQ=="
      - "KEY_PUB=iEyskoaYRwLDy5+0qNDqkbPdpxr0kXRSZxNjEsqykyE="
    volumes:
      - ./db:/db
    restart: unless-stopped

使用 Docker Secret 來保存密钥对

您还可以使用 Docker Secret 來保存密钥。 如果您使用 docker-compose 或 docker swarm,推荐您使用。 只需按照以下示例操作:

cat secrets/id_ed25519.pub | docker secret create key_pub -
cat secrets/id_ed25519 | docker secret create key_priv -
docker service create --name rustdesk-server \
  --secret key_priv --secret key_pub \
  --net=host \
  -e "RELAY=rustdeskrelay.example.com" \
  -e "ENCRYPTED_ONLY=1" \
  -e "DB_URL=/db/db_v2.sqlite3" \
  --mount "type=bind,source=$PWD/db,destination=/db" \
  lejianwen/rustdesk-server-s6:latest
version: '3'

services:
  rustdesk-server:
    container_name: rustdesk-server
    ports:
      - 21114:21114
      - 21115:21115
      - 21116:21116
      - 21116:21116/udp
      - 21117:21117
      - 21118:21118
      - 21119:21119
    image: lejianwen/rustdesk-server-s6:latest
    environment:
      - "RELAY=rustdesk.example.com:21117"
      - "ENCRYPTED_ONLY=1"
      - "DB_URL=/db/db_v2.sqlite3"
    volumes:
      - ./db:/db
    restart: unless-stopped
    secrets:
      - key_pub
      - key_priv

secrets:
  key_pub:
    file: secrets/id_ed25519.pub
  key_priv:
    file: secrets/id_ed25519      

如何生成密钥对

加密需要一对密钥;您可以按照前面所述提供它,但需要一个工具去生成密钥对。

您可以使用以下命令生成一对密钥:

/usr/bin/rustdesk-utils genkeypair

如果您沒有(或不想)在系统上安装 rustdesk-utils 套件,您可以使用 Docker 执行相同的命令:

docker run --rm --entrypoint /usr/bin/rustdesk-utils  lejianwen/rustdesk-server-s6:latest genkeypair

运行后的输出内容如下:

Public Key:  8BLLhtzUBU/XKAH4mep3p+IX4DSApe7qbAwNH9nv4yA=
Secret Key:  egAVd44u33ZEUIDTtksGcHeVeAwywarEdHmf99KM5ajwEsuG3NQFT9coAfiZ6nen4hfgNICl7upsDA0f2e/jIA==

.deb 套件

每个可执行文件都有单独的 .deb 套件可供使用,您可以在 releases 页面中找到它們。 這些套件适用于以下发行版:

  • Ubuntu 22.04 LTS
  • Ubuntu 20.04 LTS
  • Ubuntu 18.04 LTS
  • Debian 11 bullseye
  • Debian 10 buster

ENV 环境变量

可以使用这些环境变量参数來配置 hbbs 和 hbbr。 您可以像往常一样指定参数,或者使用 .env 文件。

参数 可执行文件 描述
ALWAYS_USE_RELAY hbbs 如果设定为 "Y",将关闭直接点对点连接功能
DB_URL hbbs 数据库配置
DOWNGRADE_START_CHECK hbbr 降级检查之前的延迟是啊尽(以秒为单位)
DOWNGRADE_THRESHOLD hbbr 降级检查的阈值(bit/ms)
KEY hbbs/hbbr 如果设置了此参数,将强制使用指定密钥对,如果设为 "_",则强制使用任意密钥
LIMIT_SPEED hbbr 速度限制(以Mb/s为单位)
PORT hbbs/hbbr 监听端口(hbbs为21116,hbbr为21117)
RELAY_SERVERS hbbs 运行hbbr的机器的IP地址/DNS名称(用逗号分隔)
RUST_LOG all 设置 debug level (error|warn|info|debug|trace)
2026-10-07 00:10:11 +08:00
Languages
Rust 88.7%
NSIS 3.8%
JavaScript 3.1%
Shell 3%
Dockerfile 0.5%
Other 0.9%