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 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 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 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 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}