Skip to main content

simple_mdns/
lib.rs

1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3use std::collections::HashSet;
4
5use resource_record_manager::DomainResourceFilter;
6use simple_dns::{rdata::RData, Packet, TYPE};
7
8pub mod conversion_utils;
9
10mod instance_information;
11pub use instance_information::InstanceInformation;
12
13mod network_scope;
14pub use network_scope::NetworkScope;
15
16mod resource_record_manager;
17
18mod simple_mdns_error;
19pub use simple_mdns_error::SimpleMdnsError;
20
21mod socket_helper;
22
23#[cfg(feature = "async-tokio")]
24pub mod async_discovery;
25
26#[cfg(feature = "sync")]
27pub mod sync_discovery;
28
29#[allow(unused)]
30const UNICAST_RESPONSE: bool = cfg!(not(test));
31
32#[allow(unused)]
33pub(crate) fn build_reply<'b>(
34    packet: simple_dns::Packet,
35    resources: &'b resource_record_manager::ResourceRecordManager<'b>,
36) -> Option<(Packet<'b>, bool)> {
37    let mut reply_packet = Packet::new_reply(packet.id());
38
39    let mut unicast_response = false;
40    let mut additional_records = HashSet::new();
41
42    // TODO: add dns-sd metaquery (https://datatracker.ietf.org/doc/html/rfc6763#autoid-25)
43
44    for question in packet.questions.iter() {
45        if question.unicast_response {
46            unicast_response = question.unicast_response
47        }
48
49        // FIXME: send negative response for IPv4 or IPv6 if necessary
50        for d_resources in resources
51            .get_domain_resources(&question.qname, DomainResourceFilter::authoritative(true))
52        {
53            for answer in d_resources
54                .filter(|r| r.match_qclass(question.qclass) && r.match_qtype(question.qtype))
55            {
56                reply_packet.answers.push(answer.clone());
57
58                if let RData::SRV(srv) = &answer.rdata {
59                    let target = resources
60                        .get_domain_resources(
61                            &srv.target,
62                            DomainResourceFilter::authoritative(false),
63                        )
64                        .flatten()
65                        .filter(|r| {
66                            (r.match_qtype(TYPE::A.into()) || r.match_qtype(TYPE::AAAA.into()))
67                                && r.match_qclass(question.qclass)
68                        })
69                        .cloned();
70
71                    additional_records.extend(target);
72                }
73            }
74        }
75    }
76
77    for additional_record in additional_records {
78        reply_packet.additional_records.push(additional_record);
79    }
80
81    if !reply_packet.answers.is_empty() {
82        Some((reply_packet, unicast_response))
83    } else {
84        None
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use simple_dns::Name;
91    use std::{
92        convert::TryInto,
93        net::{Ipv4Addr, Ipv6Addr},
94    };
95
96    use simple_dns::Question;
97
98    use crate::{
99        build_reply,
100        conversion_utils::{ip_addr_to_resource_record, port_to_srv_record},
101        resource_record_manager::ResourceRecordManager,
102    };
103
104    use super::*;
105
106    fn get_resources() -> ResourceRecordManager<'static> {
107        let mut resources = ResourceRecordManager::new();
108        resources.add_authoritative_resource(port_to_srv_record(
109            &Name::new_unchecked("_res1._tcp.com"),
110            8080,
111            0,
112        ));
113        resources.add_authoritative_resource(ip_addr_to_resource_record(
114            &Name::new_unchecked("_res1._tcp.com"),
115            Ipv4Addr::LOCALHOST.into(),
116            0,
117        ));
118        resources.add_authoritative_resource(ip_addr_to_resource_record(
119            &Name::new_unchecked("_res1._tcp.com"),
120            Ipv6Addr::LOCALHOST.into(),
121            0,
122        ));
123
124        resources.add_authoritative_resource(port_to_srv_record(
125            &Name::new_unchecked("_res2._tcp.com"),
126            8080,
127            0,
128        ));
129        resources.add_authoritative_resource(ip_addr_to_resource_record(
130            &Name::new_unchecked("_res2._tcp.com"),
131            Ipv4Addr::LOCALHOST.into(),
132            0,
133        ));
134        resources
135    }
136
137    #[test]
138    fn test_build_reply_with_no_questions() {
139        let resources = get_resources();
140
141        let packet = Packet::new_query(1);
142        assert!(build_reply(packet, &resources).is_none());
143    }
144
145    #[test]
146    fn test_build_reply_without_valid_answers() {
147        let resources = get_resources();
148
149        let mut packet = Packet::new_query(1);
150        packet.questions.push(Question::new(
151            "_res3._tcp.com".try_into().unwrap(),
152            simple_dns::QTYPE::ANY,
153            simple_dns::QCLASS::ANY,
154            false,
155        ));
156
157        assert!(build_reply(packet, &resources).is_none());
158    }
159
160    #[test]
161    fn test_build_reply_with_valid_answer() {
162        let resources = get_resources();
163
164        let mut packet = Packet::new_query(1);
165        packet.questions.push(Question::new(
166            "_res1._tcp.com".try_into().unwrap(),
167            simple_dns::TYPE::A.into(),
168            simple_dns::QCLASS::ANY,
169            true,
170        ));
171
172        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();
173
174        assert!(unicast_response);
175        assert_eq!(1, reply.answers.len());
176        assert_eq!(0, reply.additional_records.len());
177    }
178
179    #[test]
180    fn test_build_reply_for_srv() {
181        let resources = get_resources();
182
183        let mut packet = Packet::new_query(1);
184        packet.questions.push(Question::new(
185            "_res1._tcp.com".try_into().unwrap(),
186            simple_dns::TYPE::SRV.into(),
187            simple_dns::QCLASS::ANY,
188            false,
189        ));
190
191        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();
192
193        assert!(!unicast_response);
194        assert_eq!(1, reply.answers.len());
195        assert_eq!(2, reply.additional_records.len());
196    }
197}