Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki
hbb_common = { path = "libs/hbb_common" }
openssl-sys = { version = "0.9", features = ["vendored"] }

[dev-dependencies]
hyper = "0.14"

[workspace]
members = ["libs/hbb_common"]
exclude = ["ui"]
Expand Down
10 changes: 7 additions & 3 deletions docs/management.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

The opt-in `/v1` HTTP API is independent of RustDesk's client protocol ports. Set `RD_MANAGEMENT_BIND` to a private socket address, `RD_MANAGEMENT_TOKEN` to an independent random Bearer credential of at least 32 characters, and `RD_MANAGEMENT_DIR` to a persistent directory shared with the node agent. Do not publish this listener to the client-facing network.

hbbs exposes `GET /v1/status`, `GET /v1/config`, `GET /v1/peers` and `GET/PUT /v1/bans`. hbbr additionally exposes `GET /v1/sessions` and `DELETE /v1/sessions/:uuid`; it has no peers endpoint. All endpoints require authorization. API version is 1. Session closure is asynchronous (`202`, `state: closing`). IP bans check both relay endpoints. Reported target IDs are not authenticated source identities.
hbbs exposes `GET /v1/status`, `GET /v1/config`, `PUT /v1/config`, `GET /v1/logs`, `GET /v1/peers` and `GET/PUT /v1/bans`. hbbr additionally exposes `GET /v1/sessions` and `DELETE /v1/sessions/:uuid`; it has no peers endpoint. All endpoints require authorization. API version is 1. Session closure is asynchronous (`202`, `state: closing`). IP bans check both relay endpoints. Reported target IDs are not authenticated source identities.

Files are service-specific: `hbbs-config.json`, `hbbr-config.json`, `hbbs-bans.json`, and `hbbr-bans.json`. Configurations use `{ "values": { "port": "21116" } }`, and policies use `{ "device_ids": [], "ips": [] }`. Saved bans are validated and restored before protocol startup even when `RD_MANAGEMENT_BIND` is empty. Disabling the HTTP listener does not clear admission policy; explicitly replace the bans with empty lists before disabling management if clearing is intended. Ban replacements serialize persistence outside the protocol admission lock and activate only after the write succeeds.
Files are service-specific: `hbbs-config.json`, `hbbr-config.json`, `hbbs-bans.json`, and `hbbr-bans.json`. Configurations use `{ "values": { "port": "21116" } }`, and policies use `{ "device_ids": [], "ips": [] }`. Saved bans are validated and restored before protocol startup even when `RD_MANAGEMENT_BIND` is empty. Disabling the HTTP listener does not clear admission policy; explicitly replace the bans with empty lists before disabling management if clearing is intended. Ban replacements serialize persistence outside the protocol admission lock and activate only after the write succeeds. Config updates follow the same contract: the merged desired configuration is persisted atomically first, and running state changes only after the write succeeds.

Managed settings override startup inputs; they require a restart. Schema and validation are provided by `management-schema.json`. The API returns key placeholders, never the private key. It exposes only `id_ed25519.pub` as the public key.
`PUT /v1/config` accepts a partial `{"values": {...}}` merge against the current desired configuration. Schema entries marked `"dynamic": true` take effect immediately without a restart: hbbr applies `downgrade-threshold`, `downgrade-start-check`, `limit-speed`, `total-bandwidth` and `single-bandwidth` live, and hbbs applies `always-use-relay` and `relay-servers` live. The response reports `applied` entries and `pending_restart` entries (with `current` and `pending` values); pending entries take effect on the next restart, when the saved file is loaded before the protocol starts. `GET /v1/config` returns the live effective values, the schema and the current `pending_restart` diff. Secret entries are stored in the clear in the shared directory but are always reported as `__REDACTED__`; submitting the placeholder keeps the stored value unchanged. A missing dynamic handler falls the entry back to restart-required handling.

`GET /v1/logs?tail=N` returns up to 2000 recent in-memory log lines (default 200) captured from the standard log format; it requires no container log access.

Managed settings override startup inputs. Schema and validation are provided by `management-schema.json`. The API returns key placeholders, never the private key. It exposes only `id_ed25519.pub` as the public key.

