// translated from tailscale #32ce1bdb48078ec4cedaeeb5b1b2ff9c0ef61a49 use anyhow::{Context, Result}; use dbus::blocking::stdintf::org_freedesktop_dbus::Properties as _; use std::fs; use std::net::Ipv4Addr; use std::path::Path; use std::process::Command; use std::time::Duration; use version_compare::Cmp; // 声明依赖项(需要添加到Cargo.toml) // use dbus::blocking::Connection; // use nix::unistd::AccessFlags; // use resolv_conf::Resolver; // 常量定义 const RESOLV_CONF: &str = "/etc/resolv.conf"; const PING_TIMEOUT: Duration = Duration::from_secs(1); // 错误类型定义 #[derive(Debug)] struct DNSConfigError { message: String, source: Option, } type DbusPingFn = dyn Fn(&str, &str) -> Result<()>; type DbusReadStringFn = dyn Fn(&str, &str, &str, &str) -> Result; type NmIsUsingResolvedFn = dyn Fn() -> Result<()>; type NmVersionBetweenFn = dyn Fn(&str, &str) -> Result; type ResolvconfStyleFn = dyn Fn() -> String; // 配置环境结构体 struct OSConfigEnv { fs: Box, dbus_ping: Box, dbus_read_string: Box, nm_is_using_resolved: Box, nm_version_between: Box, resolvconf_style: Box String>, } // DNS管理器trait trait OSConfigurator: Send + Sync { // 实现相关方法 } // 文件系统操作trait trait FileSystem { fn read_file(&self, path: &str) -> Result>; fn exists(&self, path: &str) -> bool; } // 直接文件系统实现 struct DirectFS; impl FileSystem for DirectFS { fn read_file(&self, path: &str) -> Result> { fs::read(path).context("Failed to read file") } fn exists(&self, path: &str) -> bool { Path::new(path).exists() } } /// 检查 NetworkManager 是否使用 systemd-resolved 作为 DNS 管理器 pub fn nm_is_using_resolved() -> Result<()> { // 连接系统 D-Bus let conn = dbus::blocking::Connection::new_system().context("Failed to connect to D-Bus")?; // 创建 NetworkManager DnsManager 对象代理 let proxy = conn.with_proxy( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager", std::time::Duration::from_secs(1), ); // 获取 Mode 属性 let (value,): (dbus::arg::Variant>,) = proxy .method_call( "org.freedesktop.DBus.Properties", "Get", ("org.freedesktop.NetworkManager.DnsManager", "Mode"), ) .context("Failed to get NM mode property")?; // 检查 Mode 是否为 "systemd-resolved" if value.0.as_str() != Some("systemd-resolved") { return Err(anyhow::anyhow!( "NetworkManager is not using systemd-resolved, found: {:?}", value )); } Ok(()) } /// 返回系统中使用的 resolvconf 实现类型("debian" 或 "openresolv") pub fn resolvconf_style() -> String { // 检查 resolvconf 命令是否存在 if which::which("resolvconf").is_err() { return String::new(); } // 执行 resolvconf --version 命令 let output = match Command::new("resolvconf").arg("--version").output() { Ok(output) => output, Err(e) => { // 处理命令执行错误 if let Some(code) = e.raw_os_error() { // Debian 版本的 resolvconf 不支持 --version,返回特定错误码 99 if code == 99 { return "debian".to_string(); } } return String::new(); // 其他错误返回空字符串 } }; // 检查输出是否以 "Debian resolvconf" 开头 if output.stdout.starts_with(b"Debian resolvconf") { return "debian".to_string(); } // 默认视为 openresolv "openresolv".to_string() } // 构建配置环境 fn new_os_config_env() -> OSConfigEnv { OSConfigEnv { fs: Box::new(DirectFS), dbus_ping: Box::new(dbus_ping), dbus_read_string: Box::new(dbus_read_string), nm_is_using_resolved: Box::new(nm_is_using_resolved), nm_version_between: Box::new(nm_version_between), resolvconf_style: Box::new(resolvconf_style), } } // 创建DNS配置器 fn new_os_configurator(_interface_name: String) -> Result<()> { let env = new_os_config_env(); let mode = dns_mode(&env).context("Failed to detect DNS mode")?; tracing::info!("dns: using {} mode", mode); // match mode.as_str() { // "direct" => Ok(Box::new(DirectManager::new(env.fs)?)), // // "systemd-resolved" => Ok(Box::new(ResolvedManager::new( // // &logf, // // health, // // interface_name, // // )?)), // // "network-manager" => Ok(Box::new(NMManager::new(interface_name)?)), // // "debian-resolvconf" => Ok(Box::new(DebianResolvconfManager::new(&logf)?)), // // "openresolv" => Ok(Box::new(OpenresolvManager::new(&logf)?)), // _ => { // tracing::warn!("Unexpected DNS mode {}, using direct manager", mode); // Ok(Box::new(DirectManager::new(env.fs)?)) // } // } Ok(()) } use guarden::defer; use std::io::{self, BufRead, Cursor}; /// 返回 `resolv.conf` 内容的拥有者("systemd-resolved"、"NetworkManager"、"resolvconf" 或空字符串) pub fn resolv_owner(bs: &[u8]) -> String { let mut likely = String::new(); let cursor = Cursor::new(bs); let reader = io::BufReader::new(cursor); for line_result in reader.lines() { match line_result { Ok(line) => { let line = line.trim(); if line.is_empty() { continue; } if !line.starts_with('#') { // 第一个非注释且非空的行,直接返回当前结果 return likely; } // 检查注释行中的关键字 if line.contains("systemd-resolved") { likely = "systemd-resolved".to_string(); } else if line.contains("NetworkManager") { likely = "NetworkManager".to_string(); } else if line.contains("resolvconf") { likely = "resolvconf".to_string(); } } Err(_) => { // 读取错误(如无效 UTF-8),直接返回当前结果 return likely; } } } likely } // 检测DNS模式 fn dns_mode(env: &OSConfigEnv) -> Result { let debug = std::cell::RefCell::new(Vec::new()); let dbg = |k: &str, v: &str| debug.borrow_mut().push((k.to_string(), v.to_string())); // defer 日志记录 defer! { if !debug.borrow().is_empty() { let log_entries: Vec = debug.borrow().iter().map(|(k, v)| format!("{}={}", k, v)).collect(); tracing::info!("dns: [{}]", log_entries.join(" ")); } }; // 检查systemd-resolved状态 let resolved_up = (env.dbus_ping)("org.freedesktop.resolve1", "/org/freedesktop/resolve1").is_ok(); if resolved_up { dbg("resolved-ping", "yes"); } // 读取resolv.conf let content = match env.fs.read_file(RESOLV_CONF) { Ok(content) => content, Err(e) if e.to_string().contains("NotFound") => { dbg("rc", "missing"); return Ok("direct".to_string()); } Err(e) => return Err(e).context("reading /etc/resolv.conf"), }; // 检查resolv.conf所有者 match resolv_owner(&content).as_str() { "systemd-resolved" => { dbg("rc", "resolved"); // 检查是否实际使用resolved if let Err(e) = resolved_is_actually_resolver(env, &dbg, &content) { tracing::warn!("resolvedIsActuallyResolver error: {}", e); dbg("resolved", "not-in-use"); return Ok("direct".to_string()); } // NetworkManager检查逻辑... Ok("systemd-resolved".to_string()) } "resolvconf" => { // resolvconf处理逻辑... Ok("debian-resolvconf".to_string()) } "NetworkManager" => { // NetworkManager处理逻辑... Ok("systemd-resolved".to_string()) } _ => Ok("direct".to_string()), } } // D-Bus ping实现 fn dbus_ping(name: &str, object_path: &str) -> Result<()> { let conn = dbus::blocking::Connection::new_system()?; let proxy = conn.with_proxy(name, object_path, PING_TIMEOUT); let _: () = proxy.method_call("org.freedesktop.DBus.Peer", "Ping", ())?; Ok(()) } // D-Bus读取字符串实现 fn dbus_read_string(name: &str, object_path: &str, iface: &str, member: &str) -> Result { let conn = dbus::blocking::Connection::new_system()?; let proxy = conn.with_proxy(name, object_path, PING_TIMEOUT); let (value,): (String,) = proxy.method_call("org.freedesktop.DBus.Properties", "Get", (iface, member))?; Ok(value) } // NetworkManager版本检查 fn nm_version_between(first: &str, last: &str) -> Result { let conn = dbus::blocking::Connection::new_system()?; let proxy = conn.with_proxy( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", PING_TIMEOUT, ); let version: String = proxy.get("org.freedesktop.NetworkManager", "Version")?; let cmp_first = version_compare::compare(&version, first).unwrap_or(Cmp::Lt); let cmp_last = version_compare::compare(&version, last).unwrap_or(Cmp::Gt); Ok(cmp_first == Cmp::Ge && cmp_last == Cmp::Le) } // 检查是否实际使用systemd-resolved fn resolved_is_actually_resolver( env: &OSConfigEnv, dbg: &dyn Fn(&str, &str), content: &[u8], ) -> Result<()> { if is_libnss_resolve_used(env).is_ok() { dbg("resolved", "nss"); return Ok(()); } // 解析resolv.conf内容 let resolver = resolv_conf::Config::parse(content)?; // 检查nameserver配置 if resolver.nameservers.is_empty() { return Err(anyhow::anyhow!("resolv.conf has no nameservers")); } for ns in resolver.nameservers { if ns != Ipv4Addr::new(127, 0, 0, 53).into() { return Err(anyhow::anyhow!( "resolv.conf doesn't point to systemd-resolved" )); } } dbg("resolved", "file"); Ok(()) } // 检查是否使用libnss_resolve fn is_libnss_resolve_used(env: &OSConfigEnv) -> Result<()> { let content = env.fs.read_file("/etc/nsswitch.conf")?; for line in String::from_utf8_lossy(&content).lines() { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.first() == Some(&"hosts:") { for module in parts.iter().skip(1) { if *module == "dns" { return Err(anyhow::anyhow!("dns module has higher priority")); } if *module == "resolve" { return Ok(()); } } } } Err(anyhow::anyhow!("libnss_resolve not used")) } #[cfg(test)] mod tests { use super::*; #[test] fn dns_mode_test() { let env = new_os_config_env(); let mode = dns_mode(&env).unwrap(); println!("Detected DNS mode: {}", mode); } }