From 7aca3b1991097d573977289fd76d0c24139d96ad Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 22 Mar 2026 09:31:49 +0200 Subject: [PATCH] fix DNS failure on network change with upstream re-detection Upstream DNS was resolved once at startup and never updated. Switching Wi-Fi networks made all queries fail until restart. Now spawns a background task (every 30s) that re-runs system DNS discovery and swaps the upstream atomically if it changed. Also flushes stale LAN peers from the old network on change. Only activates when upstream is auto-detected (not explicitly configured). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api.rs | 3 ++- src/ctx.rs | 6 ++++-- src/lan.rs | 4 ++++ src/main.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/api.rs b/src/api.rs index a9bd7ab..0c6bc54 100644 --- a/src/api.rs +++ b/src/api.rs @@ -341,8 +341,9 @@ async fn diagnose( } // Check upstream (async, no locks held) + let upstream = *ctx.upstream.lock().unwrap(); let (upstream_matched, upstream_detail) = - forward_query_for_diagnose(&domain_lower, ctx.upstream, ctx.timeout).await; + forward_query_for_diagnose(&domain_lower, upstream, ctx.timeout).await; steps.push(DiagnoseStep { source: "upstream".to_string(), matched: upstream_matched, diff --git a/src/ctx.rs b/src/ctx.rs index 1a2f424..ebcbf97 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -32,7 +32,9 @@ pub struct ServerCtx { pub services: Mutex, pub lan_peers: Mutex, pub forwarding_rules: Vec, - pub upstream: SocketAddr, + pub upstream: Mutex, + pub upstream_auto: bool, // true = auto-detected, false = explicitly configured + pub upstream_port: u16, pub timeout: Duration, pub proxy_tld: String, pub proxy_tld_suffix: String, // pre-computed ".{tld}" to avoid per-query allocation @@ -132,7 +134,7 @@ pub async fn handle_query( } else { let upstream = crate::system_dns::match_forwarding_rule(&qname, &ctx.forwarding_rules) - .unwrap_or(ctx.upstream); + .unwrap_or_else(|| *ctx.upstream.lock().unwrap()); match forward_query(&query, upstream, ctx.timeout).await { Ok(resp) => { ctx.cache.lock().unwrap().insert(&qname, qtype, &resp); diff --git a/src/lan.rs b/src/lan.rs index 9a24b1e..917c886 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -57,6 +57,10 @@ impl PeerStore { }) .collect() } + + pub fn clear(&mut self) { + self.peers.clear(); + } } // --- Multicast --- diff --git a/src/main.rs b/src/main.rs index dd6cead..f706f6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,7 +129,9 @@ async fn main() -> numa::Result<()> { services: Mutex::new(service_store), lan_peers: Mutex::new(numa::lan::PeerStore::new(config.lan.peer_timeout_secs)), forwarding_rules, - upstream, + upstream: Mutex::new(upstream), + upstream_auto: config.upstream.address.is_empty(), + upstream_port: config.upstream.port, timeout: Duration::from_millis(config.upstream.timeout_ms), proxy_tld_suffix: if config.proxy.tld.is_empty() { String::new() @@ -240,6 +242,14 @@ async fn main() -> numa::Result<()> { } } + // Spawn upstream re-detection (only for auto-detected upstream) + if ctx.upstream_auto { + let redetect_ctx = Arc::clone(&ctx); + tokio::spawn(async move { + upstream_redetect_loop(redetect_ctx).await; + }); + } + // Spawn LAN service discovery if config.lan.enabled { let lan_ctx = Arc::clone(&ctx); @@ -264,6 +274,39 @@ async fn main() -> numa::Result<()> { } } +async fn upstream_redetect_loop(ctx: Arc) { + use numa::system_dns::discover_system_dns; + + let mut interval = tokio::time::interval(Duration::from_secs(30)); + interval.tick().await; // skip immediate tick + + loop { + interval.tick().await; + + let dns_info = discover_system_dns(); + let new_addr = match dns_info.default_upstream { + Some(addr) => addr, + None => continue, + }; + let new_upstream: SocketAddr = match format!("{}:{}", new_addr, ctx.upstream_port).parse() { + Ok(addr) => addr, + Err(_) => continue, + }; + + let mut upstream = ctx.upstream.lock().unwrap(); + let current = *upstream; + if new_upstream != current { + *upstream = new_upstream; + drop(upstream); + info!("upstream changed: {} → {}", current, new_upstream); + + // Flush stale LAN peers from old network + ctx.lan_peers.lock().unwrap().clear(); + info!("flushed LAN peers after network change"); + } + } +} + async fn load_blocklists(ctx: &ServerCtx, lists: &[String]) { let downloaded = download_blocklists(lists).await;