generalize upstream re-detection into network change watcher

Always detect network changes (LAN IP, upstream, peers) regardless
of upstream config. LAN IP is now tracked in ServerCtx and updated
every 30s — multicast announcements use the current IP instead of
the startup IP. Upstream re-detection still only runs when
auto-detected. Peer flush triggers on any network change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-22 09:38:09 +02:00
parent f01b2418cd
commit 55ea49b003
3 changed files with 39 additions and 27 deletions

View File

@@ -33,8 +33,9 @@ pub struct ServerCtx {
pub lan_peers: Mutex<PeerStore>,
pub forwarding_rules: Vec<ForwardingRule>,
pub upstream: Mutex<SocketAddr>,
pub upstream_auto: bool, // true = auto-detected, false = explicitly configured
pub upstream_auto: bool,
pub upstream_port: u16,
pub lan_ip: Mutex<std::net::Ipv4Addr>,
pub timeout: Duration,
pub proxy_tld: String,
pub proxy_tld_suffix: String, // pre-computed ".{tld}" to avoid per-query allocation

View File

@@ -113,7 +113,7 @@ pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
.as_nanos() as u64;
pid ^ ts
};
let local_ip = detect_lan_ip().unwrap_or(Ipv4Addr::LOCALHOST);
let local_ip = *ctx.lan_ip.lock().unwrap();
info!(
"LAN discovery on {}:{}, local IP {}, instance {:016x}",
multicast_group, port, local_ip, instance_id
@@ -142,7 +142,6 @@ pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
// Spawn sender
let sender_ctx = Arc::clone(&ctx);
let sender_socket = Arc::clone(&socket);
let local_ip_str = local_ip.to_string();
let dest = SocketAddr::new(IpAddr::V4(multicast_group), port);
tokio::spawn(async move {
let mut ticker = tokio::time::interval(interval);
@@ -162,9 +161,10 @@ pub async fn start_lan_discovery(ctx: Arc<ServerCtx>, config: &LanConfig) {
if services.is_empty() {
continue;
}
let current_ip = sender_ctx.lan_ip.lock().unwrap().to_string();
let announcement = Announcement {
instance_id,
host: local_ip_str.clone(),
host: current_ip,
services,
};
if let Ok(json) = serde_json::to_vec(&announcement) {

View File

@@ -132,6 +132,7 @@ async fn main() -> numa::Result<()> {
upstream: Mutex::new(upstream),
upstream_auto: config.upstream.address.is_empty(),
upstream_port: config.upstream.port,
lan_ip: Mutex::new(numa::lan::detect_lan_ip().unwrap_or(std::net::Ipv4Addr::LOCALHOST)),
timeout: Duration::from_millis(config.upstream.timeout_ms),
proxy_tld_suffix: if config.proxy.tld.is_empty() {
String::new()
@@ -242,11 +243,11 @@ async fn main() -> numa::Result<()> {
}
}
// Spawn upstream re-detection (only for auto-detected upstream)
if ctx.upstream_auto {
let redetect_ctx = Arc::clone(&ctx);
// Spawn network change watcher (upstream re-detection, LAN IP update, peer flush)
{
let watch_ctx = Arc::clone(&ctx);
tokio::spawn(async move {
upstream_redetect_loop(redetect_ctx).await;
network_watch_loop(watch_ctx).await;
});
}
@@ -274,33 +275,43 @@ async fn main() -> numa::Result<()> {
}
}
async fn upstream_redetect_loop(ctx: Arc<numa::ctx::ServerCtx>) {
use numa::system_dns::discover_system_dns;
async fn network_watch_loop(ctx: Arc<numa::ctx::ServerCtx>) {
let mut interval = tokio::time::interval(Duration::from_secs(30));
interval.tick().await; // skip immediate tick
loop {
interval.tick().await;
let mut changed = false;
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,
};
// Check LAN IP change
if let Some(new_ip) = numa::lan::detect_lan_ip() {
let mut current_ip = ctx.lan_ip.lock().unwrap();
if new_ip != *current_ip {
info!("LAN IP changed: {} → {}", current_ip, new_ip);
*current_ip = new_ip;
changed = true;
}
}
// Check upstream change (only for auto-detected upstream)
if ctx.upstream_auto {
let dns_info = numa::system_dns::discover_system_dns();
if let Some(new_addr) = dns_info.default_upstream {
if let Ok(new_upstream) =
format!("{}:{}", new_addr, ctx.upstream_port).parse::<SocketAddr>()
{
let mut upstream = ctx.upstream.lock().unwrap();
let current = *upstream;
if new_upstream != current {
if new_upstream != *upstream {
info!("upstream changed: {} → {}", *upstream, new_upstream);
*upstream = new_upstream;
drop(upstream);
info!("upstream changed: {} → {}", current, new_upstream);
changed = true;
}
}
}
}
// Flush stale LAN peers from old network
// Flush stale LAN peers on any network change
if changed {
ctx.lan_peers.lock().unwrap().clear();
info!("flushed LAN peers after network change");
}