`docker-managed/Dockerfile` builds the managed hbbs/hbbr image in CI. The Console repository supplies the node agent, combined Compose deployment, permission enforcement, UI, integration workflow and detailed lifecycle documentation in `docs/server-management.md`. Existing binaries remain compatible with deployments that do not enable management.
40 changes: 32 additions & 8 deletions management-schema.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"version": 1,
"version": 2,
"hbbs": [
{
"name": "port",
"default": "21116",
"kind": "integer",
"secret": false,
"dynamic": false,
"restart_required": true,
"minimum": 3,
"maximum": 65533
Expand All @@ -15,27 +16,31 @@
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "relay-servers",
"default": "",
"kind": "string",
"secret": false,
"restart_required": true
"dynamic": true,
"restart_required": false
},
{
"name": "key",
"default": "-",
"kind": "string",
"secret": true,
"dynamic": false,
"restart_required": true
},
{
"name": "rmem",
"default": "0",
"kind": "integer",
"secret": false,
"dynamic": false,
"restart_required": true,
"minimum": 0,
"maximum": 2147483647
Expand All @@ -45,6 +50,7 @@
"default": "0",
"kind": "integer",
"secret": false,
"dynamic": false,
"restart_required": true,
"minimum": 0,
"maximum": 2147483647
Expand All @@ -54,41 +60,47 @@
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "software-url",
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "mask",
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "local-ip",
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "db-url",
"default": "./db_v2.sqlite3",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "max-database-connections",
"default": "1",
"kind": "integer",
"secret": false,
"dynamic": false,
"restart_required": true,
"minimum": 1,
"maximum": 1024
Expand All @@ -98,20 +110,23 @@
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "always-use-relay",
"default": "N",
"kind": "relay-mode",
"secret": false,
"restart_required": true
"dynamic": true,
"restart_required": false
},
{
"name": "log",
"default": "info",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
}
],
Expand All @@ -121,6 +136,7 @@
"default": "21117",
"kind": "integer",
"secret": false,
"dynamic": false,
"restart_required": true,
"minimum": 1,
"maximum": 65533
Expand All @@ -130,21 +146,24 @@
"default": "",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
},
{
"name": "key",
"default": "",
"kind": "string",
"secret": true,
"dynamic": false,
"restart_required": true
},
{
"name": "downgrade-threshold",
"default": "0.66",
"kind": "number",
"secret": false,
"restart_required": true,
"dynamic": true,
"restart_required": false,
"minimum": 0.01,
"maximum": 1
},
Expand All @@ -153,7 +172,8 @@
"default": "1800",
"kind": "integer",
"secret": false,
"restart_required": true,
"dynamic": true,
"restart_required": false,
"minimum": 1,
"maximum": 4294967
},
Expand All @@ -162,7 +182,8 @@
"default": "32",
"kind": "number",
"secret": false,
"restart_required": true,
"dynamic": true,
"restart_required": false,
"minimum": 0.01,
"maximum": 1000000
},
Expand All @@ -171,7 +192,8 @@
"default": "1024",
"kind": "number",
"secret": false,
"restart_required": true,
"dynamic": true,
"restart_required": false,
"minimum": 0.01,
"maximum": 1000000
},
Expand All @@ -180,7 +202,8 @@
"default": "128",
"kind": "number",
"secret": false,
"restart_required": true,
"dynamic": true,
"restart_required": false,
"minimum": 0.01,
"maximum": 1000000
},
Expand All @@ -189,6 +212,7 @@
"default": "info",
"kind": "string",
"secret": false,
"dynamic": false,
"restart_required": true
}
]
Expand Down
14 changes: 14 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ use std::{
time::{Instant, SystemTime},
};

// Mirrors the standard opt_format output while retaining a bounded in-memory
// copy so the management API can serve recent log lines without any container
// log access.
pub fn log_format(
w: &mut dyn std::io::Write,
now: &mut flexi_logger::DeferredNow,
record: &log::Record,
) -> Result<(), std::io::Error> {
let mut buffer = Vec::new();
flexi_logger::opt_format(&mut buffer, now, record)?;
crate::management::push_log_line(&String::from_utf8_lossy(&buffer));
w.write_all(&buffer)
}

pub fn parse_bind_address(value: &str) -> Result<Option<IpAddr>> {
let value = value.trim();
if value.is_empty() {
Expand Down
2 changes: 1 addition & 1 deletion src/hbbr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn main() -> ResultType<()> {
managed.apply();
let _logger = Logger::try_with_env_or_str("info")?
.log_to_stdout()
.format(opt_format)
.format(common::log_format)
.write_mode(WriteMode::Async)
.start()?;
let args = format!(
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn main() -> ResultType<()> {
managed.apply();
let _logger = Logger::try_with_env_or_str("info")?
.log_to_stdout()
.format(opt_format)
.format(log_format)
.write_mode(WriteMode::Async)
.start()?;
let args = format!(
Expand Down
Loading
Loading