hickory_proto/op/dns_response.rs
1// Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! `DnsResponse` wraps a `Message` and any associated connection details
9
10use alloc::vec::Vec;
11use core::{
12 convert::TryFrom,
13 ops::{Deref, DerefMut},
14};
15
16#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19use crate::{
20 error::ProtoError,
21 op::{Message, MessageType},
22 rr::{RData, RecordType, rdata::SOA, record::RecordRef},
23};
24
25// TODO: this needs to have the IP addr of the remote system...
26// TODO: see https://github.com/hickory-dns/hickory-dns/issues/383 for removing vec of messages and instead returning a Stream
27/// A DNS response object
28///
29/// For Most DNS requests, only one response is expected, the exception is a multicast request.
30#[derive(Clone, Debug)]
31#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
32pub struct DnsResponse {
33 message: Message,
34 buffer: Vec<u8>,
35}
36
37// TODO: when `impl Trait` lands in stable, remove this, and expose FlatMap over answers, et al.
38impl DnsResponse {
39 /// Constructs a new DnsResponse with a buffer synthesized from the message
40 pub fn from_message(message: Message) -> Result<Self, ProtoError> {
41 if message.metadata.message_type != MessageType::Response {
42 return Err(ProtoError::NotAResponse);
43 }
44
45 Ok(Self {
46 buffer: message.to_vec()?,
47 message,
48 })
49 }
50
51 /// Constructs a new DnsResponse by parsing a message from a buffer.
52 ///
53 /// Returns an error if the response message cannot be decoded.
54 pub fn from_buffer(buffer: Vec<u8>) -> Result<Self, ProtoError> {
55 let message = Message::from_vec(&buffer)?;
56 if message.metadata.message_type != MessageType::Response {
57 return Err(ProtoError::NotAResponse);
58 }
59
60 Ok(Self { message, buffer })
61 }
62
63 /// Retrieves the SOA from the response. This will only exist if it was an authoritative response.
64 pub fn soa(&self) -> Option<RecordRef<'_, SOA>> {
65 self.authorities
66 .iter()
67 .find_map(|record| RecordRef::try_from(record).ok())
68 }
69
70 /// Looks in the authority section for an SOA record from the response, and returns the negative_ttl, None if not available.
71 ///
72 /// ```text
73 /// [RFC 2308](https://tools.ietf.org/html/rfc2308#section-5) DNS NCACHE March 1998
74 ///
75 /// 5 - Caching Negative Answers
76 ///
77 /// Like normal answers negative answers have a time to live (TTL). As
78 /// there is no record in the answer section to which this TTL can be
79 /// applied, the TTL must be carried by another method. This is done by
80 /// including the SOA record from the zone in the authority section of
81 /// the reply. When the authoritative server creates this record its TTL
82 /// is taken from the minimum of the SOA.MINIMUM field and SOA's TTL.
83 /// This TTL decrements in a similar manner to a normal cached answer and
84 /// upon reaching zero (0) indicates the cached negative answer MUST NOT
85 /// be used again.
86 ///
87 /// A negative answer that resulted from a name error (NXDOMAIN) should
88 /// be cached such that it can be retrieved and returned in response to
89 /// another query for the same <QNAME, QCLASS> that resulted in the
90 /// cached negative response.
91 ///
92 /// A negative answer that resulted from a no data error (NODATA) should
93 /// be cached such that it can be retrieved and returned in response to
94 /// another query for the same <QNAME, QTYPE, QCLASS> that resulted in
95 /// the cached negative response.
96 ///
97 /// The NXT record, if it exists in the authority section of a negative
98 /// answer received, MUST be stored such that it can be be located and
99 /// returned with SOA record in the authority section, as should any SIG
100 /// records in the authority section. For NXDOMAIN answers there is no
101 /// "necessary" obvious relationship between the NXT records and the
102 /// QNAME. The NXT record MUST have the same owner name as the query
103 /// name for NODATA responses.
104 ///
105 /// Negative responses without SOA records SHOULD NOT be cached as there
106 /// is no way to prevent the negative responses looping forever between a
107 /// pair of servers even with a short TTL.
108 ///
109 /// Despite the DNS forming a tree of servers, with various mis-
110 /// configurations it is possible to form a loop in the query graph, e.g.
111 /// two servers listing each other as forwarders, various lame server
112 /// configurations. Without a TTL count down a cache negative response
113 /// when received by the next server would have its TTL reset. This
114 /// negative indication could then live forever circulating between the
115 /// servers involved.
116 ///
117 /// As with caching positive responses it is sensible for a resolver to
118 /// limit for how long it will cache a negative response as the protocol
119 /// supports caching for up to 68 years. Such a limit should not be
120 /// greater than that applied to positive answers and preferably be
121 /// tunable. Values of one to three hours have been found to work well
122 /// and would make sensible a default. Values exceeding one day have
123 /// been found to be problematic.
124 /// ```
125 pub fn negative_ttl(&self) -> Option<u32> {
126 // TODO: should this ensure that the SOA zone matches the Queried Zone?
127 self.authorities
128 .iter()
129 .filter_map(|record| match &record.data {
130 RData::SOA(soa) => Some((record.ttl, soa)),
131 _ => None,
132 })
133 .next()
134 .map(|(ttl, soa)| (ttl).min(soa.minimum))
135 }
136
137 /// Does the response contain any records matching the query name and type?
138 pub fn contains_answer(&self) -> bool {
139 for q in &self.queries {
140 let found = match q.query_type() {
141 RecordType::ANY => self.all_sections().any(|r| &r.name == q.name()),
142 RecordType::SOA => {
143 // for SOA name must be part of the SOA zone
144 self.all_sections()
145 .filter(|r| r.record_type().is_soa())
146 .any(|r| r.name.zone_of(q.name()))
147 }
148 q_type => {
149 if !self.answers.is_empty() {
150 true
151 } else {
152 self.all_sections()
153 .filter(|r| r.record_type() == q_type)
154 .any(|r| &r.name == q.name())
155 }
156 }
157 };
158
159 if found {
160 return true;
161 }
162 }
163
164 false
165 }
166
167 /// Borrow the inner buffer from the response
168 pub fn as_buffer(&self) -> &[u8] {
169 &self.buffer
170 }
171
172 /// Take the inner buffer from the response
173 pub fn into_buffer(self) -> Vec<u8> {
174 self.buffer
175 }
176
177 /// Take the inner Message from the response
178 pub fn into_message(self) -> Message {
179 self.message
180 }
181
182 /// Take the inner Message and buffer from the response
183 pub fn into_parts(self) -> (Message, Vec<u8>) {
184 (self.message, self.buffer)
185 }
186}
187
188impl Deref for DnsResponse {
189 type Target = Message;
190
191 fn deref(&self) -> &Self::Target {
192 &self.message
193 }
194}
195
196impl DerefMut for DnsResponse {
197 fn deref_mut(&mut self) -> &mut Self::Target {
198 &mut self.message
199 }
200}
201
202impl From<DnsResponse> for Message {
203 fn from(response: DnsResponse) -> Self {
204 response.message
205 }
206}
207
208#[cfg(all(test, any(feature = "std", feature = "no-std-rand")))]
209mod tests {
210 use crate::op::{Message, Query, ResponseCode};
211 use crate::rr::RData;
212 use crate::rr::rdata::{A, NS, SOA};
213 use crate::rr::{Name, Record, RecordType};
214
215 use super::*;
216
217 fn xx() -> Name {
218 Name::from_ascii("XX.").unwrap()
219 }
220
221 fn ns1() -> Name {
222 Name::from_ascii("NS1.XX.").unwrap()
223 }
224
225 fn hostmaster() -> Name {
226 Name::from_ascii("HOSTMASTER.NS1.XX.").unwrap()
227 }
228
229 fn example() -> Name {
230 Name::from_ascii("EXAMPLE.").unwrap()
231 }
232
233 fn an_example() -> Name {
234 Name::from_ascii("AN.EXAMPLE.").unwrap()
235 }
236
237 fn ns1_record() -> Record {
238 Record::from_rdata(xx(), 88640, RData::NS(NS(ns1())))
239 }
240
241 fn ns1_a() -> Record {
242 Record::from_rdata(xx(), 88640, RData::A(A::new(127, 0, 0, 2)))
243 }
244
245 fn soa() -> Record {
246 Record::from_rdata(
247 example(),
248 88640,
249 RData::SOA(SOA::new(ns1(), hostmaster(), 1, 2, 3, 4, 5)),
250 )
251 }
252
253 #[test]
254 fn test_contains_answer() {
255 let mut message = Message::query();
256 message.metadata.response_code = ResponseCode::NXDomain;
257 message.add_query(Query::query(Name::root(), RecordType::A));
258 message.add_answer(Record::from_rdata(
259 Name::root(),
260 88640,
261 RData::A(A::new(127, 0, 0, 2)),
262 ));
263
264 let response = DnsResponse::from_message(message.into_response()).unwrap();
265
266 assert!(response.contains_answer())
267 }
268
269 #[test]
270 fn contains_soa() {
271 let mut message = Message::query();
272 message.metadata.response_code = ResponseCode::NoError;
273 message.add_query(Query::query(an_example(), RecordType::SOA));
274 message.add_authority(soa());
275
276 let response = DnsResponse::from_message(message.into_response()).unwrap();
277
278 assert!(response.contains_answer());
279 }
280
281 #[test]
282 fn contains_any() {
283 let mut message = Message::query();
284 message.metadata.response_code = ResponseCode::NoError;
285 message.add_query(Query::query(xx(), RecordType::ANY));
286 message.add_authority(ns1_record());
287 message.add_additional(ns1_a());
288
289 let response = DnsResponse::from_message(message.into_response()).unwrap();
290
291 assert!(response.contains_answer());
292 }
293
294 #[test]
295 fn not_a_response() {
296 assert!(matches!(
297 DnsResponse::from_message(Message::query()).unwrap_err(),
298 ProtoError::NotAResponse
299 ));
300 assert!(matches!(
301 DnsResponse::from_buffer(Message::query().to_vec().unwrap()).unwrap_err(),
302 ProtoError::NotAResponse
303 ));
304 }
305}