systemd-resolved-rs 0.1.1

A compatibility-oriented reimplementation of systemd-resolved
// SPDX-License-Identifier: LGPL-2.1-or-later
use crate::config::{DnsServerSpec, Domain, SupportMode, TlsMode, ValidationMode};
use crate::daemon::stop_requested;
use crate::dbus_resolve1_abi::flags::{
    SD_RESOLVED_DNS, SD_RESOLVED_LLMNR_IPV4, SD_RESOLVED_LLMNR_IPV6,
    SD_RESOLVED_MDNS_IPV4, SD_RESOLVED_MDNS_IPV6, SD_RESOLVED_NO_ADDRESS,
    SD_RESOLVED_NO_SEARCH, SD_RESOLVED_NO_TXT,
};
use crate::resolver::{AddressLookup, NameLookup, ResolveError, Resolver};
use crate::routing::{LinkError, LinkState};
use crate::wire::{
    extract_answer_records, extract_service_records_for_name, CLASS_IN, TYPE_A, TYPE_AAAA,
    Header, TYPE_SRV, TYPE_TXT,
};
use std::collections::{BTreeSet, HashMap};
use std::convert::TryFrom;
use std::error::Error;
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Duration;
use zbus::blocking::{Connection, ConnectionBuilder};
use zbus::dbus_interface;
use zbus::zvariant::OwnedObjectPath;
use zbus::MessageHeader;

const BUS_NAME: &str = "org.freedesktop.resolve1";
const MANAGER_PATH: &str = "/org/freedesktop/resolve1";
const LOG_CONTROL_PATH: &str = "/org/freedesktop/LogControl1";
const LINK_PATH_PREFIX: &str = "/org/freedesktop/resolve1/link";
const DNSSD_PATH_PREFIX: &str = "/org/freedesktop/resolve1/dnssd";
const DELEGATE_PATH_PREFIX: &str = "/org/freedesktop/resolve1/dns_delegate";
const AF_UNSPEC: i32 = 0;
const AF_INET: i32 = 2;
const AF_INET6: i32 = 10;
const DNS_PORT: u16 = 53;
type ClientQueryRegistry = Arc<Mutex<HashMap<String, Vec<crate::query_cancel::QueryCancellation>>>>;

#[derive(Debug)]
pub struct DbusServer {
    resolver: Arc<Resolver>,
    log_control: Arc<RwLock<LogControlState>>,
}

impl DbusServer {
    pub fn new(resolver: Arc<Resolver>) -> Self {
        Self {
            resolver,
            log_control: Arc::new(RwLock::new(LogControlState::default())),
        }
    }

    pub fn run(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
        let authorization = Arc::new(DbusAuthorization::new()?);
        let client_queries = ClientQueryRegistry::default();
        let manager = ManagerObject {
            resolver: Arc::clone(&self.resolver),
            authorization: Arc::clone(&authorization),
            client_queries: Arc::clone(&client_queries),
        };
        let log_control = LogControlObject {
            state: Arc::clone(&self.log_control),
        };
        let connection = ConnectionBuilder::system()?
            .name(BUS_NAME)?
            .serve_at(MANAGER_PATH, manager)?
            .serve_at(LOG_CONTROL_PATH, log_control)?
            .build()?;
        let bus = zbus::blocking::fdo::DBusProxy::new(&connection)?;
        register_delegate_objects(&connection, &self.resolver)?;
        let mut registered = BTreeSet::new();
        let mut registered_services = BTreeSet::new();

        while !stop_requested() {
            cancel_vanished_client_queries(&bus, &client_queries)?;
            remove_vanished_dnssd_services(&bus)?;
            synchronize_link_objects(
                &connection,
                &self.resolver,
                &authorization,
                &mut registered,
            )?;
            synchronize_dnssd_objects(
                &connection,
                &authorization,
                &mut registered_services,
            )?;
            emit_dnssd_conflicts(&connection)?;
            thread::sleep(Duration::from_millis(100));
        }
        Ok(())
    }
}

#[derive(Debug)]
struct LogControlState {
    level: String,
    target: String,
}

impl Default for LogControlState {
    fn default() -> Self {
        Self {
            level: "info".to_owned(),
            target: "journal-or-kmsg".to_owned(),
        }
    }
}

#[derive(Debug)]
struct LogControlObject {
    state: Arc<RwLock<LogControlState>>,
}

