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
29const UNICAST_RESPONSE: bool = cfg!(not(test));
30
31pub(crate) fn build_reply<'b>(
32    packet: simple_dns::Packet,
33    resources: &'b resource_record_manager::ResourceRecordManager<'b>,
34) -> Option<(Packet<'b>, bool)> {
35    let mut reply_packet = Packet::new_reply(packet.id());
36
37    let mut unicast_response = false;
38    let mut additional_records = HashSet::new();
39
40    // TODO: add dns-sd metaquery (https://datatracker.ietf.org/doc/html/rfc6763#autoid-25)
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 d_resources in resources
49            .get_domain_resources(&question.qname, DomainResourceFilter::authoritative(true))
50        {
51            for answer in d_resources
52                .filter(|r| r.match_qclass(question.qclass) && r.match_qtype(question.qtype))
53            {
54                reply_packet.answers.push(answer.clone());
55
56                if let RData::SRV(srv) = &answer.rdata {
57                    let target = resources
58                        .get_domain_resources(
59                            &srv.target,
60                            DomainResourceFilter::authoritative(false),
61                        )
62                        .flatten()
63                        .filter(|r| {
64                            (r.match_qtype(TYPE::A.into()) || r.match_qtype(TYPE::AAAA.into()))
65                                && r.match_qclass(question.qclass)
66                        })
67                        .cloned();
68
69                    additional_records.extend(target);
70                }
71            }
72        }
73    }
74
75    for additional_record in additional_records {
76        reply_packet.additional_records.push(additional_record);
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 crate::{
97        build_reply,
98        conversion_utils::{ip_addr_to_resource_record, port_to_srv_record},
99        resource_record_manager::ResourceRecordManager,
100    };
101
102    use super::*;
103
104    fn get_resources() -> ResourceRecordManager<'static> {
105        let mut resources = ResourceRecordManager::new();
106        resources.add_authoritative_resource(port_to_srv_record(
107            &Name::new_unchecked("_res1._tcp.com"),
108            8080,
109            0,
110        ));
111        resources.add_authoritative_resource(ip_addr_to_resource_record(
112            &Name::new_unchecked("_res1._tcp.com"),
113            Ipv4Addr::LOCALHOST.into(),
114            0,
115        ));
116        resources.add_authoritative_resource(ip_addr_to_resource_record(
117            &Name::new_unchecked("_res1._tcp.com"),
118            Ipv6Addr::LOCALHOST.into(),
119            0,
120        ));
121
122        resources.add_authoritative_resource(port_to_srv_record(
123            &Name::new_unchecked("_res2._tcp.com"),
124            8080,
125            0,
126        ));
127        resources.add_authoritative_resource(ip_addr_to_resource_record(
128            &Name::new_unchecked("_res2._tcp.com"),
129            Ipv4Addr::LOCALHOST.into(),
130            0,
131        ));
132        resources
133    }
134
135    #[test]
136    fn test_build_reply_with_no_questions() {
137        let resources = get_resources();
138
139        let packet = Packet::new_query(1);
140        assert!(build_reply(packet, &resources).is_none());
141    }
142
143    #[test]
144    fn test_build_reply_without_valid_answers() {
145        let resources = get_resources();
146
147        let mut packet = Packet::new_query(1);
148        packet.questions.push(Question::new(
149            "_res3._tcp.com".try_into().unwrap(),
150            simple_dns::QTYPE::ANY,
151            simple_dns::QCLASS::ANY,
152            false,
153        ));
154
155        assert!(build_reply(packet, &resources).is_none());
156    }
157
158    #[test]
159    fn test_build_reply_with_valid_answer() {
160        let resources = get_resources();
161
162        let mut packet = Packet::new_query(1);
163        packet.questions.push(Question::new(
164            "_res1._tcp.com".try_into().unwrap(),
165            simple_dns::TYPE::A.into(),
166            simple_dns::QCLASS::ANY,
167            true,
168        ));
169
170        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();
171
172        assert!(unicast_response);
173        assert_eq!(1, reply.answers.len());
174        assert_eq!(0, reply.additional_records.len());
175    }
176
177    #[test]
178    fn test_build_reply_for_srv() {
179        let resources = get_resources();
180
181        let mut packet = Packet::new_query(1);
182        packet.questions.push(Question::new(
183            "_res1._tcp.com".try_into().unwrap(),
184            simple_dns::TYPE::SRV.into(),
185            simple_dns::QCLASS::ANY,
186            false,
187        ));
188
189        let (reply, unicast_response) = build_reply(packet, &resources).unwrap();
190
191        assert!(!unicast_response);
192        assert_eq!(1, reply.answers.len());
193        assert_eq!(2, reply.additional_records.len());
194    }
195}