refactor: single-pass resolv.conf parsing, eliminate redundancies

Parse resolv.conf once for both upstream and search domains instead
of 2-3 reads. Extract CLOUD_VPC_RESOLVER constant. Use &'static str
for mode in StatsResponse. Remove dead read_upstream_from_file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-04-01 06:19:55 +03:00
parent 58ac135654
commit f1266ee263
4 changed files with 84 additions and 96 deletions

View File

@@ -160,7 +160,7 @@ struct QueryLogResponse {
struct StatsResponse { struct StatsResponse {
uptime_secs: u64, uptime_secs: u64,
upstream: String, upstream: String,
mode: String, mode: &'static str, // "recursive" or "forward" — never "auto" at runtime
config_path: String, config_path: String,
data_dir: String, data_dir: String,
dnssec: bool, dnssec: bool,
@@ -487,7 +487,7 @@ async fn stats(State(ctx): State<Arc<ServerCtx>>) -> Json<StatsResponse> {
Json(StatsResponse { Json(StatsResponse {
uptime_secs: snap.uptime_secs, uptime_secs: snap.uptime_secs,
upstream, upstream,
mode: ctx.upstream_mode.as_str().to_string(), mode: ctx.upstream_mode.as_str(),
config_path: ctx.config_path.clone(), config_path: ctx.config_path.clone(),
data_dir: ctx.data_dir.to_string_lossy().to_string(), data_dir: ctx.data_dir.to_string_lossy().to_string(),
dnssec: ctx.dnssec_enabled, dnssec: ctx.dnssec_enabled,

View File

@@ -119,7 +119,7 @@ fn default_true() -> bool {
} }
fn default_srtt() -> bool { fn default_srtt() -> bool {
true default_true()
} }
fn default_prime_tlds() -> Vec<String> { fn default_prime_tlds() -> Vec<String> {

View File

@@ -108,7 +108,6 @@ async fn main() -> numa::Result<()> {
let root_hints = numa::recursive::parse_root_hints(&config.upstream.root_hints); let root_hints = numa::recursive::parse_root_hints(&config.upstream.root_hints);
// Resolve upstream mode + address in one block
let resolved_mode; let resolved_mode;
let upstream_auto; let upstream_auto;
let (upstream, upstream_label) = if config.upstream.mode == numa::config::UpstreamMode::Auto { let (upstream, upstream_label) = if config.upstream.mode == numa::config::UpstreamMode::Auto {

View File

@@ -163,111 +163,100 @@ fn make_rule(domain: &str, nameserver: &str) -> Option<ForwardingRule> {
}) })
} }
#[cfg(target_os = "linux")]
const CLOUD_VPC_RESOLVER: &str = "169.254.169.253";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn discover_linux() -> SystemDnsInfo { fn discover_linux() -> SystemDnsInfo {
let upstream = detect_upstream_linux_or_backup(); // Parse resolv.conf once for both upstream and search domains
let (upstream, search_domains) = parse_resolv_conf("/etc/resolv.conf");
// Parse search domains and create forwarding rules to the original nameserver. let default_upstream = if let Some(ns) = upstream {
// On cloud VMs (AWS/GCP), internal domains need to reach the VPC resolver.
let forwarding_rules = detect_search_domain_rules();
if !forwarding_rules.is_empty() {
info!(
"detected {} search domain forwarding rules",
forwarding_rules.len()
);
}
SystemDnsInfo {
default_upstream: upstream,
forwarding_rules,
}
}
/// Parse search domains from resolv.conf and create forwarding rules to the
/// original nameserver or the AWS VPC resolver (169.254.169.253).
#[cfg(target_os = "linux")]
fn detect_search_domain_rules() -> Vec<ForwardingRule> {
let mut rules = Vec::new();
// Find the original nameserver to forward internal domains to
let forwarder = find_original_nameserver().unwrap_or_else(|| "169.254.169.253".to_string());
// Parse search domains from resolv.conf
for path in &["/etc/resolv.conf"] {
if let Ok(text) = std::fs::read_to_string(path) {
for line in text.lines() {
let line = line.trim();
if line.starts_with("search") || line.starts_with("domain") {
for domain in line.split_whitespace().skip(1) {
if let Some(rule) = make_rule(domain, &forwarder) {
info!("forwarding .{} to {}", domain, forwarder);
rules.push(rule);
}
}
}
}
}
}
rules
}
/// Find the original (non-loopback) nameserver from resolv.conf or systemd-resolved config.
#[cfg(target_os = "linux")]
fn find_original_nameserver() -> Option<String> {
// Try resolv.conf for a real nameserver
if let Some(ns) = read_upstream_from_file("/etc/resolv.conf") {
return Some(ns);
}
// Try systemd-resolved's actual upstream
if let Ok(output) = std::process::Command::new("resolvectl")
.args(["status", "--no-pager"])
.output()
{
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if line.contains("DNS Servers") || line.contains("Current DNS Server") {
if let Some(ip) = line.split(':').next_back() {
let ip = ip.trim();
if !is_loopback_or_stub(ip) && !ip.is_empty() {
return Some(ip.to_string());
}
}
}
}
}
None
}
#[cfg(target_os = "linux")]
fn detect_upstream_linux_or_backup() -> Option<String> {
// Try /etc/resolv.conf first
if let Some(ns) = read_upstream_from_file("/etc/resolv.conf") {
info!("detected system upstream: {}", ns); info!("detected system upstream: {}", ns);
return Some(ns); Some(ns)
} } else {
// If resolv.conf only has loopback, check the backup from `numa install` // Fallback to backup from a previous `numa install`
let backup = { let backup = {
let home = std::env::var("HOME") let home = std::env::var("HOME")
.map(std::path::PathBuf::from) .map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/root")); .unwrap_or_else(|_| std::path::PathBuf::from("/root"));
home.join(".numa").join("original-resolv.conf") home.join(".numa").join("original-resolv.conf")
}; };
if let Some(ns) = read_upstream_from_file(backup.to_str().unwrap_or("")) { let (ns, _) = parse_resolv_conf(backup.to_str().unwrap_or(""));
if let Some(ref ns) = ns {
info!("detected original upstream from backup: {}", ns); info!("detected original upstream from backup: {}", ns);
return Some(ns);
} }
None ns
};
// On cloud VMs (AWS/GCP), internal domains need to reach the VPC resolver
let forwarding_rules = if search_domains.is_empty() {
Vec::new()
} else {
let forwarder = resolvectl_dns_server().unwrap_or_else(|| CLOUD_VPC_RESOLVER.to_string());
let rules: Vec<_> = search_domains
.iter()
.filter_map(|domain| {
let rule = make_rule(domain, &forwarder)?;
info!("forwarding .{} to {}", domain, forwarder);
Some(rule)
})
.collect();
if !rules.is_empty() {
info!("detected {} search domain forwarding rules", rules.len());
}
rules
};
SystemDnsInfo {
default_upstream,
forwarding_rules,
}
} }
/// Parse resolv.conf in a single pass, extracting both the first non-loopback
/// nameserver and all search domains.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn read_upstream_from_file(path: &str) -> Option<String> { fn parse_resolv_conf(path: &str) -> (Option<String>, Vec<String>) {
let text = std::fs::read_to_string(path).ok()?; let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(_) => return (None, Vec::new()),
};
let mut upstream = None;
let mut search_domains = Vec::new();
for line in text.lines() { for line in text.lines() {
let line = line.trim(); let line = line.trim();
if line.starts_with("nameserver") { if line.starts_with("nameserver") {
if upstream.is_none() {
if let Some(ns) = line.split_whitespace().nth(1) { if let Some(ns) = line.split_whitespace().nth(1) {
if !is_loopback_or_stub(ns) { if !is_loopback_or_stub(ns) {
return Some(ns.to_string()); upstream = Some(ns.to_string());
}
}
}
} else if line.starts_with("search") || line.starts_with("domain") {
for domain in line.split_whitespace().skip(1) {
search_domains.push(domain.to_string());
}
}
}
(upstream, search_domains)
}
/// Query resolvectl for the real upstream DNS server (e.g. VPC resolver on AWS).
#[cfg(target_os = "linux")]
fn resolvectl_dns_server() -> Option<String> {
let output = std::process::Command::new("resolvectl")
.args(["status", "--no-pager"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if line.contains("DNS Servers") || line.contains("Current DNS Server") {
if let Some(ip) = line.split(':').next_back() {
let ip = ip.trim();
if !is_loopback_or_stub(ip) {
return Some(ip.to_string());
} }
} }
} }