From 1a6a2a5f312cf538409631ac1d2df379f943d60d Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 29 Mar 2026 22:42:43 +0300 Subject: [PATCH 1/3] feat: resolve .numa services to LAN IP for remote clients Remote DNS clients (e.g. phones on same WiFi) received 127.0.0.1 for local .numa services, which is unreachable from their perspective. Now returns the host's LAN IP when the query originates from a non-loopback address. Also auto-widens proxy bind to 0.0.0.0 when DNS is already public, and adds a startup warning when the proxy remains localhost-only. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ctx.rs | 9 +++++++-- src/main.rs | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/ctx.rs b/src/ctx.rs index b21e20b..4e80b16 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -108,12 +108,17 @@ 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 + // Resolve .numa: remote clients get LAN IP (can't reach 127.0.0.1), local get loopback let service_name = qname.strip_suffix(&ctx.proxy_tld_suffix).unwrap_or(&qname); + let is_remote = !src_addr.ip().is_loopback(); let resolve_ip = { let local = ctx.services.lock().unwrap(); if local.lookup(service_name).is_some() { - std::net::Ipv4Addr::LOCALHOST + if is_remote { + *ctx.lan_ip.lock().unwrap() + } else { + std::net::Ipv4Addr::LOCALHOST + } } else { let mut peers = ctx.lan_peers.lock().unwrap(); peers diff --git a/src/main.rs b/src/main.rs index 3066fdd..77d1408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -208,6 +208,7 @@ async fn main() -> numa::Result<()> { }); let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum(); + let dns_is_public = config.server.bind_addr.starts_with("0.0.0.0"); // Build banner rows, then size the box to fit the longest value let api_url = format!("http://localhost:{}", api_port); @@ -308,6 +309,17 @@ async fn main() -> numa::Result<()> { ); if let Some(ref label) = proxy_label { row("Proxy", g, label); + if !config.lan.enabled && !dns_is_public && config.proxy.bind_addr == "127.0.0.1" { + let y = "\x1b[38;2;204;176;59m"; // yellow + row( + "", + y, + &format!( + "⚠ proxy on 127.0.0.1 — .{} not LAN reachable", + config.proxy.tld + ), + ); + } } if config.lan.enabled { row("LAN", g, "mDNS (_numa._tcp.local)"); @@ -375,8 +387,8 @@ async fn main() -> numa::Result<()> { axum::serve(listener, app).await.unwrap(); }); - // Proxy binds 0.0.0.0 when LAN is enabled (cross-machine access), otherwise config value - let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled { + // Proxy binds 0.0.0.0 when LAN is enabled or DNS is already on 0.0.0.0 (cross-machine access) + let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled || dns_is_public { std::net::Ipv4Addr::UNSPECIFIED } else { config -- 2.34.1 From 5bf2fefe2e172b497cbaeb8e9a06a24be14cb75c Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 29 Mar 2026 22:59:51 +0300 Subject: [PATCH 2/3] fix: respect proxy bind_addr config, don't auto-widen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-widen silently overrode an explicit config value — the user's config should be the source of truth. Now the proxy always uses the configured bind_addr, and the warning fires whenever it's 127.0.0.1. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 77d1408..5505392 100644 --- a/src/main.rs +++ b/src/main.rs @@ -208,8 +208,6 @@ async fn main() -> numa::Result<()> { }); let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum(); - let dns_is_public = config.server.bind_addr.starts_with("0.0.0.0"); - // Build banner rows, then size the box to fit the longest value let api_url = format!("http://localhost:{}", api_port); let proxy_label = if config.proxy.enabled { @@ -309,7 +307,7 @@ async fn main() -> numa::Result<()> { ); if let Some(ref label) = proxy_label { row("Proxy", g, label); - if !config.lan.enabled && !dns_is_public && config.proxy.bind_addr == "127.0.0.1" { + if config.proxy.bind_addr == "127.0.0.1" { let y = "\x1b[38;2;204;176;59m"; // yellow row( "", @@ -387,16 +385,11 @@ async fn main() -> numa::Result<()> { axum::serve(listener, app).await.unwrap(); }); - // Proxy binds 0.0.0.0 when LAN is enabled or DNS is already on 0.0.0.0 (cross-machine access) - let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled || dns_is_public { - std::net::Ipv4Addr::UNSPECIFIED - } else { - config - .proxy - .bind_addr - .parse() - .unwrap_or(std::net::Ipv4Addr::LOCALHOST) - }; + let proxy_bind: std::net::Ipv4Addr = config + .proxy + .bind_addr + .parse() + .unwrap_or(std::net::Ipv4Addr::LOCALHOST); // Spawn HTTP reverse proxy for .numa domains if config.proxy.enabled { -- 2.34.1 From 453c7733469f9729ebb5e051bd2e8007c36396fe Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 29 Mar 2026 23:04:57 +0300 Subject: [PATCH 3/3] docs: update proxy bind_addr comment in example config Co-Authored-By: Claude Opus 4.6 (1M context) --- numa.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numa.toml b/numa.toml index 6a523ac..4fa0a3d 100644 --- a/numa.toml +++ b/numa.toml @@ -54,7 +54,7 @@ enabled = true port = 80 tls_port = 443 tld = "numa" -# bind_addr = "127.0.0.1" # default; auto 0.0.0.0 when [lan] enabled +# bind_addr = "127.0.0.1" # default; set to "0.0.0.0" for LAN access to .numa services # Pre-configured services (numa.numa is always added automatically) # [[services]] -- 2.34.1