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:
@@ -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,
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -164,110 +164,99 @@ fn make_rule(domain: &str, nameserver: &str) -> Option<ForwardingRule> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn discover_linux() -> SystemDnsInfo {
|
const CLOUD_VPC_RESOLVER: &str = "169.254.169.253";
|
||||||
let upstream = detect_upstream_linux_or_backup();
|
|
||||||
|
|
||||||
// Parse search domains and create forwarding rules to the original nameserver.
|
#[cfg(target_os = "linux")]
|
||||||
// On cloud VMs (AWS/GCP), internal domains need to reach the VPC resolver.
|
fn discover_linux() -> SystemDnsInfo {
|
||||||
let forwarding_rules = detect_search_domain_rules();
|
// Parse resolv.conf once for both upstream and search domains
|
||||||
if !forwarding_rules.is_empty() {
|
let (upstream, search_domains) = parse_resolv_conf("/etc/resolv.conf");
|
||||||
info!(
|
|
||||||
"detected {} search domain forwarding rules",
|
let default_upstream = if let Some(ns) = upstream {
|
||||||
forwarding_rules.len()
|
info!("detected system upstream: {}", ns);
|
||||||
);
|
Some(ns)
|
||||||
}
|
} else {
|
||||||
|
// Fallback to backup from a previous `numa install`
|
||||||
|
let backup = {
|
||||||
|
let home = std::env::var("HOME")
|
||||||
|
.map(std::path::PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| std::path::PathBuf::from("/root"));
|
||||||
|
home.join(".numa").join("original-resolv.conf")
|
||||||
|
};
|
||||||
|
let (ns, _) = parse_resolv_conf(backup.to_str().unwrap_or(""));
|
||||||
|
if let Some(ref ns) = ns {
|
||||||
|
info!("detected original upstream from backup: {}", ns);
|
||||||
|
}
|
||||||
|
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 {
|
SystemDnsInfo {
|
||||||
default_upstream: upstream,
|
default_upstream,
|
||||||
forwarding_rules,
|
forwarding_rules,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse search domains from resolv.conf and create forwarding rules to the
|
/// Parse resolv.conf in a single pass, extracting both the first non-loopback
|
||||||
/// original nameserver or the AWS VPC resolver (169.254.169.253).
|
/// nameserver and all search domains.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn detect_search_domain_rules() -> Vec<ForwardingRule> {
|
fn parse_resolv_conf(path: &str) -> (Option<String>, Vec<String>) {
|
||||||
let mut rules = Vec::new();
|
let text = match std::fs::read_to_string(path) {
|
||||||
|
Ok(t) => t,
|
||||||
// Find the original nameserver to forward internal domains to
|
Err(_) => return (None, Vec::new()),
|
||||||
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);
|
|
||||||
return Some(ns);
|
|
||||||
}
|
|
||||||
// If resolv.conf only has loopback, check the backup from `numa install`
|
|
||||||
let backup = {
|
|
||||||
let home = std::env::var("HOME")
|
|
||||||
.map(std::path::PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| std::path::PathBuf::from("/root"));
|
|
||||||
home.join(".numa").join("original-resolv.conf")
|
|
||||||
};
|
};
|
||||||
if let Some(ns) = read_upstream_from_file(backup.to_str().unwrap_or("")) {
|
let mut upstream = None;
|
||||||
info!("detected original upstream from backup: {}", ns);
|
let mut search_domains = Vec::new();
|
||||||
return Some(ns);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn read_upstream_from_file(path: &str) -> Option<String> {
|
|
||||||
let text = std::fs::read_to_string(path).ok()?;
|
|
||||||
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 let Some(ns) = line.split_whitespace().nth(1) {
|
if upstream.is_none() {
|
||||||
if !is_loopback_or_stub(ns) {
|
if let Some(ns) = line.split_whitespace().nth(1) {
|
||||||
return Some(ns.to_string());
|
if !is_loopback_or_stub(ns) {
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user