4 Commits

Author SHA1 Message Date
Razvan Dimescu
e4a8893214 Merge pull request #30 from razvandimescu/release/v0.9.1
chore: bump version to 0.9.1
2026-04-03 00:39:45 +03:00
Razvan Dimescu
d979cd9505 chore: bump version to 0.9.1
Fix: forwarding rules ignored in recursive mode (Tailscale/VPN).
Fix: browsers treating .numa as search query (add search domain).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:08:36 +03:00
Razvan Dimescu
8c421b9fa3 fix: check forwarding rules before recursive resolution (#29)
Conditional forwarding (Tailscale .ts.net, VPC private zones) was
only checked in the forward mode branch. In recursive mode, queries
for forwarding-rule domains went to root servers instead of the
configured upstream, returning NXDOMAIN for private domains.

Move the forwarding rule check before the recursive/forward branch
so it takes priority regardless of mode.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:07:11 +03:00
Razvan Dimescu
ad7884f2f6 fix: add numa search domain on install for browser compatibility
Chrome treats single-label TLDs (e.g. frontend.numa) as search
queries unless a trailing slash is added. Adding "numa" as a search
domain tells the OS resolver that .numa is valid, so browsers
resolve it directly.

macOS: networksetup -setsearchdomains, cleared on uninstall
Linux (resolved): Domains=~. numa in drop-in
Linux (resolv.conf): search numa

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:50:22 +03:00
4 changed files with 38 additions and 5 deletions

2
Cargo.lock generated
View File

@@ -1143,7 +1143,7 @@ dependencies = [
[[package]] [[package]]
name = "numa" name = "numa"
version = "0.9.0" version = "0.9.1"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"axum", "axum",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "numa" name = "numa"
version = "0.9.0" version = "0.9.1"
authors = ["razvandimescu <razvan@dimescu.com>"] authors = ["razvandimescu <razvan@dimescu.com>"]
edition = "2021" edition = "2021"
description = "Portable DNS resolver in Rust — .numa local domains, ad blocking, developer overrides, DNS-over-HTTPS" description = "Portable DNS resolver in Rust — .numa local domains, ad blocking, developer overrides, DNS-over-HTTPS"

View File

@@ -162,6 +162,29 @@ pub async fn handle_query(
resp.header.authed_data = true; resp.header.authed_data = true;
} }
(resp, QueryPath::Cached, cached_dnssec) (resp, QueryPath::Cached, cached_dnssec)
} else if let Some(fwd_addr) =
crate::system_dns::match_forwarding_rule(&qname, &ctx.forwarding_rules)
{
// Conditional forwarding takes priority over recursive mode
// (e.g. Tailscale .ts.net, VPC private zones)
let upstream = Upstream::Udp(fwd_addr);
match forward_query(&query, &upstream, ctx.timeout).await {
Ok(resp) => {
ctx.cache.write().unwrap().insert(&qname, qtype, &resp);
(resp, QueryPath::Forwarded, DnssecStatus::Indeterminate)
}
Err(e) => {
error!(
"{} | {:?} {} | FORWARD ERROR | {}",
src_addr, qtype, qname, e
);
(
DnsPacket::response_from(&query, ResultCode::SERVFAIL),
QueryPath::UpstreamError,
DnssecStatus::Indeterminate,
)
}
}
} else if ctx.upstream_mode == UpstreamMode::Recursive { } else if ctx.upstream_mode == UpstreamMode::Recursive {
let key = (qname.clone(), qtype); let key = (qname.clone(), qtype);
let (resp, path, err) = resolve_coalesced(&ctx.inflight, key, &query, || { let (resp, path, err) = resolve_coalesced(&ctx.inflight, key, &query, || {

View File

@@ -776,7 +776,7 @@ fn install_macos() -> Result<(), String> {
.map_err(|e| format!("failed to serialize backup: {}", e))?; .map_err(|e| format!("failed to serialize backup: {}", e))?;
std::fs::write(backup_path(), json).map_err(|e| format!("failed to write backup: {}", e))?; std::fs::write(backup_path(), json).map_err(|e| format!("failed to write backup: {}", e))?;
// Set DNS to 127.0.0.1 for each service // Set DNS to 127.0.0.1 and add "numa" search domain for each service
for service in &services { for service in &services {
let status = std::process::Command::new("networksetup") let status = std::process::Command::new("networksetup")
.args(["-setdnsservers", service, "127.0.0.1"]) .args(["-setdnsservers", service, "127.0.0.1"])
@@ -788,6 +788,11 @@ fn install_macos() -> Result<(), String> {
} else { } else {
eprintln!(" warning: failed to set DNS for \"{}\"", service); eprintln!(" warning: failed to set DNS for \"{}\"", service);
} }
// Add "numa" as search domain so browsers resolve .numa without trailing slash
let _ = std::process::Command::new("networksetup")
.args(["-setsearchdomains", service, "numa"])
.status();
} }
eprintln!("\n Original DNS saved to {}", backup_path().display()); eprintln!("\n Original DNS saved to {}", backup_path().display());
@@ -832,6 +837,11 @@ fn uninstall_macos() -> Result<(), String> {
} else { } else {
eprintln!(" warning: failed to restore DNS for \"{}\"", service); eprintln!(" warning: failed to restore DNS for \"{}\"", service);
} }
// Clear the "numa" search domain
let _ = std::process::Command::new("networksetup")
.args(["-setsearchdomains", service, "Empty"])
.status();
} }
std::fs::remove_file(&path).ok(); std::fs::remove_file(&path).ok();
@@ -1092,7 +1102,7 @@ fn install_linux() -> Result<(), String> {
let drop_in = resolved_dir.join("numa.conf"); let drop_in = resolved_dir.join("numa.conf");
std::fs::write( std::fs::write(
&drop_in, &drop_in,
"[Resolve]\nDNS=127.0.0.1\nDomains=~.\nDNSStubListener=no\n", "[Resolve]\nDNS=127.0.0.1\nDomains=~. numa\nDNSStubListener=no\n",
) )
.map_err(|e| format!("failed to write {}: {}", drop_in.display(), e))?; .map_err(|e| format!("failed to write {}: {}", drop_in.display(), e))?;
@@ -1130,7 +1140,7 @@ fn install_linux() -> Result<(), String> {
} }
let content = let content =
"# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\n"; "# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\nsearch numa\n";
std::fs::write(resolv, content) std::fs::write(resolv, content)
.map_err(|e| format!("failed to write /etc/resolv.conf: {}", e))?; .map_err(|e| format!("failed to write /etc/resolv.conf: {}", e))?;