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_query: HashSet<(&simple_dns::Name<'_>, TYPE)> = HashSet::new();
41
42    for question in packet.questions.iter() {
43        if question.unicast_response {
44            unicast_response = question.unicast_response
45        }
46
47        // FIXME: send negative response for IPv4 or IPv6 if necessary
48        for answer in resources
49            .get_domain_resources(&question.qname, DomainResourceFilter::authoritative(true))
50            .flatten()
51            .filter(|r| r.match_qclass(question.qclass) && r.match_qtype(question.qtype))
52        {
53            reply_packet.answers.push(answer.clone());
54
55            if let RData::SRV(srv) = &answer.rdata {
56                additional_query.insert((&srv.target, TYPE::A));
57                additional_query.insert((&srv.target, TYPE::AAAA));
58            }
59
60            if let RData::PTR(ptr) = &answer.rdata {
61                additional_query.insert((ptr, TYPE::A));
62                additional_query.insert((ptr, TYPE::AAAA));
63                additional_query.insert((ptr, TYPE::TXT));
64                additional_query.insert((ptr, TYPE::SRV));
65            }
66        }
67    }
68
69    for (domain, _type) in additional_query {
70        let addr_records = resources
71            .get_domain_resources(domain, DomainResourceFilter::authoritative(true))
72            .flatten()
73            .filter(|r| r.match_qtype(_type.into()))
74            .cloned();
75
76        reply_packet.additional_records.extend(addr_records);
77    }
78
79    if !reply_packet.answers.is_empty() {
80        Some((reply_packet, unicast_response))
81    } else {
82        None
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use simple_dns::Name;
89    use std::{
90        convert::TryInto,
91        net::{Ipv4Addr, Ipv6Addr},
92    };
93
94    use simple_dns::Question;
95
96    use simple_dns::ResourceRecord;
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_ptr_includes_additional_records() {
181        // RFC 6763 ยง12.1: PTR response must include SRV, TXT, and A/AAAA as additional records
182        let instance_name = Name::new_unchecked("myinst._res3._tcp.com");
183        let service_name = Name::new_unchecked("_res3._tcp.com");
184
185        let mut resources = ResourceRecordManager::new();
186        resources.add_authoritative_resource(ResourceRecord::new(
187            service_name.clone(),
188            simple_dns::CLASS::IN,
189            0,
190            simple_dns::rdata::RData::PTR(instance_name.clone().into()),
191        ));
192        resources.add_authoritative_resource(port_to_srv_record(&instance_name, 9090, 0));
193        resources.add_authoritative_resource(ip_addr_to_resource_record(
194            &instance_name,
195            Ipv4Addr::LOCALHOST.into(),
196            0,
197        ));
198        resources.add_authoritative_resource(
199            crate::conversion_utils::hashmap_to_txt(&instance_name, Default::default(), 0).unwrap(),
200        );
201
202        let mut packet = Packet::new_query(1);
203        packet.questions.push(Question::new(
204            service_name,
205            simple_dns::TYPE::PTR.into(),
206            simple_dns::QCLASS::ANY,
207            false,
208        ));
209
210        let (reply, _) = build_reply(packet, &resources).unwrap();
211
212        assert_eq!(1, reply.answers.len(), "PTR should be the only answer");
213        // additional records: SRV + TXT + A  = 3
214        assert_eq!(
215            3,
216            reply.additional_records.len(),
217            "SRV, TXT, and A should be additional records"
218        );
219    }
220
221    #[test]
222    fn test_build_reply_for_srv() {
223        let resources = get_resources();
224
225        let mut packet = Packet::new_query(1);
226        packet.questions.push(Question::new(
227            "_res1._tcp.com".try_into().unwrap(),
228            simple_dns::TYPE::SRV.into(),
229            simple_dns::QCLASS::ANY,
230            false,
231        ));
232
233        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();
234
235        assert!(!unicast_response);
236        assert_eq!(1, reply.answers.len());
237        assert_eq!(2, reply.additional_records.len());
238    }
239
240    #[test]
241    fn test_build_reply_for_meta_query() {
242        let service_name = Name::new_unchecked("_res1._tcp.com");
243        let meta_name = Name::new_unchecked("_services._dns-sd._udp.local");
244
245        let info = InstanceInformation::new("my-instance".into())
246            .with_socket_address("127.0.0.1:8080".parse().unwrap());
247
248        let instance_full_name = format!("{}.{service_name}", info.escaped_instance_name());
249        let instance_full_name = Name::new_unchecked(&instance_full_name);
250
251        let resources = resource_record_manager::service_discovery_resource_manager(
252            &service_name,
253            &instance_full_name,
254            300,
255            info,
256        )
257        .expect("failed to create resource manager");
258
259        let mut packet = Packet::new_query(1);
260        packet.questions.push(Question::new(
261            meta_name,
262            simple_dns::TYPE::PTR.into(),
263            simple_dns::QCLASS::ANY,
264            false,
265        ));
266
267        let (reply, _) = dbg!(build_reply(packet, &resources).unwrap());
268
269        assert_eq!(
270            1,
271            reply.answers.len(),
272            "meta-query PTR should be the only answer"
273        );
274        // additional records: SRV + A + AAAA for _res1._tcp.com = 3
275        assert_eq!(
276            3,
277            reply.additional_records.len(),
278            "SRV, A, and AAAA for the service type should be additional records"
279        );
280    }
281}