feat(qbzd): LAN-only access restriction, no token auth
Replaces token-based authentication with LAN-only IP restriction. Requests from non-private IPs (not 10.x, 172.16.x, 192.168.x, 127.x, link-local, ULA) are rejected with 403. Security model: LAN = trusted, WAN = blocked. Remote control outside LAN is exclusively via QConnect (cloud). cc
This commit is contained in:
3 files changed
+57
-4
No files matched your search
@@ -0,0 +1,50 @@
|
||||
//! LAN-only access restriction.
|
||||
//!
|
||||
//! Rejects HTTP requests from non-private IP addresses.
|
||||
//! If you're on the same network, you're in. If you're not, you're out.
|
||||
//! Remote control from outside the LAN is exclusively via QConnect (cloud).
|
||||
|
||||
use axum::{
|
||||
extract::Request,
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// Middleware: reject requests from non-private IPs.
|
||||
/// ConnectInfo is injected by axum::serve with into_make_service_with_connect_info.
|
||||
pub async fn lan_only(
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
// Extract client IP from ConnectInfo extension (set by axum::serve)
|
||||
if let Some(addr) = req.extensions().get::<axum::extract::ConnectInfo<std::net::SocketAddr>>() {
|
||||
if !is_private_ip(&addr.0.ip()) {
|
||||
log::warn!(
|
||||
"[qbzd] Rejected non-LAN request from {} to {}",
|
||||
addr.0.ip(),
|
||||
req.uri().path()
|
||||
);
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
}
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
fn is_private_ip(ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => {
|
||||
v4.is_loopback()
|
||||
|| v4.is_private()
|
||||
|| v4.is_link_local()
|
||||
|| v4.is_unspecified()
|
||||
}
|
||||
IpAddr::V6(v6) => {
|
||||
v6.is_loopback()
|
||||
|| v6.is_unspecified()
|
||||
|| (v6.segments()[0] & 0xffc0) == 0xfe80 // link-local
|
||||
|| (v6.segments()[0] & 0xff00) == 0xfd00 // unique local (LAN)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod events;
|
||||
pub mod favorites;
|
||||
pub mod integrations;
|
||||
pub mod library;
|
||||
pub mod middleware;
|
||||
pub mod playback;
|
||||
pub mod playlists;
|
||||
pub mod queue;
|
||||
|
||||
@@ -26,9 +26,7 @@ pub struct DaemonCore {
|
||||
|
||||
/// Run the daemon main loop.
|
||||
pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
|
||||
// Resolve token
|
||||
config.resolve_token();
|
||||
log::info!("[qbzd] API token: {}...", &config.server.token[..8.min(config.server.token.len())]);
|
||||
log::info!("[qbzd] Access: LAN-only (no authentication required on local network)");
|
||||
|
||||
// Create event bus (bounded, slow SSE clients get dropped)
|
||||
let (event_tx, _) = broadcast::channel::<DaemonEvent>(256);
|
||||
@@ -153,6 +151,7 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
|
||||
ctrl_c.await.ok();
|
||||
};
|
||||
|
||||
let app = app.into_make_service_with_connect_info::<std::net::SocketAddr>();
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown)
|
||||
.await
|
||||
@@ -338,7 +337,8 @@ macro_rules! with_daemon {
|
||||
/// Build the Axum HTTP router.
|
||||
fn build_router(daemon: Arc<DaemonCore>) -> axum::Router {
|
||||
use axum::routing::{get, post, patch, put, delete};
|
||||
use crate::api::{audio, catalog, catalog_ext, discover, favorites, integrations, library, playback, playlists, queue, search, system};
|
||||
use axum::middleware as axum_mw;
|
||||
use crate::api::{audio, catalog, catalog_ext, discover, favorites, integrations, library, middleware, playback, playlists, queue, search, system};
|
||||
|
||||
axum::Router::new()
|
||||
// System
|
||||
@@ -422,6 +422,8 @@ fn build_router(daemon: Arc<DaemonCore>) -> axum::Router {
|
||||
// System / Resources
|
||||
.route("/api/system/resources", get(with_daemon!(daemon, system::get_resources)))
|
||||
.route("/api/cache", delete(with_daemon!(daemon, system::clear_cache)))
|
||||
// LAN-only: reject requests from non-private IPs
|
||||
.layer(axum_mw::from_fn(middleware::lan_only))
|
||||
}
|
||||
|
||||
async fn ping_handler() -> &'static str {
|
||||
|
||||
Reference in new issue
Block a user