Skip to main content

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}