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) <noreply@anthropic.com>
This commit is contained in:
@@ -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?
|
/// Spike: can we bind to mDNS multicast (224.0.0.251:5353) alongside macOS mDNSResponder?
|
||||||
///
|
///
|
||||||
/// Tests:
|
/// Tests:
|
||||||
@@ -8,10 +9,8 @@
|
|||||||
/// 5. Send a _numa._tcp.local announcement — does it conflict?
|
/// 5. Send a _numa._tcp.local announcement — does it conflict?
|
||||||
///
|
///
|
||||||
/// Run: cargo run --example mdns_coexist
|
/// Run: cargo run --example mdns_coexist
|
||||||
|
|
||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||||
use socket2::{Domain, Protocol, Socket, Type};
|
|
||||||
|
|
||||||
const MDNS_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
|
const MDNS_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
|
||||||
const MDNS_PORT: u16 = 5353;
|
const MDNS_PORT: u16 = 5353;
|
||||||
@@ -66,7 +65,8 @@ fn main() -> std::io::Result<()> {
|
|||||||
loop {
|
loop {
|
||||||
match socket.recv_from(&mut buf) {
|
match socket.recv_from(&mut buf) {
|
||||||
Ok((n, addr)) => {
|
Ok((n, addr)) => {
|
||||||
let data: &[u8] = unsafe { &*(&buf[..n] as *const [MaybeUninit<u8>] as *const [u8]) };
|
let data: &[u8] =
|
||||||
|
unsafe { &*(&buf[..n] as *const [MaybeUninit<u8>] as *const [u8]) };
|
||||||
count += 1;
|
count += 1;
|
||||||
let flags = u16::from_be_bytes([data[2], data[3]]);
|
let flags = u16::from_be_bytes([data[2], data[3]]);
|
||||||
let is_response = flags & 0x8000 != 0;
|
let is_response = flags & 0x8000 != 0;
|
||||||
@@ -98,7 +98,8 @@ fn main() -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Send a _numa._tcp.local announcement
|
// 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()) {
|
match socket.send_to(&announcement, &dest.into()) {
|
||||||
Ok(n) => println!("\n[OK] Sent _numa._tcp.local announcement ({} bytes)", n),
|
Ok(n) => println!("\n[OK] Sent _numa._tcp.local announcement ({} bytes)", n),
|
||||||
Err(e) => println!("\n[FAIL] Cannot send announcement: {}", e),
|
Err(e) => println!("\n[FAIL] Cannot send announcement: {}", e),
|
||||||
@@ -111,7 +112,8 @@ fn main() -> std::io::Result<()> {
|
|||||||
loop {
|
loop {
|
||||||
match socket.recv_from(&mut buf2) {
|
match socket.recv_from(&mut buf2) {
|
||||||
Ok((n, addr)) => {
|
Ok((n, addr)) => {
|
||||||
let data: &[u8] = unsafe { &*(&buf2[..n] as *const [MaybeUninit<u8>] as *const [u8]) };
|
let data: &[u8] =
|
||||||
|
unsafe { &*(&buf2[..n] as *const [MaybeUninit<u8>] as *const [u8]) };
|
||||||
let flags = u16::from_be_bytes([data[2], data[3]]);
|
let flags = u16::from_be_bytes([data[2], data[3]]);
|
||||||
let is_response = flags & 0x8000 != 0;
|
let is_response = flags & 0x8000 != 0;
|
||||||
if is_response {
|
if is_response {
|
||||||
@@ -133,7 +135,10 @@ fn main() -> std::io::Result<()> {
|
|||||||
// Verdict
|
// Verdict
|
||||||
println!("\n=== Verdict ===");
|
println!("\n=== Verdict ===");
|
||||||
if count > 0 {
|
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");
|
println!(" Safe to proceed with mDNS-based LAN discovery");
|
||||||
} else {
|
} else {
|
||||||
println!("[WARN] No mDNS packets received — may need further investigation");
|
println!("[WARN] No mDNS packets received — may need further investigation");
|
||||||
|
|||||||
@@ -761,7 +761,10 @@ async fn add_route(
|
|||||||
if store.add_route(&name, req.path, req.port, req.strip) {
|
if store.add_route(&name, req.path, req.port, req.strip) {
|
||||||
Ok(StatusCode::CREATED)
|
Ok(StatusCode::CREATED)
|
||||||
} else {
|
} else {
|
||||||
Err((StatusCode::NOT_FOUND, format!("service '{}' not found", name)))
|
Err((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
format!("service '{}' not found", name),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
src/lan.rs
86
src/lan.rs
@@ -96,10 +96,15 @@ fn get_hostname() -> String {
|
|||||||
|
|
||||||
/// Generate a per-process instance ID for self-filtering on multi-instance hosts
|
/// Generate a per-process instance ID for self-filtering on multi-instance hosts
|
||||||
fn instance_id() -> String {
|
fn instance_id() -> String {
|
||||||
format!("{}:{}", std::process::id(), std::time::SystemTime::now()
|
format!(
|
||||||
|
"{}:{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_nanos() % 1_000_000)
|
.as_nanos()
|
||||||
|
% 1_000_000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
|
pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
|
||||||
@@ -116,7 +121,10 @@ pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
|
|||||||
let std_socket = match create_mdns_socket() {
|
let std_socket = match create_mdns_socket() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -141,13 +149,19 @@ pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
|
|||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
let services: Vec<(String, u16)> = {
|
let services: Vec<(String, u16)> = {
|
||||||
let store = sender_ctx.services.lock().unwrap();
|
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() {
|
if services.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let current_ip = *sender_ctx.lan_ip.lock().unwrap();
|
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;
|
let _ = sender_socket.send_to(pkt.filled(), dest).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,14 +184,21 @@ pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let data = &buf[..len];
|
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)
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
if !services.is_empty() {
|
if !ann.services.is_empty() {
|
||||||
ctx.lan_peers.lock().unwrap().update(peer_ip, &services);
|
ctx.lan_peers
|
||||||
debug!("LAN: {} services from {} (mDNS)", services.len(), peer_ip);
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.update(ann.peer_ip, &ann.services);
|
||||||
|
debug!(
|
||||||
|
"LAN: {} services from {} (mDNS)",
|
||||||
|
ann.services.len(),
|
||||||
|
ann.peer_ip
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +244,13 @@ fn build_announcement(
|
|||||||
|
|
||||||
// SRV: <instance>._numa._tcp.local → <hostname>.local
|
// SRV: <instance>._numa._tcp.local → <hostname>.local
|
||||||
// Port in SRV is informational; actual service ports are in TXT
|
// 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();
|
let rdlen_pos = buf.pos();
|
||||||
buf.write_u16(0)?;
|
buf.write_u16(0)?;
|
||||||
let rdata_start = buf.pos();
|
let rdata_start = buf.pos();
|
||||||
@@ -234,7 +261,13 @@ fn build_announcement(
|
|||||||
patch_rdlen(&mut buf, rdlen_pos, rdata_start)?;
|
patch_rdlen(&mut buf, rdlen_pos, rdata_start)?;
|
||||||
|
|
||||||
// TXT: services + instance ID for self-filtering
|
// 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();
|
let rdlen_pos = buf.pos();
|
||||||
buf.write_u16(0)?;
|
buf.write_u16(0)?;
|
||||||
let rdata_start = buf.pos();
|
let rdata_start = buf.pos();
|
||||||
@@ -248,7 +281,13 @@ fn build_announcement(
|
|||||||
patch_rdlen(&mut buf, rdlen_pos, rdata_start)?;
|
patch_rdlen(&mut buf, rdlen_pos, rdata_start)?;
|
||||||
|
|
||||||
// A: <hostname>.local → IP
|
// A: <hostname>.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)?;
|
buf.write_u16(4)?;
|
||||||
for &b in &ip.octets() {
|
for &b in &ip.octets() {
|
||||||
buf.write_u8(b)?;
|
buf.write_u8(b)?;
|
||||||
@@ -271,7 +310,11 @@ fn write_record_header(
|
|||||||
Ok(())
|
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;
|
let rdlen = (buf.pos() - rdata_start) as u16;
|
||||||
buf.set_u16(rdlen_pos, rdlen)
|
buf.set_u16(rdlen_pos, rdlen)
|
||||||
}
|
}
|
||||||
@@ -289,8 +332,13 @@ fn write_txt_string(buf: &mut BytePacketBuffer, s: &str) -> crate::Result<()> {
|
|||||||
|
|
||||||
// --- mDNS Packet Parsing ---
|
// --- mDNS Packet Parsing ---
|
||||||
|
|
||||||
/// Returns (services, peer_ip, instance_id) if this is a Numa mDNS announcement
|
struct MdnsAnnouncement {
|
||||||
fn parse_mdns_response(data: &[u8]) -> Option<(Vec<(String, u16)>, IpAddr, Option<String>)> {
|
services: Vec<(String, u16)>,
|
||||||
|
peer_ip: IpAddr,
|
||||||
|
instance_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mdns_response(data: &[u8]) -> Option<MdnsAnnouncement> {
|
||||||
if data.len() < 12 {
|
if data.len() < 12 {
|
||||||
return None;
|
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
|
// Trust the A record IP if present, otherwise this isn't a complete announcement
|
||||||
let peer_ip = a_ip?;
|
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<std::net::UdpSocket> {
|
fn create_mdns_socket() -> std::io::Result<std::net::UdpSocket> {
|
||||||
|
|||||||
@@ -217,7 +217,11 @@ async fn main() -> numa::Result<()> {
|
|||||||
let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled {
|
let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled {
|
||||||
std::net::Ipv4Addr::UNSPECIFIED
|
std::net::Ipv4Addr::UNSPECIFIED
|
||||||
} else {
|
} 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
|
// Spawn HTTP reverse proxy for .numa domains
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ impl ServiceEntry {
|
|||||||
/// Resolve backend port and (possibly rewritten) path for a request
|
/// Resolve backend port and (possibly rewritten) path for a request
|
||||||
pub fn resolve_route(&self, request_path: &str) -> (u16, String) {
|
pub fn resolve_route(&self, request_path: &str) -> (u16, String) {
|
||||||
// Longest prefix match
|
// Longest prefix match
|
||||||
let matched = self.routes.iter()
|
let matched = self
|
||||||
|
.routes
|
||||||
|
.iter()
|
||||||
.filter(|r| {
|
.filter(|r| {
|
||||||
request_path == r.path
|
request_path == r.path
|
||||||
|| request_path.starts_with(&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());
|
.max_by_key(|r| r.path.len());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user