osdns 0.2.0

Safe, transactional control of operating-system DNS configuration
Documentation
use std::path::Path;
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 network_manager = NetworkManager::connect().ok();
    let (nm_mode, nm_rc_manager) = network_manager
        .as_ref()
        .map(NetworkManager::dns_configuration)
        .unwrap_or(("", ""));

    if network_manager.is_some()
        && nm_mode == "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 = network_manager.is_some()
        && (file_owner == ResolvConfOwner::NetworkManager
            || nm_mode == "dnsmasq"
            || matches!(nm_rc_manager, "file" | "symlink" | "managed"));
    if nm_owns_directly {
        return Ok(Arc::new(network_manager.expect("checked above")));
    }

    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()))
}