dyns 0.7.2

DNS discovery and resolver support for DHTTP applications
Documentation
use std::collections::HashMap;

use dhttp_identity::identity::{LocalAuthority, LocalAuthorityCertificateExt};
use dquic::qbase::net::addr::EndpointAddr;
use snafu::{ResultExt, Snafu, ensure};

use crate::core::{MdnsPacket, parser::record::endpoint::EndpointAddr as DnsEndpointAddr};

#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum EncodeAuthorityDnsPacketError {
    #[snafu(display("publisher authority name does not match dns name"))]
    AuthorityNameMismatch,
    #[snafu(display("publisher authority has no valid dhttp subject key identifier"))]
    DhttpSubjectKeyIdentifier {
        source: dhttp_identity::identity::ExtractDhttpSubjectKeyIdentifierError,
    },
    #[snafu(display("failed to encode endpoint address"))]
    EncodeEndpoint,
    #[snafu(display("secondary authority cannot publish dns endpoints"))]
    SecondaryAuthority,
}

pub(crate) fn dns_endpoints_for_authority(
    authority: &dyn LocalAuthority,
    name: &str,
    endpoints: &mut dyn Iterator<Item = EndpointAddr>,
) -> Result<Vec<DnsEndpointAddr>, EncodeAuthorityDnsPacketError> {
    ensure!(
        authority.name() == name,
        encode_authority_dns_packet_error::AuthorityNameMismatchSnafu
    );

    let ski = authority
        .dhttp_subject_key_identifier()
        .context(encode_authority_dns_packet_error::DhttpSubjectKeyIdentifierSnafu)?;
    let chain = ski.chain();

    let mut encoded = Vec::new();
    for endpoint in endpoints {
        let Ok(mut endpoint) = DnsEndpointAddr::try_from(endpoint) else {
            return encode_authority_dns_packet_error::EncodeEndpointSnafu.fail();
        };
        endpoint.set_main(true);
        endpoint.set_sequence(chain.sequence());
        encoded.push(endpoint);
    }

    ensure!(
        encoded.is_empty() || crate::core::certificate::is_primary_chain_key(chain),
        encode_authority_dns_packet_error::SecondaryAuthoritySnafu
    );

    Ok(encoded)
}

pub(crate) fn dns_packet_for_authority(
    authority: &dyn LocalAuthority,
    name: &str,
    endpoints: &mut dyn Iterator<Item = EndpointAddr>,
) -> Result<Vec<u8>, EncodeAuthorityDnsPacketError> {
    let encoded = dns_endpoints_for_authority(authority, name, endpoints)?;

    let mut hosts = HashMap::new();
    hosts.insert(name.to_owned(), encoded);
    Ok(MdnsPacket::answer(0, &hosts).to_bytes())
}

