Files
rustdesk-server/README.md
T

840 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 关于此分支
> **原始仓库**: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 容器编译:
```sh
# 启动编译容器
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 基础镜像
```sh
# 拷贝二进制到 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)
```sh
# 启动 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 前端
```sh
# 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. 打完整镜像
```sh
# 修改 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. 验证运行
```sh
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 托管) |
| SINGLE_BANDWIDTH | hbbr | 单个连接的最大带宽(以Mb/s为单位) |
| TOTAL_BANDWIDTH | hbbr | 最大总带宽(以Mb/s为单位) |
---
# 1.1.17 合并记录
> 日期:2026-09-05
> 关联文档:[lejianwen/rustdesk-api README](https://github.com/lejianwen/rustdesk-api)
> 说明:将官方 `rustdesk/rustdesk-server` 1.1.17 合并到本仓库(forapi 分支),完整构建记录见 [lejianwen/rustdesk-api README](https://github.com/lejianwen/rustdesk-api)。
## 合并要点
- 官方 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](https://github.com/lejianwen/rustdesk-api)
>
> 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`:
```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`:
```go
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` 给客户端弹窗:
```rust
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](https://github.com/lejianwen/rustdesk-server/actions/workflows/build.yaml/badge.svg)](https://github.com/lejianwen/rustdesk-server/actions/workflows/build.yaml)
- 解决当客户端登录了`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镜像地址
- s6 镜像 [lejianwen/rustdesk-server-s6](https://hub.docker.com/r/lejianwen/rustdesk-server-s6)
```yaml
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
```
- 普通镜像 [lejianwen/rustdesk-server](https://hub.docker.com/r/lejianwen/rustdesk-server)
# API功能截图
![Api.png](./readme/api.png)
![commnd.png](./readme/command_simple.png)
更多查看 [RustDesk Api](https://github.com/lejianwen/rustdesk-api)
---
<p align="center">
<a href="#如何自行构建">自行构建</a> •
<a href="#Docker-镜像">Docker</a> •
<a href="#基于-S6-overlay-的镜像">S6-overlay</a> •
<a href="#如何创建密钥">密钥</a> •
<a href="#deb-套件">Debian</a> •
<a href="#ENV-环境参数">环境参数</a><br>
[<a href="README-EN.md">English</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-TW.md">繁体中文</a>]<br>
</p>
# RustDesk Server Program
[**下载**](https://github.com/lejianwen/rustdesk-server/releases)
[**说明文件**](https://rustdesk.com/docs/zh-cn/self-host/)
自行搭建属于你的RustDesk服务器,所有的一切都是免费且开源的
## 如何自行构建
```bash
cargo build --release
```
执行后会在target/release目录下生成三个对应平台的可执行程序
- hbbs - RustDesk ID/会和服务器
- hbbr - RustDesk 中继服务器
- rustdesk-utils - RustDesk 命令行工具
您可以在 [releases](https://github.com/lejianwen/rustdesk-server/releases) 页面中找到最新的服务端软件。
如果您需要额外的功能支持,[RustDesk 专业版服务器](https://rustdesk.com/pricing.html) 获取更适合您。
如果您想开发自己的服务器,[rustdesk-server-demo](https://github.com/rustdesk/rustdesk-server-demo) 应该会比直接使用这个仓库更简单快捷。
## Docker 镜像
Docker镜像会在每次 GitHub 发布新的release版本时自动构建。我们提供两种类型的镜像。
### Classic 传统镜像
这个类型的镜像是基于 `ubuntu-20.04` 进行构建,镜像仅包含两个主要的可执行程序(`hbbr` 和 `hbbs`)。它们可以通过以下tag在 [Docker Hub](https://hub.docker.com/r/lejianwen/rustdesk-server/) 上获得:
| 架构 | image:tag |
|---------| ----------------------------------------- |
| amd64 | `lejianwen/rustdesk-server:latest` |
| arm64v8 | `lejianwen/rustdesk-server:latest-arm64v8` |
您可以使用以下命令,直接通过 ``docker run`` 來启动这些镜像:
```bash
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 容器隔离。
```bash
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 进行构建,以下为配置示例:
```yaml
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](https://github.com/just-containers/s6-overlay)。 它们可以使用以下tag在 [Docker hub](https://hub.docker.com/r/lejianwen/rustdesk-server-s6/) 上获取:
| 架構 | 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` 命令直接启动镜像,如下:
```bash
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 直连功能将无法工作。
```bash
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 文件:
```yaml
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 环境变量來存储密钥。如下:
```bash
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
```
```yaml
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**,推荐您使用。
只需按照以下示例操作:
```bash
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
```
```yaml
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
```
## 如何生成密钥对
加密需要一对密钥;您可以按照前面所述提供它,但需要一个工具去生成密钥对。
您可以使用以下命令生成一对密钥:
```bash
/usr/bin/rustdesk-utils genkeypair
```
如果您沒有(或不想)在系统上安装 `rustdesk-utils` 套件,您可以使用 Docker 执行相同的命令:
```bash
docker run --rm --entrypoint /usr/bin/rustdesk-utils lejianwen/rustdesk-server-s6:latest genkeypair
```
运行后的输出内容如下:
```text
Public Key: 8BLLhtzUBU/XKAH4mep3p+IX4DSApe7qbAwNH9nv4yA=
Secret Key: egAVd44u33ZEUIDTtksGcHeVeAwywarEdHmf99KM5ajwEsuG3NQFT9coAfiZ6nen4hfgNICl7upsDA0f2e/jIA==
```
## .deb 套件
每个可执行文件都有单独的 .deb 套件可供使用,您可以在 [releases](https://github.com/lejianwen/rustdesk-server/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) |