From fb45b08697dfd89e21ab74d435f27767cce5fa7b Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 2 Oct 2026 19:47:04 +0800 Subject: [PATCH 1/7] feat: expose private management APIs for hbbs and hbbr --- .dockerignore | 5 + .github/workflows/test.yaml | 25 +- docker-managed/Dockerfile | 13 + docs/management.md | 9 + management-schema.json | 195 +++++++++++ src/hbbr.rs | 26 +- src/lib.rs | 1 + src/main.rs | 5 +- src/management.rs | 465 +++++++++++++++++++++++++++ src/peer.rs | 21 ++ src/relay_server/connection.rs | 115 +++++-- src/relay_server/mod.rs | 15 +- src/relay_server/server.rs | 38 +-- src/rendezvous_server/listener.rs | 32 +- src/rendezvous_server/punch_hole.rs | 34 +- src/rendezvous_server/server.rs | 19 +- src/rendezvous_server/tcp_handler.rs | 59 +++- src/rendezvous_server/udp_handler.rs | 31 +- 18 files changed, 995 insertions(+), 113 deletions(-) create mode 100644 .dockerignore create mode 100644 docker-managed/Dockerfile create mode 100644 docs/management.md create mode 100644 management-schema.json create mode 100644 src/management.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f24815 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +**/.git +target +ui +.env diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 284971a..b722a6f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,11 +1,26 @@ name: Test on: + workflow_dispatch: push: branches: [master] pull_request: jobs: + managed-image: + name: Managed server image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + - uses: docker/setup-buildx-action@v4 + - uses: docker/build-push-action@v7 + with: + context: . + file: docker-managed/Dockerfile + push: false + check: name: Compile Check runs-on: ubuntu-latest @@ -21,11 +36,14 @@ jobs: - uses: Swatinem/rust-cache@v2 + - name: Install native build dependencies + run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev libsodium-dev libx11-dev cmake clang + - name: Check compilation uses: actions-rs/cargo@v1 with: command: check - args: --all-features + args: --locked --all-features --all-targets test: name: Run Tests @@ -42,8 +60,11 @@ jobs: - uses: Swatinem/rust-cache@v2 + - name: Install native build dependencies + run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev libsodium-dev libx11-dev cmake clang + - name: Run tests uses: actions-rs/cargo@v1 with: command: test - args: --all-features + args: --locked --all-features diff --git a/docker-managed/Dockerfile b/docker-managed/Dockerfile new file mode 100644 index 0000000..7894076 --- /dev/null +++ b/docker-managed/Dockerfile @@ -0,0 +1,13 @@ +FROM rust:1.90-bookworm AS build +RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev libsodium-dev libx11-dev cmake clang && rm -rf /var/lib/apt/lists/* +WORKDIR /src +COPY . . +# sqlx query macros validate against the checked-in schema database at build time. +ENV DATABASE_URL=sqlite:///src/db_v2.sqlite3 +RUN cargo build --locked --release --bin hbbs --bin hbbr + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 libsodium23 libx11-6 && rm -rf /var/lib/apt/lists/* +COPY --from=build /src/target/release/hbbs /src/target/release/hbbr /usr/local/bin/ +WORKDIR /data +CMD ["hbbs"] diff --git a/docs/management.md b/docs/management.md new file mode 100644 index 0000000..4741a2c --- /dev/null +++ b/docs/management.md @@ -0,0 +1,9 @@ +# Console management integration + +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. + +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": [] }`. 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. + +`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. diff --git a/management-schema.json b/management-schema.json new file mode 100644 index 0000000..4525c6a --- /dev/null +++ b/management-schema.json @@ -0,0 +1,195 @@ +{ + "version": 1, + "hbbs": [ + { + "name": "port", + "default": "21116", + "kind": "integer", + "secret": false, + "restart_required": true, + "minimum": 3, + "maximum": 65533 + }, + { + "name": "bind", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "relay-servers", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "key", + "default": "-", + "kind": "string", + "secret": true, + "restart_required": true + }, + { + "name": "rmem", + "default": "0", + "kind": "integer", + "secret": false, + "restart_required": true, + "minimum": 0, + "maximum": 2147483647 + }, + { + "name": "serial", + "default": "0", + "kind": "integer", + "secret": false, + "restart_required": true, + "minimum": 0, + "maximum": 2147483647 + }, + { + "name": "rendezvous-servers", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "software-url", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "mask", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "local-ip", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "db-url", + "default": "./db_v2.sqlite3", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "max-database-connections", + "default": "1", + "kind": "integer", + "secret": false, + "restart_required": true, + "minimum": 1, + "maximum": 1024 + }, + { + "name": "test-hbbs", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "always-use-relay", + "default": "N", + "kind": "relay-mode", + "secret": false, + "restart_required": true + }, + { + "name": "log", + "default": "info", + "kind": "string", + "secret": false, + "restart_required": true + } + ], + "hbbr": [ + { + "name": "port", + "default": "21117", + "kind": "integer", + "secret": false, + "restart_required": true, + "minimum": 1, + "maximum": 65533 + }, + { + "name": "bind", + "default": "", + "kind": "string", + "secret": false, + "restart_required": true + }, + { + "name": "key", + "default": "", + "kind": "string", + "secret": true, + "restart_required": true + }, + { + "name": "downgrade-threshold", + "default": "0.66", + "kind": "number", + "secret": false, + "restart_required": true, + "minimum": 0.01, + "maximum": 1 + }, + { + "name": "downgrade-start-check", + "default": "1800", + "kind": "integer", + "secret": false, + "restart_required": true, + "minimum": 1, + "maximum": 4294967 + }, + { + "name": "limit-speed", + "default": "32", + "kind": "number", + "secret": false, + "restart_required": true, + "minimum": 0.01, + "maximum": 1000000 + }, + { + "name": "total-bandwidth", + "default": "1024", + "kind": "number", + "secret": false, + "restart_required": true, + "minimum": 0.01, + "maximum": 1000000 + }, + { + "name": "single-bandwidth", + "default": "128", + "kind": "number", + "secret": false, + "restart_required": true, + "minimum": 0.01, + "maximum": 1000000 + }, + { + "name": "log", + "default": "info", + "kind": "string", + "secret": false, + "restart_required": true + } + ] +} diff --git a/src/hbbr.rs b/src/hbbr.rs index e6763fa..ff4b99a 100644 --- a/src/hbbr.rs +++ b/src/hbbr.rs @@ -1,5 +1,6 @@ use clap::App; mod common; +mod management; mod relay_server; use flexi_logger::*; use hbb_common::{config::RELAY_PORT, ResultType}; @@ -7,6 +8,8 @@ use relay_server::*; mod version; fn main() -> ResultType<()> { + let managed = management::load_config("hbbr")?; + managed.apply(); let _logger = Logger::try_with_env_or_str("info")? .log_to_stdout() .format(opt_format) @@ -29,6 +32,7 @@ fn main() -> ResultType<()> { section.iter().for_each(|(k, v)| common::set_arg(k, v)); } } + managed.apply(); let mut port = RELAY_PORT; if let Some(v) = common::get_arg_opt("PORT") { let v: i32 = v.parse().unwrap_or_default(); @@ -45,10 +49,22 @@ fn main() -> ResultType<()> { .value_of("key") .map(str::to_owned) .unwrap_or_else(|| common::get_arg("KEY")); - start_with_bind( - bind_addr, - matches.value_of("port").unwrap_or(&port.to_string()), - &key, - )?; + let bind_addr = if managed.values.contains_key("bind") { + common::parse_bind_address(&common::get_arg("bind"))? + } else { + bind_addr + }; + let key = managed.values.get("key").cloned().unwrap_or(key); + let port = managed.values.get("port").cloned().unwrap_or_else(|| { + matches + .value_of("port") + .map(str::to_owned) + .unwrap_or_else(|| port.to_string()) + }); + common::set_arg( + "bind", + &bind_addr.map(|ip| ip.to_string()).unwrap_or_default(), + ); + start_with_bind(bind_addr, &port, &key)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 8da29a2..ca279c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,5 +2,6 @@ mod rendezvous_server; pub use rendezvous_server::*; pub mod common; mod database; +pub mod management; mod peer; mod version; diff --git a/src/main.rs b/src/main.rs index d422c3c..b416f9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,8 @@ use hbbs::{common::*, *}; const RMEM: usize = 0; fn main() -> ResultType<()> { + let managed = management::load_config("hbbs")?; + managed.apply(); let _logger = Logger::try_with_env_or_str("info")? .log_to_stdout() .format(opt_format) @@ -26,8 +28,9 @@ fn main() -> ResultType<()> { -k, --key=[KEY] 'Only allow the client with the same key'", ); init_args(&args, "hbbs", "RustDesk ID/Rendezvous Server"); + managed.apply(); let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::()?; - if port < 3 { + if !(3..=65533).contains(&port) { bail!("Invalid port"); } let bind_addr = parse_bind_address(&get_arg("bind"))?; diff --git a/src/management.rs b/src/management.rs new file mode 100644 index 0000000..d930a0e --- /dev/null +++ b/src/management.rs @@ -0,0 +1,465 @@ +//! Private, opt-in management API. Client protocol ports are never used for HTTP. +use axum::{ + body::Body, + extract::{Extension, Path}, + http::{Request, StatusCode}, + middleware::{self, Next}, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; +use hbb_common::{anyhow::anyhow, log, tokio, ResultType}; +use once_cell::sync::Lazy; +use serde_derive::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::{ + collections::{BTreeMap, HashMap}, + net::IpAddr, + path::PathBuf, + sync::RwLock, + time::Instant, +}; +use tokio::sync::oneshot; + +pub const REDACTED: &str = "__REDACTED__"; +static PEERS: Lazy>> = Lazy::new(|| RwLock::new(Vec::new())); +static SESSIONS: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); +static BANS: Lazy> = Lazy::new(|| RwLock::new(Bans::default())); + +#[derive(Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct Bans { + pub device_ids: Vec, + pub ips: Vec, +} + +impl Bans { + fn validate(&mut self) -> ResultType<()> { + if self.device_ids.len() > 10000 || self.ips.len() > 10000 { + return Err(anyhow!("Too many ban rules")); + } + if self + .device_ids + .iter() + .any(|id| id.is_empty() || id.len() > 100 || id.chars().any(char::is_whitespace)) + { + return Err(anyhow!("Invalid device ID")); + } + self.ips = self + .ips + .iter() + .map(|ip| ip.parse::().map(normalize_ip)) + .collect::, _>>()?; + self.device_ids.sort(); + self.device_ids.dedup(); + self.ips.sort(); + self.ips.dedup(); + Ok(()) + } + + fn contains(&self, id: &str, ip: &str) -> bool { + let normalized = ip + .parse::() + .map(normalize_ip) + .unwrap_or_else(|_| ip.to_owned()); + (!id.is_empty() && self.device_ids.iter().any(|v| v == id)) + || self.ips.iter().any(|v| v == &normalized) + } +} + +fn normalize_ip(ip: IpAddr) -> String { + match ip { + IpAddr::V6(ip) => ip + .to_ipv4_mapped() + .map(|ip| ip.to_string()) + .unwrap_or_else(|| ip.to_string()), + ip => ip.to_string(), + } +} + +pub fn is_banned(id: &str, ip: &str) -> bool { + BANS.read().unwrap().contains(id, ip) +} + +pub fn set_peers(peers: Vec) { + *PEERS.write().unwrap() = peers; +} + +struct Session { + info: Value, + ids: Vec, + ips: Vec, + cancel: Option>, +} + +pub fn open_session( + uuid: &str, + ids: Vec, + ips: Vec, + info: Value, +) -> Option> { + // Admission and policy updates use the same lock order. A ban cannot race + // between admission checking and publishing the cancellation handle. + let bans = BANS.read().unwrap(); + if ids.iter().any(|id| bans.contains(id, "")) || ips.iter().any(|ip| bans.contains("", ip)) { + return None; + } + let mut sessions = SESSIONS.write().unwrap(); + if sessions.contains_key(uuid) { + return None; + } + let (tx, rx) = oneshot::channel(); + sessions.insert( + uuid.to_owned(), + Session { + info, + ids, + ips, + cancel: Some(tx), + }, + ); + Some(rx) +} + +pub fn update_session(uuid: &str, bytes: usize, speed: usize) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(uuid) { + session.info["bytes"] = json!(bytes); + session.info["bytes_per_second"] = json!(speed); + } +} + +pub fn close_session(uuid: &str) { + SESSIONS.write().unwrap().remove(uuid); +} + +#[derive(Clone, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ManagedConfig { + pub values: BTreeMap, +} + +pub fn data_path(service: &str, kind: &str) -> PathBuf { + PathBuf::from(std::env::var("RD_MANAGEMENT_DIR").unwrap_or_else(|_| ".management".to_owned())) + .join(format!("{service}-{kind}.json")) +} + +pub fn schema(service: &str) -> Vec { + let schema: Value = serde_json::from_str(include_str!("../management-schema.json")).unwrap(); + schema[service].as_array().unwrap().clone() +} + +pub fn load_config(service: &str) -> ResultType { + let path = data_path(service, "config"); + let config: ManagedConfig = match std::fs::read(&path) { + Ok(bytes) => serde_json::from_slice(&bytes)?, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => ManagedConfig::default(), + Err(err) => return Err(err.into()), + }; + let definitions = schema(service); + for (name, value) in &config.values { + let definition = definitions + .iter() + .find(|d| d["name"].as_str() == Some(name.as_str())) + .ok_or_else(|| anyhow!("Unknown managed configuration: {name}"))?; + if value.len() > 4096 || value.contains('\0') || value == REDACTED { + return Err(anyhow!("Invalid value for {name}")); + } + if definition["kind"] == "integer" || definition["kind"] == "number" { + if definition["kind"] == "integer" { + // Startup consumers parse integers rather than floating point strings. + value.parse::()?; + } + let number: f64 = value.parse()?; + if !number.is_finite() + || number < definition["minimum"].as_f64().unwrap() + || number > definition["maximum"].as_f64().unwrap() + || (definition["kind"] == "integer" && number.fract() != 0.0) + { + return Err(anyhow!("Out of range: {name}")); + } + } + if name == "bind" { + crate::common::parse_bind_address(value)?; + } + if name == "always-use-relay" && value != "Y" && value != "N" { + return Err(anyhow!("Expected Y or N")); + } + } + Ok(config) +} + +impl ManagedConfig { + pub fn apply(&self) { + for (name, value) in &self.values { + if name == "log" { + std::env::set_var("RUST_LOG", value); + } else { + crate::common::set_arg(name, value); + } + } + } +} + +#[derive(Clone)] +struct ApiState { + service: &'static str, + token: String, + started: Instant, + values: BTreeMap, +} + +async fn authorize( + Extension(state): Extension, + request: Request, + next: Next, +) -> Response { + let expected = format!("Bearer {}", state.token); + let actual = request + .headers() + .get("authorization") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + // Fixed-length comparison without early exit on a mismatching byte. + let mut difference = actual.len() ^ expected.len(); + for (a, b) in actual.bytes().zip(expected.bytes()) { + difference |= (a ^ b) as usize; + } + if difference != 0 { + return StatusCode::UNAUTHORIZED.into_response(); + } + next.run(request).await +} + +async fn status(Extension(state): Extension) -> Json { + Json( + json!({"api_version":1,"service":state.service,"version":crate::version::VERSION, + "uptime_seconds":state.started.elapsed().as_secs(), + "public_key":std::fs::read_to_string("id_ed25519.pub").ok().map(|v| v.trim().to_owned())}), + ) +} + +async fn config(Extension(state): Extension) -> Json { + Json(json!({"schema":schema(state.service),"values":state.values})) +} + +async fn peers(Extension(state): Extension) -> Response { + if state.service != "hbbs" { + return StatusCode::NOT_FOUND.into_response(); + } + Json(json!({"data":PEERS.read().unwrap().clone()})).into_response() +} + +async fn sessions(Extension(state): Extension) -> Response { + if state.service != "hbbr" { + return StatusCode::NOT_FOUND.into_response(); + } + let mut data: Vec = SESSIONS + .read() + .unwrap() + .values() + .map(|s| s.info.clone()) + .collect(); + data.sort_by(|a, b| a["uuid"].as_str().cmp(&b["uuid"].as_str())); + Json(json!({"data":data})).into_response() +} + +async fn disconnect(Extension(state): Extension, Path(uuid): Path) -> Response { + if state.service != "hbbr" { + return StatusCode::NOT_FOUND.into_response(); + } + let mut sessions = SESSIONS.write().unwrap(); + if let Some(session) = sessions.get_mut(&uuid) { + if let Some(tx) = session.cancel.take() { + let _ = tx.send(()); + } + session.info["closing"] = json!(true); + return (StatusCode::ACCEPTED, Json(json!({"state":"closing"}))).into_response(); + } + StatusCode::NOT_FOUND.into_response() +} + +async fn bans() -> Json { + Json(BANS.read().unwrap().clone()) +} + +async fn replace_bans( + Extension(state): Extension, + Json(mut bans): Json, +) -> Response { + if let Err(err) = bans.validate() { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"error":err.to_string()})), + ) + .into_response(); + } + let mut current = BANS.write().unwrap(); + if *current == bans { + return Json(json!({"state":"applied"})).into_response(); + } + if let Err(err) = persist_bans(state.service, &bans) { + log::error!("Unable to persist management bans: {err}"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + *current = bans; + cancel_banned_sessions(¤t); + Json(json!({"state":"applied"})).into_response() +} + +fn cancel_banned_sessions(bans: &Bans) { + for session in SESSIONS.write().unwrap().values_mut() { + if session.ids.iter().any(|id| bans.contains(id, "")) + || session.ips.iter().any(|ip| bans.contains("", ip)) + { + if let Some(tx) = session.cancel.take() { + let _ = tx.send(()); + } + session.info["closing"] = json!(true); + } + } +} + +fn persist_bans(service: &str, bans: &Bans) -> ResultType<()> { + let path = data_path(service, "bans"); + std::fs::create_dir_all(path.parent().unwrap())?; + let temporary = path.with_extension("tmp"); + std::fs::write(&temporary, serde_json::to_vec(bans)?)?; + std::fs::rename(temporary, path)?; + Ok(()) +} + +pub fn start(service: &'static str, port: u16) -> ResultType<()> { + let bind = std::env::var("RD_MANAGEMENT_BIND").unwrap_or_default(); + if bind.is_empty() { + return Ok(()); + } + let token = std::env::var("RD_MANAGEMENT_TOKEN").unwrap_or_default(); + if token.len() < 32 { + return Err(anyhow!( + "RD_MANAGEMENT_TOKEN must contain at least 32 characters" + )); + } + match std::fs::read(data_path(service, "bans")) { + Ok(bytes) => { + let mut bans: Bans = serde_json::from_slice(&bytes)?; + bans.validate()?; + *BANS.write().unwrap() = bans; + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + let mut values = BTreeMap::new(); + for definition in schema(service) { + let name = definition["name"].as_str().unwrap(); + let value = if name == "port" { + port.to_string() + } else if name == "log" { + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()) + } else { + crate::common::get_arg_or(name, definition["default"].as_str().unwrap().to_owned()) + }; + values.insert( + name.to_owned(), + if definition["secret"] == true { + REDACTED.to_owned() + } else { + value + }, + ); + } + let state = ApiState { + service, + token, + started: Instant::now(), + values, + }; + let router = router(state); + let listener = std::net::TcpListener::bind(&bind)?; + listener.set_nonblocking(true)?; + let server = axum::Server::from_tcp(listener)?.serve(router.into_make_service()); + log::info!("Management API for {service} listening on {bind}"); + tokio::spawn(async move { + if let Err(err) = server.await { + log::error!("Management API failed: {err}"); + } + }); + Ok(()) +} + +fn router(state: ApiState) -> Router { + Router::new() + .route("/v1/status", get(status)) + .route("/v1/config", get(config)) + .route("/v1/peers", get(peers)) + .route("/v1/sessions", get(sessions)) + .route("/v1/sessions/:uuid", axum::routing::delete(disconnect)) + .route("/v1/bans", get(bans).put(replace_bans)) + .layer(middleware::from_fn(authorize)) + .layer(Extension(state)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + async fn http_routes_require_authorization_and_do_not_leak_tokens() { + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + listener.set_nonblocking(true).unwrap(); + let address = listener.local_addr().unwrap(); + let token = "test-management-token-that-is-at-least-32-characters"; + let app = router(ApiState { + service: "hbbs", + token: token.into(), + started: Instant::now(), + values: BTreeMap::new(), + }); + let task = tokio::spawn( + axum::Server::from_tcp(listener) + .unwrap() + .serve(app.into_make_service()), + ); + for (credential, expected) in [("incorrect", "401"), (token, "200")] { + let mut stream = TcpStream::connect(address).await.unwrap(); + let request = format!("GET /v1/status HTTP/1.1\r\nHost: localhost\r\nAuthorization: Bearer {credential}\r\nConnection: close\r\n\r\n"); + stream.write_all(request.as_bytes()).await.unwrap(); + let mut response = String::new(); + stream.read_to_string(&mut response).await.unwrap(); + assert!(response.starts_with(&format!("HTTP/1.1 {expected}"))); + assert!(!response.contains(token)); + } + task.abort(); + } + #[test] + fn policy_normalizes_addresses_and_checks_both_rule_types() { + let mut bans = Bans { + device_ids: vec!["123456".into()], + ips: vec!["::ffff:192.0.2.1".into()], + }; + bans.validate().unwrap(); + assert!(bans.contains("123456", "192.0.2.2")); + assert!(bans.contains("", "192.0.2.1")); + assert!(!bans.contains("654321", "192.0.2.2")); + assert_eq!(bans.ips, vec!["192.0.2.1"]); + } + #[test] + fn bans_cancel_existing_sessions_and_duplicates_are_rejected() { + let mut rx = open_session( + "test-session", + vec!["123456".into()], + vec!["192.0.2.1".into()], + json!({"uuid":"test-session"}), + ) + .unwrap(); + assert!(open_session("test-session", vec![], vec![], json!({})).is_none()); + cancel_banned_sessions(&Bans { + device_ids: vec![], + ips: vec!["192.0.2.1".into()], + }); + assert!(rx.try_recv().is_ok()); + close_session("test-session"); + } +} diff --git a/src/peer.rs b/src/peer.rs index 0f218df..5457c79 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -168,6 +168,27 @@ impl PeerMap { tmp } + pub(crate) async fn management_snapshot(&self) -> Vec { + let entries: Vec<_> = self + .map + .read() + .await + .iter() + .map(|(id, peer)| (id.clone(), peer.clone())) + .collect(); + let mut snapshot = Vec::with_capacity(entries.len()); + for (id, peer) in entries { + let peer = peer.read().await; + let elapsed = peer.last_reg_time.elapsed().as_secs(); + let banned = crate::management::is_banned(&id, &peer.info.ip); + snapshot.push(serde_json::json!({"id":id,"uuid":base64::encode(&peer.uuid), + "address":peer.socket_addr.to_string(),"ip":peer.info.ip,"online":elapsed < 30 && !banned, + "last_registration_age_seconds":elapsed,"banned":banned})); + } + snapshot.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str())); + snapshot + } + #[inline] pub(crate) async fn get_in_memory(&self, id: &str) -> Option { self.map.read().await.get(id).cloned() diff --git a/src/relay_server/connection.rs b/src/relay_server/connection.rs index 4fe2e57..a362544 100644 --- a/src/relay_server/connection.rs +++ b/src/relay_server/connection.rs @@ -17,10 +17,7 @@ use hbb_common::{ }, ResultType, }; -use std::{ - net::SocketAddr, - sync::atomic::Ordering, -}; +use std::{net::SocketAddr, sync::atomic::Ordering}; pub(crate) async fn io_loop( listener: TcpListener, @@ -95,6 +92,9 @@ async fn handle_connection( return; } let ip = ip.to_string(); + if crate::management::is_banned("", &ip) { + return; + } if BLOCKLIST.read().await.get(&ip).is_some() { log::info!("{} blocked", ip); return; @@ -113,6 +113,7 @@ async fn make_pair( limiter: Limiter, ws: bool, ) -> ResultType<()> { + let socket_ip = addr.ip().to_string(); if ws { use tokio_tungstenite::tungstenite::handshake::server::{Request, Response}; let callback = |req: &Request, response: Response| { @@ -140,9 +141,16 @@ async fn make_pair( Ok(response) }; let ws_stream = tokio_tungstenite::accept_hdr_async(stream, callback).await?; - make_pair_(ws_stream, addr, key, limiter).await; + make_pair_(ws_stream, addr, key, limiter, socket_ip).await; } else { - make_pair_(FramedStream::from(stream, addr), addr, key, limiter).await; + make_pair_( + FramedStream::from(stream, addr), + addr, + key, + limiter, + socket_ip, + ) + .await; } Ok(()) } @@ -152,6 +160,7 @@ async fn make_pair_( addr: SocketAddr, key: &str, limiter: Limiter, + socket_ip: String, ) { let mut stream = stream; if let Ok(Some(Ok(bytes))) = timeout(30_000, stream.recv()).await { @@ -161,30 +170,70 @@ async fn make_pair_( log::warn!("Relay authentication failed from {} - invalid key", addr); return; } - if !rf.uuid.is_empty() { - let mut peer = PEERS.lock().await.remove(&rf.uuid); - if let Some(peer) = peer.as_mut() { - log::info!("Relayrequest {} from {} got paired", rf.uuid, addr); - let id = format!("{}:{}", addr.ip(), addr.port()); - USAGE.write().await.insert(id.clone(), Default::default()); - if !stream.is_ws() && !peer.is_ws() { - peer.set_raw(); - stream.set_raw(); - log::info!("Both are raw"); - } - if let Err(err) = relay(addr, &mut stream, peer, limiter, id.clone()).await - { - log::info!("Relay of {} closed: {}", addr, err); - } else { - log::info!("Relay of {} closed", addr); + if !rf.uuid.is_empty() + && rf.uuid.len() <= 128 + && !crate::management::is_banned(&rf.id, &addr.ip().to_string()) + { + let mut peers = PEERS.lock().await; + let mut pending = match peers.remove(&rf.uuid) { + Some(pending) => pending, + None => { + let generation = uuid::Uuid::new_v4().to_string(); + peers.insert( + rf.uuid.clone(), + PendingPeer { + stream: Box::new(stream), + address: addr, + target_id: rf.id, + socket_ip, + generation: generation.clone(), + }, + ); + drop(peers); + sleep(30.).await; + let mut peers = PEERS.lock().await; + if peers.get(&rf.uuid).map(|p| &p.generation) == Some(&generation) { + peers.remove(&rf.uuid); + } + return; } - USAGE.write().await.remove(&id); - } else { - log::info!("New relay request {} from {}", rf.uuid, addr); - PEERS.lock().await.insert(rf.uuid.clone(), Box::new(stream)); - sleep(30.).await; - PEERS.lock().await.remove(&rf.uuid); + }; + drop(peers); + let peer = &mut pending.stream; + let info = serde_json::json!({"uuid":rf.uuid,"target_id":rf.id, + "peer_target_id":pending.target_id,"endpoints":[pending.address.to_string(),addr.to_string()], + "transport":if stream.is_ws() || peer.is_ws() {"websocket"} else {"tcp"}, + "started_at":crate::common::now(),"bytes":0,"bytes_per_second":0,"closing":false}); + let cancel = crate::management::open_session( + &rf.uuid, + vec![rf.id.clone(), pending.target_id.clone()], + vec![ + addr.ip().to_string(), + pending.address.ip().to_string(), + socket_ip, + pending.socket_ip.clone(), + ], + info, + ); + let mut cancel = match cancel { + Some(cancel) => cancel, + None => return, + }; + let id = format!("{}:{}", addr.ip(), addr.port()); + USAGE.write().await.insert(id.clone(), Default::default()); + if !stream.is_ws() && !peer.is_ws() { + peer.set_raw(); + stream.set_raw(); + } + let result = tokio::select! { + result = relay(addr, &mut stream, peer, limiter, id.clone(), &rf.uuid, pending.address) => result, + _ = &mut cancel => Ok(()), + }; + if let Err(err) = result { + log::info!("Relay {} closed: {}", rf.uuid, err); } + crate::management::close_session(&rf.uuid); + USAGE.write().await.remove(&id); } } } @@ -197,8 +246,11 @@ async fn relay( peer: &mut Box, total_limiter: Limiter, id: String, + session_uuid: &str, + peer_address: SocketAddr, ) -> ResultType<()> { let ip = addr.ip().to_string(); + let peer_ip = peer_address.ip().to_string(); let mut tm = std::time::Instant::now(); let mut elapsed = 0; let mut total = 0; @@ -262,13 +314,16 @@ async fn relay( let n = tm.elapsed().as_millis() as usize; if n >= 1_000 { - if BLOCKLIST.read().await.get(&ip).is_some() { + if BLOCKLIST.read().await.get(&ip).is_some() + || BLOCKLIST.read().await.get(&peer_ip).is_some() + { log::info!("{} blocked", ip); break; } blacked = BLACKLIST.read().await.get(&ip).is_some(); tm = std::time::Instant::now(); let speed = total_s / n; + crate::management::update_session(session_uuid, total / 8, total_s * 1000 / n / 8); if speed > highest_s { highest_s = speed; } @@ -293,4 +348,4 @@ async fn relay( } } Ok(()) -} \ No newline at end of file +} diff --git a/src/relay_server/mod.rs b/src/relay_server/mod.rs index 39baa7d..1c51dd9 100644 --- a/src/relay_server/mod.rs +++ b/src/relay_server/mod.rs @@ -5,9 +5,7 @@ mod server; mod stream; use async_speed_limit::Limiter; -use hbb_common::{ - tokio::sync::{Mutex, RwLock}, -}; +use hbb_common::tokio::sync::{Mutex, RwLock}; use std::{ collections::{HashMap, HashSet}, sync::atomic::{AtomicUsize, Ordering}, @@ -18,7 +16,7 @@ pub use server::start_with_bind; pub(crate) type Usage = (usize, usize, usize, usize); lazy_static::lazy_static! { - pub(crate) static ref PEERS: Mutex>> = Default::default(); + pub(crate) static ref PEERS: Mutex> = Default::default(); pub(crate) static ref USAGE: RwLock> = Default::default(); pub(crate) static ref BLACKLIST: RwLock> = Default::default(); pub(crate) static ref BLOCKLIST: RwLock> = Default::default(); @@ -32,3 +30,12 @@ pub(crate) static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * pub(crate) const BLACKLIST_FILE: &str = "blacklist.txt"; pub(crate) const BLOCKLIST_FILE: &str = "blocklist.txt"; + +// Pending streams retain both endpoints' metadata after pairing. +pub(crate) struct PendingPeer { + pub stream: Box, + pub address: std::net::SocketAddr, + pub target_id: String, + pub socket_ip: String, + pub generation: String, +} diff --git a/src/relay_server/server.rs b/src/relay_server/server.rs index 466d4fb..6231f45 100644 --- a/src/relay_server/server.rs +++ b/src/relay_server/server.rs @@ -1,20 +1,9 @@ use super::*; -use hbb_common::{ - log, - tokio, - ResultType, -}; -use std::{ - io::prelude::*, - net::IpAddr, -}; +use hbb_common::{log, tokio, ResultType}; +use std::{io::prelude::*, net::IpAddr}; #[tokio::main(flavor = "multi_thread")] -pub async fn start_with_bind( - bind_addr: Option, - port: &str, - key: &str, -) -> ResultType<()> { +pub async fn start_with_bind(bind_addr: Option, port: &str, key: &str) -> ResultType<()> { let key = helper::get_server_sk(key); if let Ok(mut file) = std::fs::File::open(BLACKLIST_FILE) { let mut contents = String::new(); @@ -47,19 +36,24 @@ pub async fn start_with_bind( BLOCKLIST.read().await.len() ); let port: u16 = port.parse()?; + if port == 0 || port > 65533 { + return Err(hbb_common::anyhow::anyhow!("Invalid relay port")); + } log::info!("Listening on tcp :{}", port); let port2 = port + 2; log::info!("Listening on websocket :{}", port2); let main_task = async move { + let mut management_started = false; loop { log::info!("Start"); - connection::io_loop( - crate::common::listen_tcp(bind_addr, port).await?, - crate::common::listen_tcp(bind_addr, port2).await?, - crate::common::listen_console(bind_addr, port).await?, - &key, - ) - .await; + let listener = crate::common::listen_tcp(bind_addr, port).await?; + let websocket = crate::common::listen_tcp(bind_addr, port2).await?; + let console = crate::common::listen_console(bind_addr, port).await?; + if !management_started { + crate::management::start("hbbr", port)?; + management_started = true; + } + connection::io_loop(listener, websocket, console, &key).await; } }; let listen_signal = crate::common::listen_signal(); @@ -67,4 +61,4 @@ pub async fn start_with_bind( res = main_task => res, res = listen_signal => res, ) -} \ No newline at end of file +} diff --git a/src/rendezvous_server/listener.rs b/src/rendezvous_server/listener.rs index 481c7e2..987b5f3 100644 --- a/src/rendezvous_server/listener.rs +++ b/src/rendezvous_server/listener.rs @@ -2,14 +2,10 @@ use super::*; use crate::common::*; use crate::peer::*; use hbb_common::{ - allow_err, - bail, + allow_err, bail, bytes::Bytes, bytes_codec::BytesCodec, - futures_util::{ - sink::SinkExt, - stream::StreamExt, - }, + futures_util::{sink::SinkExt, stream::StreamExt}, log, protobuf::Message as _, rendezvous_proto::*, @@ -22,8 +18,7 @@ use hbb_common::{ sync::Mutex, }, tokio_util::codec::Framed, - try_into_v4, - ResultType, + try_into_v4, ResultType, }; use sodiumoxide::crypto::{box_, sign}; use std::net::SocketAddr; @@ -46,6 +41,9 @@ impl RendezvousServer { }); return; } + if crate::management::is_banned("", &ip.to_string()) { + return; + } let stream = hbb_common::tcp::FramedStream::from(stream, addr); tokio::spawn(async move { let mut stream = stream; @@ -110,7 +108,11 @@ impl RendezvousServer { // keys[1] = encrypted symmetric key let client_box_pk_bytes = &ke.keys[0]; let symmetric_data = &ke.keys[1]; - match Encrypt::decode(symmetric_data, client_box_pk_bytes, &server_box_sk) { + match Encrypt::decode( + symmetric_data, + client_box_pk_bytes, + &server_box_sk, + ) { Ok(key) => { log::debug!("secure_tcp handshake completed"); return Ok((Some(Encrypt::new(key)), None)); @@ -280,7 +282,10 @@ impl RendezvousServer { // Process first message from fallback mode (client sent non-KeyExchange response) if let Some(bytes) = first_msg { if !bytes.is_empty() { - if !self.handle_tcp(&bytes, &mut sink, addr, key, ws, None).await { + if !self + .handle_tcp(&bytes, &mut sink, addr, key, ws, None) + .await + { return Ok(()); } } @@ -308,7 +313,10 @@ impl RendezvousServer { continue; } - if !self.handle_tcp(&bytes, &mut sink, addr, key, ws, None).await { + if !self + .handle_tcp(&bytes, &mut sink, addr, key, ws, None) + .await + { break; } } @@ -326,4 +334,4 @@ impl RendezvousServer { log::debug!("Tcp connection from {:?} closed", addr); Ok(()) } -} \ No newline at end of file +} diff --git a/src/rendezvous_server/punch_hole.rs b/src/rendezvous_server/punch_hole.rs index 0603531..6c538ea 100644 --- a/src/rendezvous_server/punch_hole.rs +++ b/src/rendezvous_server/punch_hole.rs @@ -1,16 +1,11 @@ use super::*; use crate::common::*; use crate::peer::*; +use hbb_common::bytes::BytesMut; use hbb_common::{ - log, - rendezvous_proto::*, - tcp::FramedStream, - try_into_v4, - udp::FramedSocket, - AddrMangle, + log, rendezvous_proto::*, tcp::FramedStream, try_into_v4, udp::FramedSocket, AddrMangle, ResultType, }; -use hbb_common::bytes::BytesMut; use std::net::SocketAddr; use std::time::Instant; @@ -93,7 +88,11 @@ impl RendezvousServer { ) -> ResultType<(RendezvousMessage, Option)> { let mut ph = ph; if !key.is_empty() && ph.licence_key != key { - log::warn!("Authentication failed from {} for peer {} - invalid key", addr, ph.id); + log::warn!( + "Authentication failed from {} for peer {} - invalid key", + addr, + ph.id + ); let mut msg_out = RendezvousMessage::new(); msg_out.set_punch_hole_response(PunchHoleResponse { failure: punch_hole_response::Failure::LICENSE_MISMATCH.into(), @@ -108,6 +107,14 @@ impl RendezvousServer { // because punch hole won't work if in the same intranet, // all routers will drop such self-connections. if let Some(peer) = self.pm.get(&id).await { + if crate::management::is_banned(&id, &peer.read().await.info.ip) { + let mut response = RendezvousMessage::new(); + response.set_punch_hole_response(PunchHoleResponse { + other_failure: "Blocked by server policy".to_owned(), + ..Default::default() + }); + return Ok((response, None)); + } let (elapsed, peer_addr) = { let r = peer.read().await; (r.last_reg_time.elapsed().as_millis() as i64, r.socket_addr) @@ -151,7 +158,8 @@ impl RendezvousServer { let peer_is_lan = self.is_lan(peer_addr); let is_lan = self.is_lan(addr); let mut relay_server = self.get_relay_server(addr.ip(), peer_addr.ip()); - if ALWAYS_USE_RELAY.load(std::sync::atomic::Ordering::SeqCst) || (peer_is_lan ^ is_lan) { + if ALWAYS_USE_RELAY.load(std::sync::atomic::Ordering::SeqCst) || (peer_is_lan ^ is_lan) + { if peer_is_lan { // https://github.com/rustdesk/rustdesk-server/issues/24 relay_server = self.inner.local_ip.clone() @@ -215,7 +223,11 @@ impl RendezvousServer { let mut states = BytesMut::zeroed((peers.len() + 7) / 8); for (i, peer_id) in peers.iter().enumerate() { if let Some(peer) = self.pm.get_in_memory(peer_id).await { - let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i64; + let peer = peer.read().await; + if crate::management::is_banned(peer_id, &peer.info.ip) { + continue; + } + let elapsed = peer.last_reg_time.elapsed().as_millis() as i64; // bytes index from left to right let states_idx = i / 8; let bit_idx = 7 - i % 8; @@ -234,4 +246,4 @@ impl RendezvousServer { Ok(()) } -} \ No newline at end of file +} diff --git a/src/rendezvous_server/server.rs b/src/rendezvous_server/server.rs index 55f9a6a..51b0d4b 100644 --- a/src/rendezvous_server/server.rs +++ b/src/rendezvous_server/server.rs @@ -2,8 +2,7 @@ use super::*; use crate::common::*; use crate::peer::*; use hbb_common::{ - allow_err, - config, + allow_err, config, futures_util::stream::StreamExt, log, rendezvous_proto::*, @@ -77,6 +76,19 @@ impl RendezvousServer { ws_map: Arc::new(hbb_common::tokio::sync::Mutex::new(HashMap::new())), tcp_map: Arc::new(hbb_common::tokio::sync::Mutex::new(HashMap::new())), }; + if std::env::var("RD_MANAGEMENT_BIND") + .map(|v| !v.is_empty()) + .unwrap_or(false) + { + let peers = rs.pm.clone(); + tokio::spawn(async move { + let mut timer = interval(Duration::from_secs(2)); + loop { + timer.tick().await; + crate::management::set_peers(peers.management_snapshot().await); + } + }); + } log::info!("mask: {:?}", rs.inner.mask); log::info!("local-ip: {:?}", rs.inner.local_ip); std::env::set_var("PORT_FOR_API", port.to_string()); @@ -85,6 +97,7 @@ impl RendezvousServer { let mut listener2 = helper::create_tcp_listener(bind_addr, nat_port).await?; let mut listener3 = helper::create_tcp_listener(bind_addr, ws_port).await?; let mut listener_console = listen_console(bind_addr, nat_port as _).await?; + crate::management::start("hbbs", port as u16)?; log::info!("Listening on tcp/udp {}", listener.local_addr()?); log::info!( "Listening on tcp {}, extra port for NAT test", @@ -267,4 +280,4 @@ impl RendezvousServer { } } } -} \ No newline at end of file +} diff --git a/src/rendezvous_server/tcp_handler.rs b/src/rendezvous_server/tcp_handler.rs index 1d85af4..46932e1 100644 --- a/src/rendezvous_server/tcp_handler.rs +++ b/src/rendezvous_server/tcp_handler.rs @@ -1,17 +1,15 @@ use super::*; use crate::common::*; use crate::peer::*; +use hbb_common::tokio::sync::Mutex; use hbb_common::{ allow_err, bytes::BytesMut, log, protobuf::{Message as _, MessageField}, rendezvous_proto::*, - try_into_v4, - AddrMangle, - ResultType, + try_into_v4, AddrMangle, ResultType, }; -use hbb_common::tokio::sync::Mutex; use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; @@ -27,7 +25,27 @@ impl RendezvousServer { ws: bool, forwarded_ip: Option<&str>, ) -> bool { + if crate::management::is_banned("", &try_into_v4(addr).ip().to_string()) + || forwarded_ip + .map(|ip| crate::management::is_banned("", ip)) + .unwrap_or(false) + { + return false; + } if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) { + let target_id = match msg_in.union.as_ref() { + Some(rendezvous_message::Union::RegisterPeer(v)) => v.id.as_str(), + Some(rendezvous_message::Union::RegisterPk(v)) => v.id.as_str(), + Some(rendezvous_message::Union::PunchHoleRequest(v)) => v.id.as_str(), + Some(rendezvous_message::Union::RequestRelay(v)) => v.id.as_str(), + Some(rendezvous_message::Union::PunchHoleSent(v)) => v.id.as_str(), + Some(rendezvous_message::Union::LocalAddr(v)) => v.id.as_str(), + Some(rendezvous_message::Union::RelayResponse(v)) => v.id(), + _ => "", + }; + if crate::management::is_banned(target_id, "") { + return false; + } match msg_in.union { Some(rendezvous_message::Union::PunchHoleRequest(ph)) => { // there maybe several attempt, so sink can be none @@ -43,6 +61,9 @@ impl RendezvousServer { self.tcp_punch.lock().await.insert(try_into_v4(addr), sink); } if let Some(peer) = self.pm.get_in_memory(&rf.id).await { + if crate::management::is_banned(&rf.id, &peer.read().await.info.ip) { + return false; + } let mut msg_out = RendezvousMessage::new(); rf.socket_addr = AddrMangle::encode(addr).into(); msg_out.set_request_relay(rf); @@ -141,7 +162,9 @@ impl RendezvousServer { // Always update for WS/TCP peers: socket_addr must reflect the // current connection address even when key/uuid unchanged if changed || ws { - self.pm.update_pk(id.clone(), peer, addr, rk.uuid, rk.pk, ip).await; + self.pm + .update_pk(id.clone(), peer, addr, rk.uuid, rk.pk, ip) + .await; } // Ensure socket_addr and last_reg_time are always up-to-date if let Some(p) = self.pm.get_in_memory(&id).await { @@ -149,20 +172,31 @@ impl RendezvousServer { p.socket_addr = addr; p.last_reg_time = Instant::now(); } - let msg_out = Self::make_register_pk_response(register_pk_response::Result::OK); - if let Some(s) = sink.as_mut() { Self::send_to_sink(s, msg_out).await; } + let msg_out = + Self::make_register_pk_response(register_pk_response::Result::OK); + if let Some(s) = sink.as_mut() { + Self::send_to_sink(s, msg_out).await; + } // Store sink in persistent map for later message delivery if let Some(s) = sink.take() { if ws { - self.ws_map.lock().await.insert(try_into_v4(addr), Arc::new(Mutex::new(s))); + self.ws_map + .lock() + .await + .insert(try_into_v4(addr), Arc::new(Mutex::new(s))); } else { - self.tcp_map.lock().await.insert(try_into_v4(addr), Arc::new(Mutex::new(s))); + self.tcp_map + .lock() + .await + .insert(try_into_v4(addr), Arc::new(Mutex::new(s))); } } return true; } Err(msg_out) => { - if let Some(s) = sink.as_mut() { Self::send_to_sink(s, msg_out).await; } + if let Some(s) = sink.as_mut() { + Self::send_to_sink(s, msg_out).await; + } return false; } } @@ -226,7 +260,8 @@ impl RendezvousServer { let mut states = BytesMut::zeroed((peers.len() + 7) / 8); for (i, peer_id) in peers.iter().enumerate() { if let Some(peer) = self.pm.get_in_memory(peer_id).await { - let elapsed = peer.read().await.last_reg_time.elapsed().as_millis() as i64; + let elapsed = + peer.read().await.last_reg_time.elapsed().as_millis() as i64; let states_idx = i / 8; let bit_idx = 7 - i % 8; if elapsed < REG_TIMEOUT { @@ -292,4 +327,4 @@ impl RendezvousServer { } Ok(()) } -} \ No newline at end of file +} diff --git a/src/rendezvous_server/udp_handler.rs b/src/rendezvous_server/udp_handler.rs index 82dbf12..4a8431e 100644 --- a/src/rendezvous_server/udp_handler.rs +++ b/src/rendezvous_server/udp_handler.rs @@ -1,15 +1,10 @@ use super::*; use crate::common::*; use crate::peer::*; +use hbb_common::bytes::BytesMut; use hbb_common::{ - log, - protobuf::Message as _, - rendezvous_proto::*, - try_into_v4, - udp::FramedSocket, - ResultType, + log, protobuf::Message as _, rendezvous_proto::*, try_into_v4, udp::FramedSocket, ResultType, }; -use hbb_common::bytes::BytesMut; use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; @@ -23,7 +18,23 @@ impl RendezvousServer { socket: &mut FramedSocket, key: &str, ) -> ResultType<()> { + if crate::management::is_banned("", &try_into_v4(addr).ip().to_string()) { + return Ok(()); + } if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) { + let target_id = match msg_in.union.as_ref() { + Some(rendezvous_message::Union::RegisterPeer(v)) => v.id.as_str(), + Some(rendezvous_message::Union::RegisterPk(v)) => v.id.as_str(), + Some(rendezvous_message::Union::PunchHoleRequest(v)) => v.id.as_str(), + Some(rendezvous_message::Union::RequestRelay(v)) => v.id.as_str(), + Some(rendezvous_message::Union::PunchHoleSent(v)) => v.id.as_str(), + Some(rendezvous_message::Union::LocalAddr(v)) => v.id.as_str(), + Some(rendezvous_message::Union::RelayResponse(v)) => v.id(), + _ => "", + }; + if crate::management::is_banned(target_id, "") { + return Ok(()); + } match msg_in.union { Some(rendezvous_message::Union::RegisterPeer(rp)) => { // B registered @@ -70,9 +81,7 @@ impl RendezvousServer { }); socket.send(&msg_out, addr).await? } - Err(msg_out) => { - socket.send(&msg_out, addr).await? - } + Err(msg_out) => socket.send(&msg_out, addr).await?, } } Some(rendezvous_message::Union::PunchHoleSent(phs)) => { @@ -158,4 +167,4 @@ impl RendezvousServer { }); socket.send(&msg_out, socket_addr).await } -} \ No newline at end of file +} From bea7c95b17a8dbc75240168f3f98d14634d4bc02 Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 2 Oct 2026 19:51:13 +0800 Subject: [PATCH 2/7] fix: align lockfile with pinned common dependencies --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 77ac957..904a0c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1653,6 +1653,7 @@ dependencies = [ "mac_address", "machine-uid 0.3.0", "osascript", + "percent-encoding", "protobuf", "protobuf-codegen", "rand 0.8.8", From 88e1adc4f21cefd49c7c3228699a255cf05b542a Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 2 Oct 2026 19:51:44 +0800 Subject: [PATCH 3/7] ci: resolve pinned submodule dependencies and export lockfile --- .github/workflows/test.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b722a6f..b080122 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -39,6 +39,15 @@ jobs: - name: Install native build dependencies run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev libsodium-dev libx11-dev cmake clang + - name: Refresh lockfile for the pinned common submodule + run: cargo update --workspace + + - name: Export resolved lockfile + uses: actions/upload-artifact@v4 + with: + name: resolved-cargo-lock + path: Cargo.lock + - name: Check compilation uses: actions-rs/cargo@v1 with: From fc2a912ccf3deaf0b3e956671d9a7c68bca47c6b Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 2 Oct 2026 19:54:34 +0800 Subject: [PATCH 4/7] fix: use CI-resolved lockfile for the pinned common submodule --- .github/workflows/test.yaml | 9 - Cargo.lock | 512 ++++++------------------------------ 2 files changed, 82 insertions(+), 439 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b080122..b722a6f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -39,15 +39,6 @@ jobs: - name: Install native build dependencies run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev libsodium-dev libx11-dev cmake clang - - name: Refresh lockfile for the pinned common submodule - run: cargo update --workspace - - - name: Export resolved lockfile - uses: actions/upload-artifact@v4 - with: - name: resolved-cargo-lock - path: Cargo.lock - - name: Check compilation uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index 904a0c5..132b947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -343,21 +334,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide 0.8.9", - "object", - "rustc-demangle", - "windows-link", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -493,31 +469,6 @@ dependencies = [ "serde", ] -[[package]] -name = "calloop" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" -dependencies = [ - "bitflags 2.13.1", - "polling", - "rustix 1.1.4", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop", - "rustix 1.1.4", - "wayland-backend", - "wayland-client", -] - [[package]] name = "cbc" version = "0.1.2" @@ -578,17 +529,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.2" @@ -600,19 +540,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20 0.9.1", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.45" @@ -634,7 +561,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", - "zeroize", ] [[package]] @@ -723,15 +649,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "config" version = "0.11.0" @@ -893,12 +810,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1117,48 +1028,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dtls" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f531dd7c181beaf3cebab3716afa4d0d41ab888be85232583f56bbaf07ca208a" -dependencies = [ - "aes", - "aes-gcm", - "async-trait", - "bincode", - "byteorder", - "cbc", - "ccm", - "chacha20poly1305", - "der-parser", - "hmac", - "log", - "p256", - "p384", - "portable-atomic", - "rand 0.9.5", - "rand_core 0.6.4", - "rcgen", - "ring 0.17.14", - "rustls 0.23.43", - "sec1", - "serde", - "sha1", - "sha2", - "thiserror 1.0.69", - "tokio", - "webrtc-util", - "x25519-dalek", - "x509-parser", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -1289,16 +1158,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.12" @@ -1312,7 +1171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide 0.9.1", + "miniz_oxide", "zlib-rs", ] @@ -1561,12 +1420,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glob" version = "0.3.4" @@ -1630,7 +1483,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion", - "backtrace", "base64 0.22.1", "bytes", "chrono", @@ -1641,7 +1493,6 @@ dependencies = [ "dirs-next", "dlopen", "env_logger 0.11.11", - "filetime", "flexi_logger 0.27.4", "futures", "futures-util", @@ -1652,7 +1503,6 @@ dependencies = [ "log", "mac_address", "machine-uid 0.3.0", - "osascript", "percent-encoding", "protobuf", "protobuf-codegen", @@ -1665,7 +1515,6 @@ dependencies = [ "serde_derive", "serde_json", "sha2", - "smithay-client-toolkit", "socket2 0.3.19", "sodiumoxide", "sysinfo", @@ -1684,7 +1533,6 @@ dependencies = [ "webpki-roots 1.0.9", "webrtc", "whoami", - "winapi", "x11", "zstd", ] @@ -2160,16 +2008,15 @@ dependencies = [ [[package]] name = "interceptor" -version = "0.15.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea51375727680dc15f06e8ad90fa31df75d79dd030100e8ad60eef1c27fe2c98" +checksum = "1ac0781c825d602095113772e389ef0607afcb869ae0e68a590d8e0799cdcef8" dependencies = [ "async-trait", "bytes", - "futures", "log", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "rtcp", "rtp", "thiserror 1.0.69", @@ -2510,15 +2357,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.7.1" @@ -2559,15 +2397,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "miniz_oxide" version = "0.9.1" @@ -2758,15 +2587,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -2857,17 +2677,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "osascript" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" -dependencies = [ - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "p256" version = "0.13.2" @@ -3051,31 +2860,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.3", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "polyval" version = "0.6.2" @@ -3212,15 +2996,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe" -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - [[package]] name = "quinn" version = "0.11.11" @@ -3334,7 +3109,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.2", + "chacha20", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3579,9 +3354,9 @@ dependencies = [ [[package]] name = "rtcp" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d30d1c4091644431c22acf9f8be6191b56805e0e977f15ca7104b4a6d6eaec" +checksum = "e9689528bf3a9eb311fd938d05516dd546412f9ce4fffc8acfc1db27cc3dbf72" dependencies = [ "bytes", "thiserror 1.0.69", @@ -3590,14 +3365,14 @@ dependencies = [ [[package]] name = "rtp" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f126f38ea84c02480e32e547c1459a939052f74fb92117ac3eef23fdac6b023" +checksum = "c54733451a67d76caf9caa07a7a2cec6871ea9dda92a7847f98063d459200f4b" dependencies = [ "bytes", "memchr", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "serde", "thiserror 1.0.69", "webrtc-util", @@ -3613,12 +3388,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -3819,11 +3588,11 @@ dependencies = [ [[package]] name = "sdp" -version = "0.10.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6" +checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69" dependencies = [ - "rand 0.9.5", + "rand 0.8.8", "substring", "thiserror 1.0.69", "url", @@ -4037,33 +3806,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.13.1", - "calloop", - "calloop-wayland-source", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.4", - "thiserror 2.0.20", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - [[package]] name = "smol_str" version = "0.2.2" @@ -4288,15 +4030,15 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "stun" -version = "0.9.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a512c5d501e3e3b5a4bb3e8e31462d56d54a66b95a28b8596e14422bf21c32b" +checksum = "7dbc2bab375524093c143dc362a03fb6a1fb79e938391cdb21665688f88a088a" dependencies = [ "base64 0.22.1", "crc", "lazy_static", "md-5", - "rand 0.9.5", + "rand 0.8.8", "ring 0.17.14", "subtle", "thiserror 1.0.69", @@ -4866,9 +4608,9 @@ dependencies = [ [[package]] name = "turn" -version = "0.11.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed995882f66ab94238de77c62e5e778389698ab700afa4696f4754da8f457cb" +checksum = "3f5aea1116456e1da71c45586b87c72e3b43164fbf435eb93ff6aa475416a9a4" dependencies = [ "async-trait", "base64 0.22.1", @@ -4876,7 +4618,7 @@ dependencies = [ "log", "md-5", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "ring 0.17.14", "stun", "thiserror 1.0.69", @@ -5143,124 +4885,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wayland-backend" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.4", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" -dependencies = [ - "bitflags 2.13.1", - "rustix 1.1.4", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.13.1", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" -dependencies = [ - "rustix 1.1.4", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" -dependencies = [ - "bitflags 2.13.1", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" -dependencies = [ - "proc-macro2 1.0.107", - "quick-xml", - "quote 1.0.47", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "pkg-config", -] - [[package]] name = "web-sys" version = "0.3.105" @@ -5329,25 +4953,26 @@ dependencies = [ [[package]] name = "webrtc" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08fd686c0920ac08f3a57eacc48e31f0e4ca1ffefba4478784606f78c14e83ad" +checksum = "24bab7195998d605c862772f90a452ba655b90a2f463c850ac032038890e367a" dependencies = [ "arc-swap", "async-trait", "bytes", - "dtls", + "cfg-if", "hex", "interceptor", "lazy_static", "log", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "rcgen", "regex", "ring 0.17.14", "rtcp", "rtp", + "rustls 0.23.43", "sdp", "serde", "serde_json", @@ -5355,12 +4980,13 @@ dependencies = [ "smol_str", "stun", "thiserror 1.0.69", + "time", "tokio", "turn", - "unicase", "url", "waitgroup", "webrtc-data", + "webrtc-dtls", "webrtc-ice", "webrtc-mdns", "webrtc-media", @@ -5371,9 +4997,9 @@ dependencies = [ [[package]] name = "webrtc-data" -version = "0.12.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062a5438d63bb0756a221693d76cc0dd6119affee1dfdfe57abe3a2a8c8b3eea" +checksum = "4e97b932854da633a767eff0cc805425a2222fc6481e96f463e57b015d949d1d" dependencies = [ "bytes", "log", @@ -5384,18 +5010,55 @@ dependencies = [ "webrtc-util", ] +[[package]] +name = "webrtc-dtls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ccbe4d9049390ab52695c3646c1395c877e16c15fb05d3bda8eee0c7351711c" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "der-parser", + "hkdf", + "hmac", + "log", + "p256", + "p384", + "portable-atomic", + "rand 0.8.8", + "rand_core 0.6.4", + "rcgen", + "ring 0.17.14", + "rustls 0.23.43", + "sec1", + "serde", + "sha1", + "sha2", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + [[package]] name = "webrtc-ice" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cb13fd1a373e68addc4bba0c8ca058627518e54342583d024bdcbb8ae5d97d" +checksum = "eb51bde0d790f109a15bfe4d04f1b56fb51d567da231643cb3f21bb74d678997" dependencies = [ "arc-swap", "async-trait", "crc", "log", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "serde", "serde_json", "stun", @@ -5411,9 +5074,9 @@ dependencies = [ [[package]] name = "webrtc-mdns" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17279a067e75df72ce923fdeb7f04cd808f6f5aa4910dc6bcb4fbe66b396ace" +checksum = "979cc85259c53b7b620803509d10d35e2546fa505d228850cbe3f08765ea6ea8" dependencies = [ "log", "socket2 0.5.10", @@ -5424,22 +5087,22 @@ dependencies = [ [[package]] name = "webrtc-media" -version = "0.11.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a84c910fec0848fd5a0d8a5651e0ddbdedaf25a7d3ae3f0b15f71ac73a1773" +checksum = "80041211deccda758a3e19aa93d6b10bc1d37c9183b519054b40a83691d13810" dependencies = [ "byteorder", "bytes", - "rand 0.9.5", + "rand 0.8.8", "rtp", "thiserror 1.0.69", ] [[package]] name = "webrtc-sctp" -version = "0.13.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f985465467d8910c1f8ac4382cd64f83b1f6a1a75021a82b221546f6fb3b856f" +checksum = "07439c134425d51d2f10907aaf2f815fdfb587dce19fe94a4ae8b5faf2aae5ae" dependencies = [ "arc-swap", "async-trait", @@ -5447,7 +5110,7 @@ dependencies = [ "crc", "log", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "thiserror 1.0.69", "tokio", "webrtc-util", @@ -5455,9 +5118,9 @@ dependencies = [ [[package]] name = "webrtc-srtp" -version = "0.16.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d8cdc33413f1d0192670a80ce93d17cb78d57fe3a2414be30d6f6dff121123" +checksum = "01e773f79b09b057ffbda6b03fe7b43403b012a240cf8d05d630674c3723b5bb" dependencies = [ "aead", "aes", @@ -5478,19 +5141,20 @@ dependencies = [ [[package]] name = "webrtc-util" -version = "0.12.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c0c7e0c8f280f2bbfae442701465777ac07adaf46ce0c5863cd58e13fe472a" +checksum = "64bfb10dbe6d762f80169ae07cf252bafa1f764b9594d140008a0231c0cdce58" dependencies = [ "async-trait", "bitflags 1.3.2", "bytes", "ipnet", "lazy_static", + "libc", "log", "nix 0.26.4", "portable-atomic", - "rand 0.9.5", + "rand 0.8.8", "thiserror 1.0.69", "tokio", "winapi", @@ -5931,18 +5595,6 @@ dependencies = [ "time", ] -[[package]] -name = "xcursor" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - [[package]] name = "yasna" version = "0.5.2" From f4b5b92d4b64757d14874c47d42cbc7cbdc9c860 Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 2 Oct 2026 20:52:32 +0800 Subject: [PATCH 5/7] fix: use Axum 0.5 management middleware signature --- src/management.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/management.rs b/src/management.rs index d930a0e..b242087 100644 --- a/src/management.rs +++ b/src/management.rs @@ -208,11 +208,12 @@ struct ApiState { values: BTreeMap, } -async fn authorize( - Extension(state): Extension, - request: Request, - next: Next, -) -> Response { +async fn authorize(request: Request, next: Next) -> Response { + // Axum 0.5 from_fn middleware takes only Request and Next. + let state = match request.extensions().get::() { + Some(state) => state, + None => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; let expected = format!("Bearer {}", state.token); let actual = request .headers() From 98b6554a6cc6c7da617c575d79ee2cd7f83a37c2 Mon Sep 17 00:00:00 2001 From: hashbk Date: Fri, 2 Oct 2026 23:10:26 +0800 Subject: [PATCH 6/7] fix: persist bans without blocking admission and restore policy on startup --- docs/management.md | 4 +- src/hbbr.rs | 1 + src/main.rs | 1 + src/management.rs | 172 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 157 insertions(+), 21 deletions(-) diff --git a/docs/management.md b/docs/management.md index 4741a2c..9d24d38 100644 --- a/docs/management.md +++ b/docs/management.md @@ -4,6 +4,8 @@ The opt-in `/v1` HTTP API is independent of RustDesk's client protocol ports. Se 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. -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": [] }`. 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. +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. + +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. `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. diff --git a/src/hbbr.rs b/src/hbbr.rs index ff4b99a..b40fafa 100644 --- a/src/hbbr.rs +++ b/src/hbbr.rs @@ -8,6 +8,7 @@ use relay_server::*; mod version; fn main() -> ResultType<()> { + management::restore_bans("hbbr")?; let managed = management::load_config("hbbr")?; managed.apply(); let _logger = Logger::try_with_env_or_str("info")? diff --git a/src/main.rs b/src/main.rs index b416f9f..e606a21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use hbbs::{common::*, *}; const RMEM: usize = 0; fn main() -> ResultType<()> { + management::restore_bans("hbbs")?; let managed = management::load_config("hbbs")?; managed.apply(); let _logger = Logger::try_with_env_or_str("info")? diff --git a/src/management.rs b/src/management.rs index b242087..ba7d466 100644 --- a/src/management.rs +++ b/src/management.rs @@ -19,12 +19,13 @@ use std::{ sync::RwLock, time::Instant, }; -use tokio::sync::oneshot; +use tokio::sync::{oneshot, Mutex}; pub const REDACTED: &str = "__REDACTED__"; static PEERS: Lazy>> = Lazy::new(|| RwLock::new(Vec::new())); static SESSIONS: Lazy>> = Lazy::new(|| RwLock::new(HashMap::new())); static BANS: Lazy> = Lazy::new(|| RwLock::new(Bans::default())); +static BAN_UPDATES: Lazy> = Lazy::new(|| Mutex::new(())); #[derive(Clone, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] @@ -294,17 +295,31 @@ async fn replace_bans( ) .into_response(); } - let mut current = BANS.write().unwrap(); - if *current == bans { - return Json(json!({"state":"applied"})).into_response(); - } - if let Err(err) = persist_bans(state.service, &bans) { - log::error!("Unable to persist management bans: {err}"); - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + // The task owns the update through persistence and activation even if the + // HTTP caller disconnects while the blocking filesystem work is in flight. + match tokio::spawn(async move { + let _update = BAN_UPDATES.lock().await; + if *BANS.read().unwrap() == bans { + return Ok(()); + } + let service = state.service; + let persisted = bans.clone(); + tokio::task::spawn_blocking(move || persist_bans(service, &persisted)).await??; + // Never hold the protocol admission lock during filesystem I/O. + // Keep the BANS -> SESSIONS order for activation and admission. + let mut current = BANS.write().unwrap(); + *current = bans; + cancel_banned_sessions(¤t); + Ok::<(), hbb_common::anyhow::Error>(()) + }) + .await + { + Ok(Ok(())) => Json(json!({"state":"applied"})).into_response(), + result => { + log::error!("Unable to persist management bans: {result:?}"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } } - *current = bans; - cancel_banned_sessions(¤t); - Json(json!({"state":"applied"})).into_response() } fn cancel_banned_sessions(bans: &Bans) { @@ -329,6 +344,20 @@ fn persist_bans(service: &str, bans: &Bans) -> ResultType<()> { Ok(()) } +// Persisted admission policy is independent of management HTTP exposure. +pub fn restore_bans(service: &str) -> ResultType<()> { + match std::fs::read(data_path(service, "bans")) { + Ok(bytes) => { + let mut bans: Bans = serde_json::from_slice(&bytes)?; + bans.validate()?; + *BANS.write().unwrap() = bans; + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + Ok(()) +} + pub fn start(service: &'static str, port: u16) -> ResultType<()> { let bind = std::env::var("RD_MANAGEMENT_BIND").unwrap_or_default(); if bind.is_empty() { @@ -340,15 +369,6 @@ pub fn start(service: &'static str, port: u16) -> ResultType<()> { "RD_MANAGEMENT_TOKEN must contain at least 32 characters" )); } - match std::fs::read(data_path(service, "bans")) { - Ok(bytes) => { - let mut bans: Bans = serde_json::from_slice(&bytes)?; - bans.validate()?; - *BANS.write().unwrap() = bans; - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), - } let mut values = BTreeMap::new(); for definition in schema(service) { let name = definition["name"].as_str().unwrap(); @@ -402,6 +422,117 @@ fn router(state: ApiState) -> Router { #[cfg(test)] mod tests { use super::*; + static POLICY_TEST: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + struct PolicyFixture { + directory: PathBuf, + previous_directory: Option, + previous_bind: Option, + previous_bans: Bans, + } + impl PolicyFixture { + fn new() -> Self { + let directory = std::env::temp_dir().join(format!( + "rustdesk-policy-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&directory).unwrap(); + let fixture = Self { + directory, + previous_directory: std::env::var("RD_MANAGEMENT_DIR").ok(), + previous_bind: std::env::var("RD_MANAGEMENT_BIND").ok(), + previous_bans: BANS.read().unwrap().clone(), + }; + std::env::set_var("RD_MANAGEMENT_DIR", &fixture.directory); + std::env::remove_var("RD_MANAGEMENT_BIND"); + *BANS.write().unwrap() = Bans::default(); + fixture + } + } + impl Drop for PolicyFixture { + fn drop(&mut self) { + *BANS.write().unwrap() = self.previous_bans.clone(); + for (name, previous) in [ + ("RD_MANAGEMENT_DIR", &self.previous_directory), + ("RD_MANAGEMENT_BIND", &self.previous_bind), + ] { + if let Some(value) = previous { + std::env::set_var(name, value); + } else { + std::env::remove_var(name); + } + } + let _ = std::fs::remove_dir_all(&self.directory); + } + } + + #[tokio::test] + async fn concurrent_policy_updates_persist_the_active_policy_and_write_failures_do_not_activate( + ) { + let _test = POLICY_TEST.lock().unwrap(); + let fixture = PolicyFixture::new(); + let state = ApiState { + service: "hbbs", + token: "test".into(), + started: Instant::now(), + values: BTreeMap::new(), + }; + let a = Bans { + device_ids: vec!["policy-a".into()], + ips: vec![], + }; + let b = Bans { + device_ids: vec!["policy-b".into()], + ips: vec![], + }; + let (a_result, b_result) = tokio::join!( + replace_bans(Extension(state.clone()), Json(a)), + replace_bans(Extension(state.clone()), Json(b)), + ); + assert_eq!(a_result.status(), StatusCode::OK); + assert_eq!(b_result.status(), StatusCode::OK); + let persisted: Bans = + serde_json::from_slice(&std::fs::read(data_path("hbbs", "bans")).unwrap()).unwrap(); + assert!(*BANS.read().unwrap() == persisted); + // Force the next rename to fail; the active policy must remain intact. + std::fs::remove_file(data_path("hbbs", "bans")).unwrap(); + std::fs::create_dir(data_path("hbbs", "bans")).unwrap(); + let result = replace_bans( + Extension(state), + Json(Bans { + device_ids: vec!["policy-c".into()], + ips: vec![], + }), + ) + .await; + assert_eq!(result.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert!(*BANS.read().unwrap() == persisted); + drop(fixture); + } + + #[test] + fn saved_policy_is_restored_when_management_listener_is_disabled() { + let _test = POLICY_TEST.lock().unwrap(); + let _fixture = PolicyFixture::new(); + persist_bans( + "hbbs", + &Bans { + device_ids: vec!["blocked-device".into()], + ips: vec!["192.0.2.25".into()], + }, + ) + .unwrap(); + restore_bans("hbbs").unwrap(); + start("hbbs", 21116).unwrap(); + assert!(is_banned("blocked-device", "")); + assert!(is_banned("", "192.0.2.25")); + std::fs::write(data_path("hbbs", "bans"), b"invalid policy").unwrap(); + assert!(restore_bans("hbbs").is_err()); + } #[tokio::test] async fn http_routes_require_authorization_and_do_not_leak_tokens() { use tokio::{ @@ -448,6 +579,7 @@ mod tests { } #[test] fn bans_cancel_existing_sessions_and_duplicates_are_rejected() { + let _test = POLICY_TEST.lock().unwrap(); let mut rx = open_session( "test-session", vec!["123456".into()], From a18a4ab905d2c65b9e5e89ba27f9d0fab0246525 Mon Sep 17 00:00:00 2001 From: hashbk Date: Sat, 3 Oct 2026 13:49:45 +0800 Subject: [PATCH 7/7] fix: canonicalize forwarded client addresses for ban enforcement --- .github/workflows/test.yaml | 1 - src/management.rs | 46 +++++++++++++++++++++++++++---- src/relay_server/connection.rs | 11 ++++---- src/rendezvous_server/listener.rs | 6 +++- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b722a6f..c69cf63 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,7 +1,6 @@ name: Test on: - workflow_dispatch: push: branches: [master] pull_request: diff --git a/src/management.rs b/src/management.rs index ba7d466..5822f7d 100644 --- a/src/management.rs +++ b/src/management.rs @@ -14,7 +14,7 @@ use serde_derive::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::{ collections::{BTreeMap, HashMap}, - net::IpAddr, + net::{IpAddr, SocketAddr}, path::PathBuf, sync::RwLock, time::Instant, @@ -59,15 +59,26 @@ impl Bans { } fn contains(&self, id: &str, ip: &str) -> bool { - let normalized = ip - .parse::() - .map(normalize_ip) - .unwrap_or_else(|_| ip.to_owned()); + let normalized = normalize_client_ip(ip); (!id.is_empty() && self.device_ids.iter().any(|v| v == id)) || self.ips.iter().any(|v| v == &normalized) } } +// Forwarded headers may contain a bare IP, bracketed IPv6 or an endpoint. +// Preserve malformed values for the existing proxy/header compatibility policy. +pub fn normalize_client_ip(value: &str) -> String { + let candidate = value + .strip_prefix('[') + .and_then(|v| v.strip_suffix(']')) + .unwrap_or(value); + candidate + .parse::() + .or_else(|_| value.parse::().map(|addr| addr.ip())) + .map(normalize_ip) + .unwrap_or_else(|_| value.to_owned()) +} + fn normalize_ip(ip: IpAddr) -> String { match ip { IpAddr::V6(ip) => ip @@ -578,6 +589,31 @@ mod tests { assert_eq!(bans.ips, vec!["192.0.2.1"]); } #[test] + fn endpoint_and_bracketed_forwarded_addresses_match_bare_ip_bans() { + let bans = Bans { + device_ids: vec![], + ips: vec!["203.0.113.5".into(), "2001:db8::5".into()], + }; + for ip in [ + "203.0.113.5", + "203.0.113.5:443", + "[::ffff:203.0.113.5]:443", + "2001:db8::5", + "[2001:db8::5]", + "[2001:db8::5]:443", + ] { + assert!( + bans.contains("", ip), + "forwarded address must match ban: {ip}" + ); + } + assert!(!bans.contains("", "203.0.113.6:443")); + assert_eq!( + normalize_client_ip("unparseable:header"), + "unparseable:header" + ); + } + #[test] fn bans_cancel_existing_sessions_and_duplicates_are_rejected() { let _test = POLICY_TEST.lock().unwrap(); let mut rx = open_session( diff --git a/src/relay_server/connection.rs b/src/relay_server/connection.rs index a362544..00f71a3 100644 --- a/src/relay_server/connection.rs +++ b/src/relay_server/connection.rs @@ -131,11 +131,12 @@ async fn make_pair( .get("X-Real-IP") .or_else(|| headers.get("X-Forwarded-For")) .and_then(|header_value| header_value.to_str().ok()); - if let Some(ip) = real_ip { - if ip.contains('.') { - addr = format!("{ip}:0").parse().unwrap_or(addr); - } else { - addr = format!("[{ip}]:0").parse().unwrap_or(addr); + if let Some(value) = real_ip { + let ip = crate::management::normalize_client_ip( + value.split(',').next().unwrap_or(value).trim(), + ); + if let Ok(ip) = ip.parse() { + addr = std::net::SocketAddr::new(ip, 0); } } Ok(response) diff --git a/src/rendezvous_server/listener.rs b/src/rendezvous_server/listener.rs index 987b5f3..af1b491 100644 --- a/src/rendezvous_server/listener.rs +++ b/src/rendezvous_server/listener.rs @@ -210,7 +210,11 @@ impl RendezvousServer { .and_then(|header_value| header_value.to_str().ok()) // X-Forwarded-For can be a comma-separated chain; the // original client is always the first entry. - .map(|s| s.split(',').next().unwrap_or(s).trim().to_string()) + .map(|s| { + crate::management::normalize_client_ip( + s.split(',').next().unwrap_or(s).trim(), + ) + }) .filter(|s| !s.is_empty()); if let Some(ip) = real_ip { *forwarded_ip_cb.lock().unwrap() = Some(ip);