feat: resolve .numa services to LAN IP for remote clients (#23)

* 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) <noreply@anthropic.com>

* fix: respect proxy bind_addr config, don't auto-widen

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) <noreply@anthropic.com>

* docs: update proxy bind_addr comment in example config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #23.
This commit is contained in:
Razvan Dimescu
2026-03-29 23:15:42 +03:00
committed by GitHub
parent 32cd8624b4
commit 65dcd9a9c5
3 changed files with 24 additions and 14 deletions

View File

@@ -54,7 +54,7 @@ enabled = true
port = 80 port = 80
tls_port = 443 tls_port = 443
tld = "numa" 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) # Pre-configured services (numa.numa is always added automatically)
# [[services]] # [[services]]

View File

@@ -108,12 +108,17 @@ pub async fn handle_query(
} else if !ctx.proxy_tld_suffix.is_empty() } else if !ctx.proxy_tld_suffix.is_empty()
&& (qname.ends_with(&ctx.proxy_tld_suffix) || qname == ctx.proxy_tld) && (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 service_name = qname.strip_suffix(&ctx.proxy_tld_suffix).unwrap_or(&qname);
let is_remote = !src_addr.ip().is_loopback();
let resolve_ip = { let resolve_ip = {
let local = ctx.services.lock().unwrap(); let local = ctx.services.lock().unwrap();
if local.lookup(service_name).is_some() { 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 { } else {
let mut peers = ctx.lan_peers.lock().unwrap(); let mut peers = ctx.lan_peers.lock().unwrap();
peers peers

View File

@@ -208,7 +208,6 @@ async fn main() -> numa::Result<()> {
}); });
let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum(); let zone_count: usize = ctx.zone_map.values().map(|m| m.len()).sum();
// Build banner rows, then size the box to fit the longest value // Build banner rows, then size the box to fit the longest value
let api_url = format!("http://localhost:{}", api_port); let api_url = format!("http://localhost:{}", api_port);
let proxy_label = if config.proxy.enabled { let proxy_label = if config.proxy.enabled {
@@ -308,6 +307,17 @@ async fn main() -> numa::Result<()> {
); );
if let Some(ref label) = proxy_label { if let Some(ref label) = proxy_label {
row("Proxy", g, label); row("Proxy", g, label);
if 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 { if config.lan.enabled {
row("LAN", g, "mDNS (_numa._tcp.local)"); row("LAN", g, "mDNS (_numa._tcp.local)");
@@ -375,16 +385,11 @@ async fn main() -> numa::Result<()> {
axum::serve(listener, app).await.unwrap(); 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 = config
let proxy_bind: std::net::Ipv4Addr = if config.lan.enabled { .proxy
std::net::Ipv4Addr::UNSPECIFIED .bind_addr
} else { .parse()
config .unwrap_or(std::net::Ipv4Addr::LOCALHOST);
.proxy
.bind_addr
.parse()
.unwrap_or(std::net::Ipv4Addr::LOCALHOST)
};
// Spawn HTTP reverse proxy for .numa domains // Spawn HTTP reverse proxy for .numa domains
if config.proxy.enabled { if config.proxy.enabled {