#[dbus_interface(name = "org.freedesktop.LogControl1")]
impl LogControlObject {
    #[dbus_interface(property, name = "LogLevel")]
    fn log_level(&self) -> String {
        self.state
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .level
            .clone()
    }

    #[dbus_interface(property, name = "LogLevel")]
    fn set_log_level(&mut self, level: &str) -> zbus::fdo::Result<()> {
        if !valid_log_level(level) {
            return Err(zbus::fdo::Error::InvalidArgs(format!(
                "Invalid log level '{level}'"
            )));
        }
        self.state
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .level = level.to_owned();
        Ok(())
    }

    #[dbus_interface(property, name = "LogTarget")]
    fn log_target(&self) -> String {
        self.state
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .target
            .clone()
    }

    #[dbus_interface(property, name = "LogTarget")]
    fn set_log_target(&mut self, target: &str) -> zbus::fdo::Result<()> {
        if !valid_log_target(target) {
            return Err(zbus::fdo::Error::InvalidArgs(format!(
                "Invalid log target '{target}'"
            )));
        }
        self.state
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .target = target.to_owned();
        Ok(())
    }

    #[dbus_interface(property, name = "SyslogIdentifier")]
    fn syslog_identifier(&self) -> &str {
        "systemd-resolved"
    }
}

fn valid_log_level(level: &str) -> bool {
    matches!(
        level,
        "emerg" | "alert" | "crit" | "err" | "warning" | "notice" | "info" | "debug"
    )
}

fn valid_log_target(target: &str) -> bool {
    matches!(
        target,
        "console"
            | "console-prefixed"
            | "kmsg"
            | "journal"
            | "journal-or-kmsg"
            | "syslog"
            | "syslog-or-kmsg"
            | "auto"
            | "null"
    )
}

#[derive(Debug, zbus::DBusError)]
#[dbus_error(prefix = "org.freedesktop")]
enum DbusError {
    #[dbus_error(zbus_error)]
    ZBus(zbus::Error),
    #[dbus_error(name = "resolve1.NoNameServers")]
    NoNameServers(String),
    #[dbus_error(name = "resolve1.InvalidReply")]
    InvalidReply(String),
    #[dbus_error(name = "resolve1.CNameLoop")]
    CNameLoop(String),
    #[dbus_error(name = "resolve1.Aborted")]
    Aborted(String),
    #[dbus_error(name = "resolve1.NoSuchRR")]
    NoSuchResourceRecord(String),
    #[dbus_error(name = "resolve1.NoSuchService")]
    NoSuchService(String),
    #[dbus_error(name = "resolve1.InconsistentServiceRecords")]
    InconsistentServiceRecords(String),
    #[dbus_error(name = "resolve1.DnssecFailed")]
    DnssecFailed(String),
    #[dbus_error(name = "resolve1.NoTrustAnchor")]
    NoTrustAnchor(String),
    #[dbus_error(name = "resolve1.NoSuchDnssdService")]
    NoSuchDnssdService(String),
    #[dbus_error(name = "resolve1.NoSuchDelegate")]
    NoSuchDelegate(String),
    #[dbus_error(name = "resolve1.DnssdServiceExists")]
    DnssdServiceExists(String),
    #[dbus_error(name = "resolve1.ResourceRecordTypeUnsupported")]
    ResourceRecordTypeUnsupported(String),
    #[dbus_error(name = "resolve1.NoSuchLink")]
    NoSuchLink(String),
    #[dbus_error(name = "resolve1.LinkBusy")]
    LinkBusy(String),
    #[dbus_error(name = "resolve1.NetworkDown")]
    NetworkDown(String),
    #[dbus_error(name = "resolve1.NoSource")]
    NoSource(String),
    #[dbus_error(name = "resolve1.StubLoop")]
    StubLoop(String),
    #[dbus_error(name = "DBus.Error.InvalidArgs")]
    InvalidArgs(String),
    #[dbus_error(name = "DBus.Error.NotSupported")]
    NotSupported(String),
    #[dbus_error(name = "DBus.Error.Timeout")]
    Timeout(String),
    #[dbus_error(name = "DBus.Error.AccessDenied")]
    AccessDenied(String),
    #[dbus_error(name = "DBus.Error.InteractiveAuthorizationRequired")]
    InteractiveAuthorizationRequired(String),
    #[dbus_error(name = "resolve1.DnsError.FORMERR")]
    DnsFormErr(String),
    #[dbus_error(name = "resolve1.DnsError.SERVFAIL")]
    DnsServFail(String),
    #[dbus_error(name = "resolve1.DnsError.NXDOMAIN")]
    DnsNxDomain(String),
    #[dbus_error(name = "resolve1.DnsError.NOTIMP")]
    DnsNotImp(String),
    #[dbus_error(name = "resolve1.DnsError.REFUSED")]
    DnsRefused(String),
    #[dbus_error(name = "resolve1.DnsError.YXDOMAIN")]
    DnsYxDomain(String),
    #[dbus_error(name = "resolve1.DnsError.YRRSET")]
    DnsYrrset(String),
    #[dbus_error(name = "resolve1.DnsError.NXRRSET")]
    DnsNxrrset(String),
    #[dbus_error(name = "resolve1.DnsError.NOTAUTH")]
    DnsNotAuth(String),
    #[dbus_error(name = "resolve1.DnsError.NOTZONE")]
    DnsNotZone(String),
    #[dbus_error(name = "resolve1.DnsError.BADVERS")]
    DnsBadVers(String),
    #[dbus_error(name = "resolve1.DnsError.BADKEY")]
    DnsBadKey(String),
    #[dbus_error(name = "resolve1.DnsError.BADTIME")]
    DnsBadTime(String),
    #[dbus_error(name = "resolve1.DnsError.BADMODE")]
    DnsBadMode(String),
    #[dbus_error(name = "resolve1.DnsError.BADNAME")]
    DnsBadName(String),
    #[dbus_error(name = "resolve1.DnsError.BADALG")]
    DnsBadAlg(String),
    #[dbus_error(name = "resolve1.DnsError.BADTRUNC")]
    DnsBadTrunc(String),
    #[dbus_error(name = "resolve1.DnsError.BADCOOKIE")]
    DnsBadCookie(String),
}

