diff --git a/crates/qbzd/src/cli/copy.rs b/crates/qbzd/src/cli/copy.rs index eed565f58..a490055b6 100644 --- a/crates/qbzd/src/cli/copy.rs +++ b/crates/qbzd/src/cli/copy.rs @@ -136,11 +136,18 @@ pub fn foreign_qbzd(addr: &str) -> String { /// `warning:` — an open LAN renderer (Sonos/Chromecast posture) is the /// intended default, the Origin shield already guards browsers, and this line /// just orients the operator toward the two ways to restrict it further. -/// `addr` is the bound `ip:port`. -pub fn lan_posture_note(addr: &str) -> String { - format!( - "control plane listening on {addr} — anyone on your network can control playback (set [server] bind = \"127.0.0.1\" or [server] token in qbzd.toml to restrict)" - ) +/// With `[server] token` set the API is already gated, so the line says that +/// instead of nagging. `addr` is the bound `ip:port`. +pub fn lan_posture_note(addr: &str, token_set: bool) -> String { + if token_set { + format!( + "control plane listening on {addr} — LAN clients must send the [server] token (Authorization: Bearer)" + ) + } else { + format!( + "control plane listening on {addr} — anyone on your network can control playback (set [server] bind = \"127.0.0.1\" or [server] token in qbzd.toml to restrict)" + ) + } } /// Version skew — daemon and CLI run different bin semvers (02 §1.6). A warning: @@ -234,11 +241,12 @@ mod tests { use super::*; #[test] - fn lan_posture_note_renders_the_verbatim_copy() { + fn lan_posture_note_renders_the_verbatim_copy_when_the_api_is_open() { // FB6 (successor to the old LAN-exposure warning) — one INFO line logged - // by the daemon at boot when the control API is NOT loopback-only. This - // test pins the exact wording so it cannot drift from the spec. - let rendered = lan_posture_note("0.0.0.0:6789"); + // by the daemon at boot when the control API is NOT loopback-only and + // no token gates it. This test pins the exact wording so it cannot + // drift from the spec. + let rendered = lan_posture_note("0.0.0.0:6789", false); assert!( rendered.contains("control plane listening on 0.0.0.0:6789"), "{rendered}" @@ -253,4 +261,18 @@ mod tests { "{rendered}" ); } + + #[test] + fn lan_posture_note_with_a_token_does_not_nag() { + let rendered = lan_posture_note("0.0.0.0:6789", true); + assert!( + rendered.contains("control plane listening on 0.0.0.0:6789"), + "{rendered}" + ); + assert!(rendered.contains("[server] token"), "{rendered}"); + assert!( + !rendered.contains("anyone on your network"), + "a gated API must not be described as open: {rendered}" + ); + } } diff --git a/crates/qbzd/src/daemon.rs b/crates/qbzd/src/daemon.rs index 7cd7a1eac..2a6d6091a 100644 --- a/crates/qbzd/src/daemon.rs +++ b/crates/qbzd/src/daemon.rs @@ -89,7 +89,15 @@ pub async fn run(roots: ProfileRoots, cfg: QbzdConfig, warns: Vec, orbit // FB6: the default bind is now 0.0.0.0 — LAN-first posture (Sonos/ // Chromecast parity), not a misconfiguration. One INFO line, not a // stderr warning; loopback binds stay silent. - log::info!("{}", crate::cli::copy::lan_posture_note(&bind_addr.to_string())); + let token_set = cfg + .server + .token + .as_deref() + .is_some_and(|t| !t.trim().is_empty()); + log::info!( + "{}", + crate::cli::copy::lan_posture_note(&bind_addr.to_string(), token_set) + ); } // 6.-9. compose stores + runtime + restore credentials + restore session. diff --git a/packaging/linux/qbzd-standalone-README.md b/packaging/linux/qbzd-standalone-README.md index 0988e6805..6bf76609d 100644 --- a/packaging/linux/qbzd-standalone-README.md +++ b/packaging/linux/qbzd-standalone-README.md @@ -82,6 +82,27 @@ budgets exclude active streaming buffers, decoders and offline downloads. The policy applies live without reopening the output device. +## Network access + +By default `qbzd run` listens on `0.0.0.0:8182`: any device on your network +can control playback, the queue and your playlists, the same way a Sonos or +Chromecast renderer is open on the LAN. Browsers are blocked by an Origin +check; other programs are not. The daemon logs one line about this at boot. + +To restrict it, edit `~/.config/qbzd/qbzd.toml`: + +```toml +[server] +bind = "127.0.0.1" # local clients only +# or keep the LAN bind and require a token on every request: +token = "choose-a-long-random-string" +``` + +With a token set, clients send `Authorization: Bearer `; the `qbzd` +CLI on the same machine picks it up from `qbzd.toml` automatically (or from +`QBZD_TOKEN`). `qbzd setup` has the same field on its Network screen +("Access token"). + ## OpenRC and runit Do not reuse the systemd unit on a non-systemd host. `qbzd` generates a