add LAN service discovery via UDP multicast

Numa instances on the same network auto-discover each other's .numa
services. No config, no cloud — just multicast on 239.255.70.78:5390.

- PeerStore with lazy expiry (90s timeout, 30s broadcast interval)
- DNS resolves remote .numa services to peer's LAN IP (not localhost)
- Proxy forwards to peer IP for remote services
- Graceful degradation if multicast bind fails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-21 16:45:46 +02:00
parent 6a8e47bbb5
commit c9f1d98f45
8 changed files with 310 additions and 11 deletions

View File

@@ -16,6 +16,7 @@ use crate::packet::DnsPacket;
use crate::query_log::{QueryLog, QueryLogEntry};
use crate::question::QueryType;
use crate::record::DnsRecord;
use crate::lan::PeerStore;
use crate::service_store::ServiceStore;
use crate::stats::{QueryPath, ServerStats};
use crate::system_dns::ForwardingRule;
@@ -29,6 +30,7 @@ pub struct ServerCtx {
pub blocklist: Mutex<BlocklistStore>,
pub query_log: Mutex<QueryLog>,
pub services: Mutex<ServiceStore>,
pub lan_peers: Mutex<PeerStore>,
pub forwarding_rules: Vec<ForwardingRule>,
pub upstream: SocketAddr,
pub timeout: Duration,
@@ -67,16 +69,39 @@ pub async fn handle_query(
} else if !ctx.proxy_tld_suffix.is_empty()
&& (qname.ends_with(&ctx.proxy_tld_suffix) || qname == ctx.proxy_tld)
{
// Resolve .numa: local services → 127.0.0.1, LAN peers → peer IP
let service_name = qname
.strip_suffix(&ctx.proxy_tld_suffix)
.unwrap_or(&qname);
let resolve_ip = {
let local = ctx.services.lock().unwrap();
if local.lookup(service_name).is_some() {
std::net::Ipv4Addr::LOCALHOST
} else {
let mut peers = ctx.lan_peers.lock().unwrap();
peers
.lookup(service_name)
.and_then(|(ip, _)| match ip {
std::net::IpAddr::V4(v4) => Some(v4),
_ => None,
})
.unwrap_or(std::net::Ipv4Addr::LOCALHOST)
}
};
let mut resp = DnsPacket::response_from(&query, ResultCode::NOERROR);
match qtype {
QueryType::AAAA => resp.answers.push(DnsRecord::AAAA {
domain: qname.clone(),
addr: std::net::Ipv6Addr::LOCALHOST,
addr: if resolve_ip == std::net::Ipv4Addr::LOCALHOST {
std::net::Ipv6Addr::LOCALHOST
} else {
resolve_ip.to_ipv6_mapped()
},
ttl: 300,
}),
_ => resp.answers.push(DnsRecord::A {
domain: qname.clone(),
addr: std::net::Ipv4Addr::LOCALHOST,
addr: resolve_ip,
ttl: 300,
}),
}