impl From<DbusError> for zbus::fdo::Error {
    fn from(error: DbusError) -> Self {
        Self::Failed(error.to_string())
    }
}

#[derive(Debug)]
struct ManagerObject {
    resolver: Arc<Resolver>,
    authorization: Arc<DbusAuthorization>,
    client_queries: ClientQueryRegistry,
}

#[derive(Debug)]
struct RegisteredClientQuery {
    owner: String,
    cancellation: crate::query_cancel::QueryCancellation,
    registry: ClientQueryRegistry,
}

impl RegisteredClientQuery {
    fn new(header: &MessageHeader<'_>, registry: &ClientQueryRegistry) -> Result<Self, DbusError> {
        let owner = header
            .sender()
            .map_err(|error| DbusError::InvalidArgs(error.to_string()))?
            .ok_or_else(|| DbusError::InvalidArgs("D-Bus query has no sender".to_owned()))?
            .as_str()
            .to_owned();
        let cancellation = crate::query_cancel::QueryCancellation::default();
        registry
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .entry(owner.clone())
            .or_default()
            .push(cancellation.clone());
        Ok(Self {
            owner,
            cancellation,
            registry: Arc::clone(registry),
        })
    }
}

impl Drop for RegisteredClientQuery {
    fn drop(&mut self) {
        let mut registry = self
            .registry
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        let remove_owner = if let Some(queries) = registry.get_mut(&self.owner) {
            queries.retain(|query| !query.same_as(&self.cancellation));
            queries.is_empty()
        } else {
            false
        };
        if remove_owner {
            registry.remove(&self.owner);
        }
    }
}

fn cancel_vanished_client_queries(
    bus: &zbus::blocking::fdo::DBusProxy<'_>,
    registry: &ClientQueryRegistry,
) -> Result<(), Box<dyn Error + Send + Sync>> {
    let owners = registry
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .keys()
        .cloned()
        .collect::<Vec<_>>();
    for owner in owners {
        let name = zbus::names::BusName::try_from(owner.as_str())?;
        if !bus.name_has_owner(name)? {
            cancel_client_queries(registry, &owner);
        }
    }
    Ok(())
}

fn cancel_client_queries(registry: &ClientQueryRegistry, owner: &str) {
    let queries = registry
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .remove(owner)
        .unwrap_or_default();
    for query in queries {
        query.cancel();
    }
}