setdns 0.1.1

Temporarily apply and restore system DNS configuration, including global and split DNS.
Documentation
mod resolv_conf;
mod resolved;

use crate::{Error, Result, config::NormalizedConfig};

pub(crate) enum SetDns {
    Resolved(resolved::SetDns),
    ResolvConf(resolv_conf::SetDns),
}

#[derive(Debug, thiserror::Error)]
enum LinuxError {
    #[error("global DNS is unsupported: {reason}")]
    UnsupportedGlobalDns { reason: &'static str },
    #[error("split DNS requires systemd-resolved")]
    SplitDnsNeedsResolved {
        #[source]
        source: resolved::ResolvedError,
    },
    #[error("split DNS is unsupported: {reason}")]
    UnsupportedSplitDns { reason: &'static str },
}

impl From<LinuxError> for Error {
    fn from(error: LinuxError) -> Self {
        Self::Backend(Box::new(error))
    }
}

impl SetDns {
    pub(crate) fn apply(config: NormalizedConfig) -> Result<Self> {
        let resolved = resolved::Manager::connect();

        if config.domains.is_empty() {
            if let (Ok(manager), Some(_)) = (&resolved, config.device.as_ref()) {
                log::debug!(
                    "selected Linux systemd-resolved backend for global DNS on device {}",
                    config.device.as_deref().expect("device checked by pattern")
                );
                return manager.apply(config).map(Self::Resolved);
            }

            if resolv_conf::is_managed_by_resolved() {
                log::debug!(
                    "Linux global DNS without device is unsupported because /etc/resolv.conf is \
                     managed by systemd-resolved"
                );
                return Err(LinuxError::UnsupportedGlobalDns {
                    reason: "/etc/resolv.conf is managed by systemd-resolved and no device was \
                             specified",
                }
                .into());
            }

            log::debug!("selected Linux /etc/resolv.conf backend for global DNS");
            return resolv_conf::SetDns::apply(config).map(Self::ResolvConf);
        }

        let manager = resolved.map_err(|source| LinuxError::SplitDnsNeedsResolved { source })?;
        if config.device.is_none() {
            log::debug!("Linux split DNS is unsupported without a device/interface name");
            return Err(LinuxError::UnsupportedSplitDns {
                reason: "Linux split DNS requires a device/interface name",
            }
            .into());
        }

        log::debug!(
            "selected Linux systemd-resolved backend for split DNS on device {}",
            config.device.as_deref().expect("device checked above")
        );
        manager.apply(config).map(Self::Resolved)
    }

    pub(crate) fn close(self) -> Result<()> {
        match self {
            Self::Resolved(inner) => inner.close(),
            Self::ResolvConf(inner) => inner.close(),
        }
    }
}