diff --git a/.cursor/bug-scan-progress.md b/.cursor/bug-scan-progress.md index 0898963..e306b08 100644 --- a/.cursor/bug-scan-progress.md +++ b/.cursor/bug-scan-progress.md @@ -1,16 +1,25 @@ # Bug scan progress -Last scanned: logger (2026-07-02) +Last scanned: config (2026-08-05) ## Modules - [x] config — .env loader, env overrides -- [x] db — SQLite persistence, stream/publisher/player CRUD -- [x] http — REST API, auth, stats endpoints -- [x] server — App lifecycle, HTTP+RTMP wiring, deleted_streams eviction -- [x] rtmp_bridge — RTMP protocol ↔ DB integration seam -- [x] keygen — Stream key generation -- [x] logger — Logging +- [ ] db — SQLite persistence, stream/publisher/player CRUD +- [ ] http — REST API, auth, stats endpoints +- [ ] server — App lifecycle, HTTP+RTMP wiring, deleted_streams eviction +- [ ] rtmp_bridge — RTMP protocol ↔ DB integration seam +- [ ] keygen — Stream key generation +- [ ] logger — Logging + +## Findings (2026-08-05 config pass) + +- **Critical (fixed):** CLI `-p`/`-w` port overrides rewrote bind addresses as + `0.0.0.0:{port}`, discarding a configured localhost-only host + (`RTMP_BIND=127.0.0.1:1935` or `HTTP_BIND=127.0.0.1:8080`). An operator + changing only the port via `-p`/`-w` would unintentionally expose RTMP/HTTP on + all interfaces. Fixed with `set_bind_port()` that preserves the configured host + (including bracketed IPv6) while replacing the port. ## Findings (2026-07-02 logger pass) diff --git a/Cargo.lock b/Cargo.lock index 364dc97..c7563d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -888,9 +888,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_terminal_polyfill" @@ -1404,9 +1404,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", diff --git a/src/config.rs b/src/config.rs index 8e3fbfa..62f137b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -154,6 +154,30 @@ impl ServerConfig { /// /// The only case with an explicit port is `"[v6addr]:port"` or exactly one /// unbracketed `:` (`"host:port"`). +/// Host portion of a "host:port" bind string, with IPv6 bracketing when needed. +/// Mirrors the host parsing rules used by `port_of` and `server::bind_with_default_port`. +fn bind_host_of(bind: &str) -> String { + let bind = bind.trim(); + if let Some(bracket_end) = bind.rfind(']') { + return bind[..=bracket_end].to_string(); + } + let colon_count = bind.chars().filter(|&c| c == ':').count(); + match colon_count { + 0 => bind.to_string(), + 1 => match bind.rsplit_once(':') { + Some((host, port)) if port.parse::().is_ok() => host.to_string(), + Some((host, _)) => host.to_string(), + None => bind.to_string(), + }, + _ => format!("[{bind}]"), + } +} + +/// Replace the port in a bind string while preserving the configured host. +pub fn set_bind_port(bind: &str, new_port: u16) -> String { + format!("{}:{new_port}", bind_host_of(bind)) +} + fn port_of(bind: &str, default: u16) -> u16 { if let Some(bracket_end) = bind.rfind(']') { return bind[bracket_end + 1..] @@ -783,6 +807,20 @@ mod tests { assert_eq!(config.http_max_body_bytes, 2048); } + #[test] + fn set_bind_port_preserves_localhost_host() { + assert_eq!(set_bind_port("127.0.0.1:1935", 1936), "127.0.0.1:1936"); + assert_eq!(set_bind_port("127.0.0.1:8080", 8081), "127.0.0.1:8081"); + } + + #[test] + fn set_bind_port_preserves_wildcard_and_ipv6_hosts() { + assert_eq!(set_bind_port("0.0.0.0:1935", 1936), "0.0.0.0:1936"); + assert_eq!(set_bind_port("[::1]:1935", 1936), "[::1]:1936"); + assert_eq!(set_bind_port("::1", 1936), "[::1]:1936"); + assert_eq!(set_bind_port("127.0.0.1", 1935), "127.0.0.1:1935"); + } + #[test] fn parse_max_body_bytes_clamps_and_invalid_falls_back() { assert_eq!(parse_max_body_bytes("500"), 1024); diff --git a/src/main.rs b/src/main.rs index b2cf422..d855e0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use librtmp2_server::config::{ServerConfig, config_apply_env, config_load}; +use librtmp2_server::config::{ServerConfig, config_apply_env, config_load, set_bind_port}; use librtmp2_server::logger; use librtmp2_server::server::ServerApp; @@ -44,10 +44,10 @@ fn run() -> Result<(), String> { config_apply_env(&mut config); if let Some(port) = cli.rtmp_port { - config.rtmp_bind = format!("0.0.0.0:{port}"); + config.rtmp_bind = set_bind_port(&config.rtmp_bind, port); } if let Some(port) = cli.http_port { - config.http_bind = format!("0.0.0.0:{port}"); + config.http_bind = set_bind_port(&config.http_bind, port); } if cli.verbose {