mhost 0.3.0

More than host - A modern take on the classic host DNS lookup utility including an easy to use and very fast Rust lookup library
Documentation
// Copyright 2017-2021 Lukas Pustina <lukas@pustina.de>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use std::iter::FromIterator;
use std::net::IpAddr;

use anyhow::{anyhow, Result};
use indexmap::set::IndexSet;
use tracing::info;

use crate::app::modules::check::config::CheckConfig;
use crate::app::modules::check::lints::cnames::Cnames;
use crate::app::modules::check::lints::{CheckResult, CheckResults};
use crate::app::modules::{Environment, PartialResult};
use crate::app::resolver::AppResolver;
use crate::app::utils::time;
use crate::diff::SetDiffer;
use crate::nameserver::NameServerConfig;
use crate::resolver::lookup::Uniquify;
use crate::resolver::{Lookups, MultiQuery, ResolverConfig};
use crate::resources::rdata::SOA;
use crate::{Name, RecordType};

pub struct Soa<'a> {
    pub env: Environment<'a, CheckConfig>,
    pub domain_name: Name,
    pub app_resolver: AppResolver,
    pub check_results: CheckResults,
}

impl<'a> Soa<'a> {
    pub async fn soa(self) -> PartialResult<Cnames<'a>> {
        let result = if self.env.mod_config.soa {
            let results = self.do_soa().await;
            Some(results)
        } else {
            None
        };

        Ok(Cnames {
            env: self.env,
            domain_name: self.domain_name,
            app_resolver: self.app_resolver,
            check_results: self.check_results.soa(result),
        })
    }

    async fn do_soa(&self) -> Vec<CheckResult> {
        if self.env.console.show_partial_headers() {
            self.env.console.caption("Checking SOA lints");
        }
        let mut results = Vec::new();

        let authoritative_soa_records = self.check_authoritative_servers(&mut results).await;
        if let Ok(authoritative_soa_records) = authoritative_soa_records {
            self.check_authoritative_records(authoritative_soa_records, &mut results);
        }

        if self.env.console.show_partial_results() {
            for r in &results {
                match r {
                    CheckResult::NotFound() => self.env.console.info("No SOA records found."),
                    CheckResult::Ok(str) => self.env.console.ok(str),
                    CheckResult::Warning(str) => self.env.console.attention(str),
                    CheckResult::Failed(str) => self.env.console.failed(str),
                }
            }
        }

        results
    }

    async fn check_authoritative_servers(&self, results: &mut Vec<CheckResult>) -> Result<Vec<SOA>> {
        if self.env.console.show_partial_headers() {
            self.env.console.itemize("Authoritative SOA records");
        }

        let auth_servers = self.lookup_authoritative_name_servers().await?;
        let name_server_ips = self.lookup_name_server_ips(auth_servers).await?;
        let soas = self.lookup_soa_records(name_server_ips).await;
        if let Err(err) = soas {
            results.push(CheckResult::NotFound());
            return Err(err);
        }

        let records: IndexSet<_> = soas?.soa().unique().to_owned().into_iter().collect();
        let diffs = records.differences();

        let check = match diffs.map(|x| x.len()) {
            Some(diffs) => CheckResult::Failed(format!("Found {} differences in authoritative SOA records", diffs)),
            None => CheckResult::Ok("All authoritative SOA records are in sync".to_string()),
        };
        results.push(check);

        Ok(Vec::from_iter(records))
    }

    async fn lookup_authoritative_name_servers(&self) -> Result<Vec<Name>> {
        let domain_name = self.env.name_builder.from_str(&self.env.mod_config.domain_name)?;
        let query = MultiQuery::single(domain_name, RecordType::NS)?;

        let lookups = intermediate_lookups!(self, query, "Running lookups for authoritative name servers.");
        if !lookups.has_records() {
            return Err(anyhow!("No authoritative name server records found."));
        }

        let auth_severs = lookups.ns().unique().to_owned();

        Ok(Vec::from_iter(auth_severs))
    }

    async fn lookup_name_server_ips(&self, name_servers: Vec<Name>) -> Result<Vec<IpAddr>> {
        let query = MultiQuery::new(name_servers, vec![RecordType::A, RecordType::AAAA])?;

        let lookups = intermediate_lookups!(
            self,
            query,
            "Running lookups for IP addresses of authoritative name servers."
        );
        if !lookups.has_records() {
            return Err(anyhow!("No IP addresses for authoritative nameservers found."));
        }

        let name_server_ips = lookups
            .a()
            .unique()
            .to_owned()
            .into_iter()
            .map(IpAddr::from)
            .chain(lookups.aaaa().unique().to_owned().into_iter().map(IpAddr::from))
            .collect();

        Ok(name_server_ips)
    }

    async fn lookup_soa_records(&self, name_server_ips: Vec<IpAddr>) -> Result<Lookups> {
        let authoritative_name_servers = name_server_ips
            .into_iter()
            .map(|ip| NameServerConfig::udp((ip, 53)))
            .map(ResolverConfig::new);
        let resolvers = AppResolver::from_configs(authoritative_name_servers, &self.env.app_config).await?;
        let query = MultiQuery::single(self.env.mod_config.domain_name.as_str(), RecordType::SOA)?;

        let lookups = intermediate_lookups!(
            self,
            query,
            resolver: resolvers,
            "Running lookups for SOA records from authoritative name servers."
        );
        if !lookups.has_records() {
            return Err(anyhow!("No SOA records from authoritative nameservers found."));
        }

        Ok(lookups)
    }

    fn check_authoritative_records(&self, mut authoritative_soa_records: Vec<SOA>, results: &mut Vec<CheckResult>) {
        if self.env.console.show_partial_headers() {
            self.env
                .console
                .itemize("Comparing authoritative with looked up SOA records");
        }

        let mut lookuped_soa_records = self.check_results.lookups.soa().into_iter().cloned().collect();
        authoritative_soa_records.append(&mut lookuped_soa_records);

        let records: IndexSet<_> = authoritative_soa_records.unique().to_owned().into_iter().collect();
        let diffs = records.differences();

        let check = match diffs.map(|x| x.len()) {
            Some(_) => CheckResult::Failed("Looked up SOA records differ from authoritative SOA records".to_string()),
            None => CheckResult::Ok("Looked up SOA records match authoritative SOA records".to_string()),
        };
        results.push(check);
    }
}