osdns 0.1.1

Safe, transactional control of operating-system DNS configuration
Documentation
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::error::Result;
use crate::platform::Backend;
use crate::platform::linux::direct::DirectResolvConf;
use crate::platform::linux::network_manager::NetworkManager;
use crate::platform::linux::resolvconf::{self, Resolvconf};
use crate::platform::linux::resolved::SystemdResolved;

const RESOLV_CONF: &str = crate::platform::linux::RESOLV_CONF_PATH;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ResolvConfOwner {
    SystemdResolved,
    NetworkManager,
    Resolvconf,
    Foreign,
}

fn resolv_conf_owner() -> ResolvConfOwner {
    let path = Path::new(RESOLV_CONF);
    let target = std::fs::read_link(path)
        .map(|t| t.to_string_lossy().to_string())
        .ok();
    if let Some(target) = target {
        if target.contains("systemd/resolve") {
            return ResolvConfOwner::SystemdResolved;
        }
        if target.contains("NetworkManager") {
            return ResolvConfOwner::NetworkManager;
        }
        if target.contains("resolvconf") {
            return ResolvConfOwner::Resolvconf;
        }
        return ResolvConfOwner::Foreign;
    }
    if let Ok(bytes) = std::fs::read(path) {
        let head = String::from_utf8_lossy(&bytes);
        for line in head.lines().take(5) {
            let line = line.to_ascii_lowercase();
            if line.contains("generated by resolvconf") {
                return ResolvConfOwner::Resolvconf;
            }
            if line.contains("generated by networkmanager") {
                return ResolvConfOwner::NetworkManager;
            }
            if line.contains("generated by") && line.contains("resolved") {
                return ResolvConfOwner::SystemdResolved;
            }
        }
    }
    ResolvConfOwner::Foreign
}

fn resolvconf_indicated() -> bool {
    if resolvconf::probe().is_some() {
        return true;
    }
    Path::new("/etc/resolvconf.conf").is_file()
}

/// Determines who actually owns DNS configuration on this host, not merely
/// which components are installed.
///
/// Precedence (first match wins):
/// 1. `systemd-resolved` when it manages `/etc/resolv.conf` (stub/upstream
///    symlink, owned header, or `ResolvConfMode` stub/managed/upstream/static),
///    or when NetworkManager's DNS mode delegates to it.
/// 2. NetworkManager when it owns `/etc/resolv.conf` directly (symlink,
///    generated header, `dns=dnsmasq`, or an active `rc-manager` file mode).
/// 3. resolvconf/openresolv when its state directory exists and a binary is
///    available (including when NetworkManager delegates via
///    `rc-manager=resolvconf`).
/// 4. Direct `/etc/resolv.conf` manipulation as the last resort.
pub(crate) fn select(owner: &str) -> Result<Arc<dyn Backend>> {
    let file_owner = resolv_conf_owner();
    let resolved = SystemdResolved::connect().ok();
    let resolved_mode = resolved.as_ref().and_then(|backend| backend.mode_hint());
    let resolved_owns = resolved.is_some()
        && (matches!(
            resolved_mode.as_deref(),
            Some("stub") | Some("managed") | Some("upstream") | Some("static")
        ) || file_owner == ResolvConfOwner::SystemdResolved);

    if resolved_owns && let Some(backend) = resolved {
        return Ok(Arc::new(backend));
    }

    let nm_conf = nm_conf_main();
    let nm_available = NetworkManager::connect().is_ok();
    let nm_rc_manager = nm_conf.rc_manager.unwrap_or_else(|| "symlink".to_string());

    if nm_available
        && nm_conf.dns.as_deref() == Some("systemd-resolved")
        && let Some(backend) = resolved
    {
        return Ok(Arc::new(backend));
    }

    if nm_rc_manager == "resolvconf"
        && let Some(probe) = resolvconf::probe()
    {
        return Ok(Arc::new(Resolvconf::new(probe, owner)));
    }

    let nm_owns_directly = nm_available
        && (file_owner == ResolvConfOwner::NetworkManager
            || nm_conf.dns.as_deref() == Some("dnsmasq")
            || matches!(nm_rc_manager.as_str(), "file" | "symlink" | "managed"));
    if nm_owns_directly {
        return Ok(Arc::new(NetworkManager::connect()?));
    }

    let resolvconf_owns = file_owner == ResolvConfOwner::Resolvconf || resolvconf_indicated();
    if resolvconf_owns && let Some(probe) = resolvconf::probe() {
        return Ok(Arc::new(Resolvconf::new(probe, owner)));
    }

    if let Some(backend) = resolved {
        return Ok(Arc::new(backend));
    }

    Ok(Arc::new(DirectResolvConf::new()))
}

fn nm_conf_main() -> crate::platform::text_config::NmMainConf {
    let mut conf = crate::platform::text_config::NmMainConf::default();
    if let Ok(text) = std::fs::read_to_string("/etc/NetworkManager/NetworkManager.conf") {
        conf = crate::platform::text_config::parse_nm_main_conf(&text);
    }
    if conf.dns.is_none()
        && let Ok(entries) = std::fs::read_dir("/etc/NetworkManager/conf.d")
    {
        let mut paths: Vec<PathBuf> = entries
            .flatten()
            .map(|e| e.path())
            .filter(|p| p.extension().map(|e| e == "conf").unwrap_or(false))
            .collect();
        paths.sort();
        for path in paths {
            if let Ok(text) = std::fs::read_to_string(&path) {
                let parsed = crate::platform::text_config::parse_nm_main_conf(&text);
                if parsed.dns.is_some() {
                    conf.dns = parsed.dns;
                    break;
                }
                if conf.rc_manager.is_none() && parsed.rc_manager.is_some() {
                    conf.rc_manager = parsed.rc_manager;
                }
            }
        }
    }
    conf
}