From c836903db5b3a4507ef1d6582ba96881cc9614d9 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Mon, 23 Mar 2026 06:57:57 +0200 Subject: [PATCH] simplify: unify route structs, fix prefix collision, lint fixes - Unify RouteConfig/RouteEntry/RouteResponse into single RouteEntry - Fix prefix collision: /api no longer matches /apiary (segment boundary check) - Add path traversal rejection in route API - Extract MdnsAnnouncement struct (clippy type_complexity) - cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/mdns_coexist.rs | 33 ++++++++------ src/api.rs | 5 ++- src/lan.rs | 96 +++++++++++++++++++++++++++++++--------- src/main.rs | 6 ++- src/service_store.rs | 7 ++- 5 files changed, 107 insertions(+), 40 deletions(-) diff --git a/examples/mdns_coexist.rs b/examples/mdns_coexist.rs index 6386cae..2772d72 100644 --- a/examples/mdns_coexist.rs +++ b/examples/mdns_coexist.rs @@ -1,3 +1,4 @@ +use socket2::{Domain, Protocol, Socket, Type}; /// Spike: can we bind to mDNS multicast (224.0.0.251:5353) alongside macOS mDNSResponder? /// /// Tests: @@ -8,10 +9,8 @@ /// 5. Send a _numa._tcp.local announcement — does it conflict? /// /// Run: cargo run --example mdns_coexist - use std::mem::MaybeUninit; use std::net::{Ipv4Addr, SocketAddrV4}; -use socket2::{Domain, Protocol, Socket, Type}; const MDNS_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); const MDNS_PORT: u16 = 5353; @@ -66,7 +65,8 @@ fn main() -> std::io::Result<()> { loop { match socket.recv_from(&mut buf) { Ok((n, addr)) => { - let data: &[u8] = unsafe { &*(&buf[..n] as *const [MaybeUninit] as *const [u8]) }; + let data: &[u8] = + unsafe { &*(&buf[..n] as *const [MaybeUninit] as *const [u8]) }; count += 1; let flags = u16::from_be_bytes([data[2], data[3]]); let is_response = flags & 0x8000 != 0; @@ -98,7 +98,8 @@ fn main() -> std::io::Result<()> { } // Step 6: Send a _numa._tcp.local announcement - let announcement = build_mdns_announcement("_numa._tcp.local", "test-numa._numa._tcp.local", 5380); + let announcement = + build_mdns_announcement("_numa._tcp.local", "test-numa._numa._tcp.local", 5380); match socket.send_to(&announcement, &dest.into()) { Ok(n) => println!("\n[OK] Sent _numa._tcp.local announcement ({} bytes)", n), Err(e) => println!("\n[FAIL] Cannot send announcement: {}", e), @@ -111,7 +112,8 @@ fn main() -> std::io::Result<()> { loop { match socket.recv_from(&mut buf2) { Ok((n, addr)) => { - let data: &[u8] = unsafe { &*(&buf2[..n] as *const [MaybeUninit] as *const [u8]) }; + let data: &[u8] = + unsafe { &*(&buf2[..n] as *const [MaybeUninit] as *const [u8]) }; let flags = u16::from_be_bytes([data[2], data[3]]); let is_response = flags & 0x8000 != 0; if is_response { @@ -133,7 +135,10 @@ fn main() -> std::io::Result<()> { // Verdict println!("\n=== Verdict ==="); if count > 0 { - println!("[PASS] mDNS coexistence works — received {} packets alongside mDNSResponder", count); + println!( + "[PASS] mDNS coexistence works — received {} packets alongside mDNSResponder", + count + ); println!(" Safe to proceed with mDNS-based LAN discovery"); } else { println!("[WARN] No mDNS packets received — may need further investigation"); @@ -163,7 +168,7 @@ fn build_mdns_query(name: &str) -> Vec { pkt.push(0); // root label pkt.extend_from_slice(&[0, 12]); // QTYPE = PTR (12) - pkt.extend_from_slice(&[0, 1]); // QCLASS = IN (1) + pkt.extend_from_slice(&[0, 1]); // QCLASS = IN (1) pkt } @@ -173,17 +178,17 @@ fn build_mdns_announcement(service_type: &str, instance_name: &str, port: u16) - let mut pkt = Vec::new(); // Header: ID=0, flags=0x8400 (response, authoritative), ANCOUNT=1 - pkt.extend_from_slice(&[0, 0]); // ID + pkt.extend_from_slice(&[0, 0]); // ID pkt.extend_from_slice(&[0x84, 0x00]); // Flags: QR=1, AA=1 - pkt.extend_from_slice(&[0, 0]); // QDCOUNT - pkt.extend_from_slice(&[0, 1]); // ANCOUNT = 1 (just PTR for now) - pkt.extend_from_slice(&[0, 0]); // NSCOUNT - pkt.extend_from_slice(&[0, 0]); // ARCOUNT + pkt.extend_from_slice(&[0, 0]); // QDCOUNT + pkt.extend_from_slice(&[0, 1]); // ANCOUNT = 1 (just PTR for now) + pkt.extend_from_slice(&[0, 0]); // NSCOUNT + pkt.extend_from_slice(&[0, 0]); // ARCOUNT // PTR record: _numa._tcp.local → test-numa._numa._tcp.local encode_name(&mut pkt, service_type); - pkt.extend_from_slice(&[0, 12]); // TYPE = PTR - pkt.extend_from_slice(&[0, 1]); // CLASS = IN + pkt.extend_from_slice(&[0, 12]); // TYPE = PTR + pkt.extend_from_slice(&[0, 1]); // CLASS = IN pkt.extend_from_slice(&[0, 0, 0, 120]); // TTL = 120s // RDATA: the instance name diff --git a/src/api.rs b/src/api.rs index 5167d3e..069c156 100644 --- a/src/api.rs +++ b/src/api.rs @@ -761,7 +761,10 @@ async fn add_route( if store.add_route(&name, req.path, req.port, req.strip) { Ok(StatusCode::CREATED) } else { - Err((StatusCode::NOT_FOUND, format!("service '{}' not found", name))) + Err(( + StatusCode::NOT_FOUND, + format!("service '{}' not found", name), + )) } } diff --git a/src/lan.rs b/src/lan.rs index ea2e6b7..d82b977 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -96,10 +96,15 @@ fn get_hostname() -> String { /// Generate a per-process instance ID for self-filtering on multi-instance hosts fn instance_id() -> String { - format!("{}:{}", std::process::id(), std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() % 1_000_000) + format!( + "{}:{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + % 1_000_000 + ) } pub async fn start_lan_discovery(ctx: Arc, config: &LanConfig) { @@ -116,7 +121,10 @@ pub async fn start_lan_discovery(ctx: Arc, config: &LanConfig) { let std_socket = match create_mdns_socket() { Ok(s) => s, Err(e) => { - warn!("LAN: could not bind mDNS socket: {} — LAN discovery disabled", e); + warn!( + "LAN: could not bind mDNS socket: {} — LAN discovery disabled", + e + ); return; } }; @@ -141,13 +149,19 @@ pub async fn start_lan_discovery(ctx: Arc, config: &LanConfig) { ticker.tick().await; let services: Vec<(String, u16)> = { let store = sender_ctx.services.lock().unwrap(); - store.list().iter().map(|e| (e.name.clone(), e.target_port)).collect() + store + .list() + .iter() + .map(|e| (e.name.clone(), e.target_port)) + .collect() }; if services.is_empty() { continue; } let current_ip = *sender_ctx.lan_ip.lock().unwrap(); - if let Ok(pkt) = build_announcement(&sender_hostname, current_ip, &services, &sender_instance_id) { + if let Ok(pkt) = + build_announcement(&sender_hostname, current_ip, &services, &sender_instance_id) + { let _ = sender_socket.send_to(pkt.filled(), dest).await; } } @@ -170,14 +184,21 @@ pub async fn start_lan_discovery(ctx: Arc, config: &LanConfig) { }; let data = &buf[..len]; - if let Some((services, peer_ip, peer_id)) = parse_mdns_response(data) { + if let Some(ann) = parse_mdns_response(data) { // Skip our own announcements via instance ID (works on multi-instance same-host) - if peer_id.as_deref() == Some(our_instance_id.as_str()) { + if ann.instance_id.as_deref() == Some(our_instance_id.as_str()) { continue; } - if !services.is_empty() { - ctx.lan_peers.lock().unwrap().update(peer_ip, &services); - debug!("LAN: {} services from {} (mDNS)", services.len(), peer_ip); + if !ann.services.is_empty() { + ctx.lan_peers + .lock() + .unwrap() + .update(ann.peer_ip, &ann.services); + debug!( + "LAN: {} services from {} (mDNS)", + ann.services.len(), + ann.peer_ip + ); } } } @@ -223,18 +244,30 @@ fn build_announcement( // SRV: ._numa._tcp.local → .local // Port in SRV is informational; actual service ports are in TXT - write_record_header(&mut buf, &instance_name, QueryType::SRV.to_num(), 0x8001, MDNS_TTL)?; + write_record_header( + &mut buf, + &instance_name, + QueryType::SRV.to_num(), + 0x8001, + MDNS_TTL, + )?; let rdlen_pos = buf.pos(); buf.write_u16(0)?; let rdata_start = buf.pos(); - buf.write_u16(0)?; // priority - buf.write_u16(0)?; // weight - buf.write_u16(0)?; // port (services have individual ports in TXT) + buf.write_u16(0)?; // priority + buf.write_u16(0)?; // weight + buf.write_u16(0)?; // port (services have individual ports in TXT) buf.write_qname(&host_local)?; patch_rdlen(&mut buf, rdlen_pos, rdata_start)?; // TXT: services + instance ID for self-filtering - write_record_header(&mut buf, &instance_name, QueryType::TXT.to_num(), 0x8001, MDNS_TTL)?; + write_record_header( + &mut buf, + &instance_name, + QueryType::TXT.to_num(), + 0x8001, + MDNS_TTL, + )?; let rdlen_pos = buf.pos(); buf.write_u16(0)?; let rdata_start = buf.pos(); @@ -248,7 +281,13 @@ fn build_announcement( patch_rdlen(&mut buf, rdlen_pos, rdata_start)?; // A: .local → IP - write_record_header(&mut buf, &host_local, QueryType::A.to_num(), 0x8001, MDNS_TTL)?; + write_record_header( + &mut buf, + &host_local, + QueryType::A.to_num(), + 0x8001, + MDNS_TTL, + )?; buf.write_u16(4)?; for &b in &ip.octets() { buf.write_u8(b)?; @@ -271,7 +310,11 @@ fn write_record_header( Ok(()) } -fn patch_rdlen(buf: &mut BytePacketBuffer, rdlen_pos: usize, rdata_start: usize) -> crate::Result<()> { +fn patch_rdlen( + buf: &mut BytePacketBuffer, + rdlen_pos: usize, + rdata_start: usize, +) -> crate::Result<()> { let rdlen = (buf.pos() - rdata_start) as u16; buf.set_u16(rdlen_pos, rdlen) } @@ -289,8 +332,13 @@ fn write_txt_string(buf: &mut BytePacketBuffer, s: &str) -> crate::Result<()> { // --- mDNS Packet Parsing --- -/// Returns (services, peer_ip, instance_id) if this is a Numa mDNS announcement -fn parse_mdns_response(data: &[u8]) -> Option<(Vec<(String, u16)>, IpAddr, Option)> { +struct MdnsAnnouncement { + services: Vec<(String, u16)>, + peer_ip: IpAddr, + instance_id: Option, +} + +fn parse_mdns_response(data: &[u8]) -> Option { if data.len() < 12 { return None; } @@ -381,7 +429,11 @@ fn parse_mdns_response(data: &[u8]) -> Option<(Vec<(String, u16)>, IpAddr, Optio // Trust the A record IP if present, otherwise this isn't a complete announcement let peer_ip = a_ip?; - Some((services, peer_ip, peer_instance_id)) + Some(MdnsAnnouncement { + services, + peer_ip, + instance_id: peer_instance_id, + }) } fn create_mdns_socket() -> std::io::Result { diff --git a/src/main.rs b/src/main.rs index b070b70..ed661a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -217,7 +217,11 @@ async fn main() -> numa::Result<()> { let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled { std::net::Ipv4Addr::UNSPECIFIED } else { - config.proxy.bind_addr.parse().unwrap_or(std::net::Ipv4Addr::LOCALHOST) + config + .proxy + .bind_addr + .parse() + .unwrap_or(std::net::Ipv4Addr::LOCALHOST) }; // Spawn HTTP reverse proxy for .numa domains diff --git a/src/service_store.rs b/src/service_store.rs index 393d99e..e8a4ebc 100644 --- a/src/service_store.rs +++ b/src/service_store.rs @@ -24,11 +24,14 @@ impl ServiceEntry { /// Resolve backend port and (possibly rewritten) path for a request pub fn resolve_route(&self, request_path: &str) -> (u16, String) { // Longest prefix match - let matched = self.routes.iter() + let matched = self + .routes + .iter() .filter(|r| { request_path == r.path || request_path.starts_with(&r.path) - && (r.path.ends_with('/') || request_path.as_bytes().get(r.path.len()) == Some(&b'/')) + && (r.path.ends_with('/') + || request_path.as_bytes().get(r.path.len()) == Some(&b'/')) }) .max_by_key(|r| r.path.len());