From 91ce00927188df7d77947014cbb6a2b7694ba5c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Feb 2026 13:52:05 +0000 Subject: [PATCH] Add --trust-proxy flag for extracting client IPs behind reverse proxies When running behind a reverse proxy (nginx, caddy, traefik, etc.), the server sees the proxy's IP instead of the real client IP. The new --trust-proxy flag makes the server read X-Forwarded-For and X-Real-IP headers to extract the actual client IP. Falls back to the socket address if no proxy header is present. https://claude.ai/code/session_01Mp2BozcMQX2kWm4ZP9mxJw --- lanshare/src/main.rs | 57 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/lanshare/src/main.rs b/lanshare/src/main.rs index bb53c99..b2b2afa 100644 --- a/lanshare/src/main.rs +++ b/lanshare/src/main.rs @@ -70,6 +70,15 @@ struct Args { /// connect via ws:// instead. #[arg(long, default_value_t = 8081, value_name = "PORT")] http_port: u16, + + /// Trust proxy headers (X-Forwarded-For, X-Real-IP) for client IPs + /// + /// Enable this when running behind a reverse proxy (e.g. nginx, caddy, + /// traefik). The server will use the IP from proxy headers instead of + /// the direct connection address. Do NOT enable this without a trusted + /// proxy, as clients can spoof these headers. + #[arg(long)] + trust_proxy: bool, } /// Messages sent between peers via the signaling server @@ -155,12 +164,14 @@ struct PeerState { /// Application state shared across all connections struct AppState { peers: DashMap, + trust_proxy: bool, } impl AppState { - fn new() -> Self { + fn new(trust_proxy: bool) -> Self { Self { peers: DashMap::new(), + trust_proxy, } } @@ -202,7 +213,7 @@ async fn main() { ) .init(); - let state = Arc::new(AppState::new()); + let state = Arc::new(AppState::new(args.trust_proxy)); let app = Router::new() .route("/", get(serve_index)) @@ -244,6 +255,9 @@ async fn main() { } } } + if args.trust_proxy { + println!(" \x1b[1;36mProxy mode:\x1b[0m trusting X-Forwarded-For / X-Real-IP headers"); + } println!(); println!(" \x1b[90mOpen this URL in browsers on your local network.\x1b[0m"); println!(" \x1b[90mPress Ctrl+C to stop the server.\x1b[0m"); @@ -290,14 +304,47 @@ async fn serve_index() -> impl IntoResponse { async fn ws_handler( ws: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, + headers: axum::http::HeaderMap, State(state): State>, ) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state, addr)) + let peer_ip = if state.trust_proxy { + extract_proxy_ip(&headers).unwrap_or_else(|| addr.ip().to_string()) + } else { + addr.ip().to_string() + }; + ws.on_upgrade(move |socket| handle_socket(socket, state, peer_ip)) } -async fn handle_socket(socket: WebSocket, state: Arc, addr: SocketAddr) { +/// Extract the client IP from proxy headers. +/// Checks X-Forwarded-For first (uses the leftmost/first IP), then X-Real-IP. +fn extract_proxy_ip(headers: &axum::http::HeaderMap) -> Option { + // X-Forwarded-For: client, proxy1, proxy2 + if let Some(forwarded_for) = headers.get("x-forwarded-for") { + if let Ok(value) = forwarded_for.to_str() { + if let Some(first_ip) = value.split(',').next() { + let trimmed = first_ip.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + } + + // X-Real-IP: client + if let Some(real_ip) = headers.get("x-real-ip") { + if let Ok(value) = real_ip.to_str() { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + + None +} + +async fn handle_socket(socket: WebSocket, state: Arc, peer_ip: String) { let peer_id = Uuid::new_v4().to_string(); - let peer_ip = addr.ip().to_string(); let (tx, _) = broadcast::channel::(64); // Add peer to state with default name