simple-mdns 0.6.3

Rust implementation of mDNS for service discovering (DNS-SD)
Documentation
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use std::collections::HashSet;

use resource_record_manager::DomainResourceFilter;
use simple_dns::{rdata::RData, Packet, TYPE};

pub mod conversion_utils;

mod instance_information;
pub use instance_information::InstanceInformation;

mod network_scope;
pub use network_scope::NetworkScope;

mod resource_record_manager;

mod simple_mdns_error;
pub use simple_mdns_error::SimpleMdnsError;

mod socket_helper;

#[cfg(feature = "async-tokio")]
pub mod async_discovery;

#[cfg(feature = "sync")]
pub mod sync_discovery;

#[allow(unused)]
const UNICAST_RESPONSE: bool = cfg!(not(test));

#[allow(unused)]
pub(crate) fn build_reply<'b>(
    packet: simple_dns::Packet,
    resources: &'b resource_record_manager::ResourceRecordManager<'b>,
) -> Option<(Packet<'b>, bool)> {
    let mut reply_packet = Packet::new_reply(packet.id());

    let mut unicast_response = false;
    let mut additional_records = HashSet::new();

    // TODO: add dns-sd metaquery (https://datatracker.ietf.org/doc/html/rfc6763#autoid-25)

    for question in packet.questions.iter() {
        if question.unicast_response {
            unicast_response = question.unicast_response
        }

        // FIXME: send negative response for IPv4 or IPv6 if necessary
        for d_resources in resources
            .get_domain_resources(&question.qname, DomainResourceFilter::authoritative(true))
        {
            for answer in d_resources
                .filter(|r| r.match_qclass(question.qclass) && r.match_qtype(question.qtype))
            {
                reply_packet.answers.push(answer.clone());

                if let RData::SRV(srv) = &answer.rdata {
                    let target = resources
                        .get_domain_resources(
                            &srv.target,
                            DomainResourceFilter::authoritative(false),
                        )
                        .flatten()
                        .filter(|r| {
                            (r.match_qtype(TYPE::A.into()) || r.match_qtype(TYPE::AAAA.into()))
                                && r.match_qclass(question.qclass)
                        })
                        .cloned();

                    additional_records.extend(target);
                }
            }
        }
    }

    for additional_record in additional_records {
        reply_packet.additional_records.push(additional_record);
    }

    if !reply_packet.answers.is_empty() {
        Some((reply_packet, unicast_response))
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use simple_dns::Name;
    use std::{
        convert::TryInto,
        net::{Ipv4Addr, Ipv6Addr},
    };

    use simple_dns::Question;

    use crate::{
        build_reply,
        conversion_utils::{ip_addr_to_resource_record, port_to_srv_record},
        resource_record_manager::ResourceRecordManager,
    };

    use super::*;

    fn get_resources() -> ResourceRecordManager<'static> {
        let mut resources = ResourceRecordManager::new();
        resources.add_authoritative_resource(port_to_srv_record(
            &Name::new_unchecked("_res1._tcp.com"),
            8080,
            0,
        ));
        resources.add_authoritative_resource(ip_addr_to_resource_record(
            &Name::new_unchecked("_res1._tcp.com"),
            Ipv4Addr::LOCALHOST.into(),
            0,
        ));
        resources.add_authoritative_resource(ip_addr_to_resource_record(
            &Name::new_unchecked("_res1._tcp.com"),
            Ipv6Addr::LOCALHOST.into(),
            0,
        ));

        resources.add_authoritative_resource(port_to_srv_record(
            &Name::new_unchecked("_res2._tcp.com"),
            8080,
            0,
        ));
        resources.add_authoritative_resource(ip_addr_to_resource_record(
            &Name::new_unchecked("_res2._tcp.com"),
            Ipv4Addr::LOCALHOST.into(),
            0,
        ));
        resources
    }

    #[test]
    fn test_build_reply_with_no_questions() {
        let resources = get_resources();

        let packet = Packet::new_query(1);
        assert!(build_reply(packet, &resources).is_none());
    }

    #[test]
    fn test_build_reply_without_valid_answers() {
        let resources = get_resources();

        let mut packet = Packet::new_query(1);
        packet.questions.push(Question::new(
            "_res3._tcp.com".try_into().unwrap(),
            simple_dns::QTYPE::ANY,
            simple_dns::QCLASS::ANY,
            false,
        ));

        assert!(build_reply(packet, &resources).is_none());
    }

    #[test]
    fn test_build_reply_with_valid_answer() {
        let resources = get_resources();

        let mut packet = Packet::new_query(1);
        packet.questions.push(Question::new(
            "_res1._tcp.com".try_into().unwrap(),
            simple_dns::TYPE::A.into(),
            simple_dns::QCLASS::ANY,
            true,
        ));

        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();

        assert!(unicast_response);
        assert_eq!(1, reply.answers.len());
        assert_eq!(0, reply.additional_records.len());
    }

    #[test]
    fn test_build_reply_for_srv() {
        let resources = get_resources();

        let mut packet = Packet::new_query(1);
        packet.questions.push(Question::new(
            "_res1._tcp.com".try_into().unwrap(),
            simple_dns::TYPE::SRV.into(),
            simple_dns::QCLASS::ANY,
            false,
        ));

        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();

        assert!(!unicast_response);
        assert_eq!(1, reply.answers.len());
        assert_eq!(2, reply.additional_records.len());
    }
}