move system_config to dns::system

This commit is contained in:
Luna Yao
2026-02-23 03:40:16 +01:00
parent 202d8ec121
commit 4ed9cbc4a1
7 changed files with 6 additions and 7 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
pub mod config;
mod node;
mod node_mgr;
pub mod config;
mod peer_mgr;
pub mod server;
pub mod system;
mod utils;
pub mod zone;
+135
View File
@@ -0,0 +1,135 @@
use std::{
collections::HashSet,
fs::{self, OpenOptions},
io::{self, Write},
os::unix::fs::PermissionsExt,
path::Path,
};
use super::{OSConfig, SystemConfig};
const MAC_RESOLVER_FILE_HEADER: &str = "# Added by easytier\n";
const ETC_RESOLVER: &str = "/etc/resolver";
const ETC_RESOLV_CONF: &str = "/etc/resolv.conf";
#[derive(Default)]
pub struct DarwinConfigurator {}
impl DarwinConfigurator {
pub fn new() -> Self {
DarwinConfigurator {}
}
pub fn do_close(&self) -> io::Result<()> {
self.remove_resolver_files(|_| true)
}
pub fn supports_split_dns(&self) -> bool {
true
}
pub fn do_set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
fs::create_dir_all(ETC_RESOLVER)?;
let mut keep = HashSet::new();
// 写 search.easytier 文件
if !cfg.search_domains.is_empty() {
let search_file = "search.easytier";
keep.insert(search_file.to_string());
let mut content = String::from(MAC_RESOLVER_FILE_HEADER);
content.push_str("search");
for domain in &cfg.search_domains {
content.push(' ');
content.push_str(domain.trim_end_matches('.'));
}
content.push('\n');
Self::write_resolver_file(search_file, &content)?;
}
// 写 match_domains 文件
let mut ns_content = String::from(MAC_RESOLVER_FILE_HEADER);
for ns in &cfg.nameservers {
ns_content.push_str(&format!("nameserver {}\n", ns));
}
for domain in &cfg.match_domains {
let file_base = domain.trim_end_matches('.');
keep.insert(file_base.to_string());
Self::write_resolver_file(file_base, &ns_content)?;
}
// 删除未保留的 resolver 文件
self.remove_resolver_files(|domain| !keep.contains(domain))?;
Ok(())
}
fn write_resolver_file(file_name: &str, content: &str) -> io::Result<()> {
let path = Path::new(ETC_RESOLVER).join(file_name);
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&path)?;
file.set_permissions(fs::Permissions::from_mode(0o644))?;
file.write_all(content.as_bytes())?;
Ok(())
}
fn remove_resolver_files<F>(&self, should_delete: F) -> io::Result<()>
where
F: Fn(&str) -> bool,
{
let entries = match fs::read_dir(ETC_RESOLVER) {
Ok(e) => e,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
for entry in entries {
let entry = entry?;
let file_type = entry.file_type()?;
if !file_type.is_file() {
continue;
}
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !should_delete(&name_str) {
continue;
}
let full_path = entry.path();
let content = fs::read_to_string(&full_path)?;
if !content.starts_with(MAC_RESOLVER_FILE_HEADER) {
continue;
}
fs::remove_file(&full_path)?;
}
Ok(())
}
}
impl SystemConfig for DarwinConfigurator {
fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
self.do_set_dns(cfg)
}
fn close(&self) -> io::Result<()> {
self.do_close()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn set_dns_test() -> io::Result<()> {
let config = OSConfig {
nameservers: vec!["8.8.8.8".into()],
search_domains: vec!["example.com".into()],
match_domains: vec!["test.local".into()],
};
let configurator = DarwinConfigurator::new();
configurator.set_dns(&config)?;
configurator.close()?;
Ok(())
}
}
+362
View File
@@ -0,0 +1,362 @@
// translated from tailscale #32ce1bdb48078ec4cedaeeb5b1b2ff9c0ef61a49
use crate::defer;
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<anyhow::Error>,
}
type DbusPingFn = dyn Fn(&str, &str) -> Result<()>;
type DbusReadStringFn = dyn Fn(&str, &str, &str, &str) -> Result<String>;
type NmIsUsingResolvedFn = dyn Fn() -> Result<()>;
type NmVersionBetweenFn = dyn Fn(&str, &str) -> Result<bool>;
type ResolvconfStyleFn = dyn Fn() -> String;
// 配置环境结构体
struct OSConfigEnv {
fs: Box<dyn FileSystem>,
dbus_ping: Box<DbusPingFn>,
dbus_read_string: Box<DbusReadStringFn>,
nm_is_using_resolved: Box<NmIsUsingResolvedFn>,
nm_version_between: Box<NmVersionBetweenFn>,
resolvconf_style: Box<dyn Fn() -> String>,
}
// DNS管理器trait
trait OSConfigurator: Send + Sync {
// 实现相关方法
}
// 文件系统操作trait
trait FileSystem {
fn read_file(&self, path: &str) -> Result<Vec<u8>>;
fn exists(&self, path: &str) -> bool;
}
// 直接文件系统实现
struct DirectFS;
impl FileSystem for DirectFS {
fn read_file(&self, path: &str) -> Result<Vec<u8>> {
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<Box<dyn dbus::arg::RefArg + 'static>>,) = 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 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<String> {
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<String> =
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<String> {
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<bool> {
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);
}
}
+20
View File
@@ -0,0 +1,20 @@
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(all(target_os = "macos", not(feature = "macos-ne")))]
pub mod darwin;
#[derive(Default, Debug)]
pub struct OSConfig {
pub nameservers: Vec<String>,
pub search_domains: Vec<String>,
pub match_domains: Vec<String>,
}
pub trait SystemConfig: Send + Sync {
fn set_dns(&self, cfg: &OSConfig) -> std::io::Result<()>;
fn close(&self) -> std::io::Result<()>;
}
+246
View File
@@ -0,0 +1,246 @@
use std::net::IpAddr;
use std::process::Command;
use std::io;
use winreg::RegKey;
use crate::common::ifcfg::RegistryManager;
use super::{OSConfig, SystemConfig};
pub fn is_windows_10_or_better() -> io::Result<bool> {
let hklm = winreg::enums::HKEY_LOCAL_MACHINE;
let key_path = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
let key = winreg::RegKey::predef(hklm).open_subkey(key_path)?;
// check CurrentMajorVersionNumber, which only exists on Windows 10 and later
let value_name = "CurrentMajorVersionNumber";
key.get_raw_value(value_name).map(|_| true)
}
// 假设 interface_guid 是你的网络接口 GUID
pub struct InterfaceControl {
interface_guid: String,
}
impl InterfaceControl {
// 构造函数
pub fn new(interface_guid: &str) -> Self {
InterfaceControl {
interface_guid: interface_guid.to_string(),
}
}
// 删除注册表值(模拟 delValue)
fn delete_value(key: &RegKey, value_name: &str) -> io::Result<()> {
match key.delete_value(value_name) {
Ok(_) => Ok(()),
Err(e) => {
if matches!(e.kind(), io::ErrorKind::NotFound) {
Ok(()) // 忽略不存在的值
} else {
Err(e)
}
}
}
}
pub fn set_primary_dns(&self, resolvers: &[IpAddr], domains: &[String]) -> io::Result<()> {
let (ipsv4, ipsv6): (Vec<String>, Vec<String>) = resolvers
.iter()
.map(|ip| ip.to_string())
.partition(|ip| ip.contains('.'));
let dom_strs: Vec<String> = domains
.iter()
.map(|d| d.trim_end_matches('.').to_string())
.collect();
// IPv4 处理
if let Ok(key4) = RegistryManager::open_interface_key(
&self.interface_guid,
RegistryManager::IPV4_TCPIP_INTERFACE_PREFIX,
) {
if ipsv4.is_empty() {
Self::delete_value(&key4, "NameServer")?;
} else {
key4.set_value("NameServer", &ipsv4.join(","))?;
}
if dom_strs.is_empty() {
Self::delete_value(&key4, "SearchList")?;
} else {
key4.set_value("SearchList", &dom_strs.join(","))?;
}
// 禁用 LLMNR(通过 DisableMulticast
key4.set_value("EnableMulticast", &0u32)?;
}
// IPv6 处理
if let Ok(key6) = RegistryManager::open_interface_key(
&self.interface_guid,
RegistryManager::IPV6_TCPIP_INTERFACE_PREFIX,
) {
if ipsv6.is_empty() {
Self::delete_value(&key6, "NameServer")?;
} else {
key6.set_value("NameServer", &ipsv6.join(","))?;
}
if dom_strs.is_empty() {
Self::delete_value(&key6, "SearchList")?;
} else {
key6.set_value("SearchList", &dom_strs.join(","))?;
}
key6.set_value("EnableMulticast", &0u32)?;
}
Ok(())
}
fn flush_dns(&self) -> io::Result<()> {
// 刷新 DNS 缓存
let output = Command::new("ipconfig")
.arg("/flushdns")
.output()
.expect("failed to execute process");
if !output.status.success() {
return Err(io::Error::other("Failed to flush DNS cache"));
}
Ok(())
}
// re-register DNS
pub fn re_register_dns(&self) -> io::Result<()> {
// ipconfig /registerdns
let output = Command::new("ipconfig")
.arg("/registerdns")
.output()
.expect("failed to execute process");
if !output.status.success() {
return Err(io::Error::other("Failed to register DNS"));
}
Ok(())
}
}
pub struct WindowsDNSManager {
tun_dev_name: String,
interface_control: InterfaceControl,
}
impl WindowsDNSManager {
pub fn new(tun_dev_name: &str) -> io::Result<Self> {
let interface_guid = RegistryManager::find_interface_guid(tun_dev_name)?;
Ok(WindowsDNSManager {
tun_dev_name: tun_dev_name.to_string(),
interface_control: InterfaceControl::new(&interface_guid),
})
}
pub fn set_primary_dns(&self, resolvers: &[IpAddr], domains: &[String]) -> io::Result<()> {
self.interface_control.set_primary_dns(resolvers, domains)?;
self.interface_control.flush_dns()?;
Ok(())
}
}
impl SystemConfig for WindowsDNSManager {
fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> {
self.set_primary_dns(
&cfg.nameservers
.iter()
.map(|s| s.parse::<IpAddr>().unwrap())
.collect::<Vec<_>>(),
&cfg.match_domains,
)?;
Ok(())
}
fn close(&self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use cidr::Ipv4Inet;
#[cfg(target_os = "windows")]
#[tokio::test]
async fn test_windows_set_primary_server() {
use std::{net::Ipv4Addr, str::FromStr as _, time::Duration};
use tokio_util::sync::CancellationToken;
use crate::instance::dns_server::{
runner::DnsRunner,
tests::{check_dns_record, prepare_env},
};
let tun_ip = Ipv4Inet::from_str("10.144.144.10/24").unwrap();
let (peer_mgr, virtual_nic) = prepare_env("test1", tun_ip).await;
let tun_name = virtual_nic.ifname().await.unwrap();
println!("dev_name: {}", tun_name);
let fake_ip = Ipv4Addr::from_str("100.100.100.101").unwrap();
let mut dns_runner = DnsRunner::new(peer_mgr, Some(tun_name.clone()), tun_ip, fake_ip);
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
let t = tokio::spawn(async move {
dns_runner.run(cancel_token_clone).await;
});
// windows is slow to add a ip address, wait for a longer time for dns server ready ,with ping
let now = std::time::Instant::now();
while now.elapsed() < Duration::from_secs(15) {
tokio::time::sleep(Duration::from_secs(1)).await;
if let Ok(o) = tokio::process::Command::new("ping")
.arg("-n")
.arg("1")
.arg("-w")
.arg("100")
.arg(&fake_ip.to_string())
.output()
.await
{
if o.status.success() {
break;
}
}
}
check_dns_record(&fake_ip, "test1.et.net", "10.144.144.10").await;
let dns_mgr = super::WindowsDNSManager::new(&tun_name).unwrap();
println!("dev_name: {}", tun_name);
println!("guid: {}", dns_mgr.interface_control.interface_guid);
dns_mgr
.interface_control
.set_primary_dns(
&["100.100.100.101".parse().unwrap()],
&[".et.net.".to_string()],
)
.unwrap();
dns_mgr.interface_control.flush_dns().unwrap();
tracing::info!("check dns record with nslookup");
// nslookup should return 10.144.144.10
let ret = tokio::process::Command::new("nslookup")
.arg("test1.et.net")
.output()
.await
.expect("failed to execute process");
assert!(ret.status.success());
let output = String::from_utf8_lossy(&ret.stdout);
println!("nslookup output: {}", output);
assert!(output.contains("10.144.144.10"));
cancel_token.cancel();
let _ = t.await;
}
}