#[cfg(test)]
mod tests {
    use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};

    use dhttp_identity::identity::LocalAuthority;
    use dquic::qbase::net::addr::EndpointAddr as DquicEndpointAddr;
    use futures::future::BoxFuture;
    use rustls::pki_types::CertificateDer;

    use super::{dns_endpoints_for_authority, dns_packet_for_authority};
    use crate::core::parser::{
        packet::be_packet,
        record::{RData, Type},
    };

    #[derive(Debug)]
    struct TestAuthority {
        name: &'static str,
        cert_chain: Vec<CertificateDer<'static>>,
    }

    impl LocalAuthority for TestAuthority {
        fn name(&self) -> &str {
            self.name
        }

        fn cert_chain(&self) -> &[CertificateDer<'static>] {
            &self.cert_chain
        }

        fn sign(
            &self,
            _data: &[u8],
        ) -> BoxFuture<'_, Result<Vec<u8>, dhttp_identity::identity::SignError>> {
            Box::pin(std::future::ready(Ok(Vec::new())))
        }
    }

    fn authority(name: &'static str) -> TestAuthority {
        TestAuthority {
            name,
            cert_chain: vec![CertificateDer::from(
                include_bytes!("../../tests/fixtures/valid.der").to_vec(),
            )],
        }
    }

    fn authority_with_chain(name: &'static str, sequence: u8, kind_flag: u8) -> TestAuthority {
        assert!(kind_flag <= 1, "certificate kind flag must be 0 or 1");
        let mut certificate = include_bytes!("../../tests/fixtures/valid.der").to_vec();
        let marker = b"0:0:0123456789abcdef";
        let offset = certificate
            .windows(marker.len())
            .position(|window| window == marker)
            .expect("fixture contains dhttp subject key identifier");
        certificate[offset] = b'0' + sequence;
        certificate[offset + 2] = b'0' + kind_flag;
        TestAuthority {
            name,
            cert_chain: vec![CertificateDer::from(certificate)],
        }
    }

    fn endpoint() -> DquicEndpointAddr {
        DquicEndpointAddr::direct(SocketAddr::V4(SocketAddrV4::new(
            Ipv4Addr::new(203, 0, 113, 10),
            4433,
        )))
    }

    #[test]
    fn dns_packet_for_authority_encodes_chain_key_from_authority_ski() {
        let authority = authority("client.example.com.dhttp.net");
        let mut endpoints = std::iter::once(endpoint());

        let packet =
            dns_packet_for_authority(&authority, "client.example.com.dhttp.net", &mut endpoints)
                .expect("authority packet");

        let (remain, parsed) = be_packet(&packet).expect("dns packet parses");
        assert!(remain.is_empty());
        assert_eq!(parsed.answers.len(), 1);
        assert_eq!(parsed.answers[0].name(), "client.example.com.dhttp.net");
        assert_eq!(parsed.answers[0].typ(), Type::E);

        let RData::E(encoded) = parsed.answers[0].data() else {
            panic!("answer must be an E record");
        };
        assert!(!encoded.is_signed());
        assert!(encoded.is_main());
        assert_eq!(encoded.normalized_sequence().get(), 0);
    }

    #[test]
    fn dns_packet_for_authority_rejects_name_mismatch() {
        let authority = authority("client.example.com.dhttp.net");
        let mut endpoints = std::iter::once(endpoint());

        let error =
            dns_packet_for_authority(&authority, "other.example.com.dhttp.net", &mut endpoints)
                .expect_err("name mismatch fails");

        assert_eq!(
            error.to_string(),
            "publisher authority name does not match dns name"
        );
    }

    #[test]
    fn dns_packet_for_authority_allows_empty_endpoint_set() {
        let authority = authority("client.example.com.dhttp.net");
        let mut endpoints = std::iter::empty();

        let packet =
            dns_packet_for_authority(&authority, "client.example.com.dhttp.net", &mut endpoints)
                .expect("authority packet");

        let (remain, parsed) = be_packet(&packet).expect("dns packet parses");
        assert!(remain.is_empty());
        assert!(parsed.answers.is_empty());
    }

    #[test]
    fn dns_endpoints_for_primary_authority_stamp_sequence_metadata() {
        let authority = authority_with_chain("client.example.com.dhttp.net", 7, 0);
        let mut endpoints = std::iter::once(endpoint());

        let encoded =
            dns_endpoints_for_authority(&authority, "client.example.com.dhttp.net", &mut endpoints)
                .expect("primary endpoints encode");

        assert_eq!(encoded.len(), 1);
        assert!(encoded[0].is_main());
        assert_eq!(encoded[0].normalized_sequence().get(), 7);
    }

    #[test]
    fn dns_endpoints_for_secondary_authority_reject_non_empty_publication() {
        let authority = authority_with_chain("client.example.com.dhttp.net", 7, 1);
        let mut endpoints = std::iter::once(endpoint());

        let error =
            dns_endpoints_for_authority(&authority, "client.example.com.dhttp.net", &mut endpoints)
                .expect_err("secondary publication is rejected");

        assert_eq!(
            error.to_string(),
            "secondary authority cannot publish dns endpoints"
        );
    }

    #[test]
    fn dns_endpoints_for_secondary_authority_allows_empty_clear() {
        let authority = authority_with_chain("client.example.com.dhttp.net", 7, 1);
        let mut endpoints = std::iter::empty();

        let encoded =
            dns_endpoints_for_authority(&authority, "client.example.com.dhttp.net", &mut endpoints)
                .expect("empty secondary clear is allowed");

        assert!(encoded.is_empty());
    }
}