Skip to main content

hickory_resolver/
lookup.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//! Lookup result from a resolution of ipv4 and ipv6 records with a Resolver.
9
10use std::{
11    cmp::min,
12    time::{Duration, Instant},
13};
14
15use crate::{
16    cache::MAX_TTL,
17    proto::{
18        op::{Message, OpCode, Query},
19        rr::{RData, Record},
20    },
21};
22
23/// Result of a DNS query when querying for any record type supported by the Hickory DNS Proto library.
24///
25/// For IP resolution see LookupIp, as it has more features for A and AAAA lookups.
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct Lookup {
28    message: Message,
29    valid_until: Instant,
30}
31
32impl Lookup {
33    /// Create a new Lookup from a complete DNS Message.
34    pub(crate) fn new(message: Message, valid_until: Instant) -> Self {
35        debug_assert!(
36            !message.queries.is_empty(),
37            "lookup message must have at least one query"
38        );
39
40        Self {
41            message,
42            valid_until,
43        }
44    }
45
46    /// Return new instance with given rdata and the maximum TTL.
47    pub fn from_rdata(query: Query, rdata: RData) -> Self {
48        let record = Record::from_rdata(query.name().clone(), MAX_TTL, rdata);
49        Self::new_with_max_ttl(query, [record])
50    }
51
52    /// Return new instance with given records and the maximum TTL.
53    pub fn new_with_max_ttl(query: Query, answers: impl IntoIterator<Item = Record>) -> Self {
54        let valid_until = Instant::now() + Duration::from_secs(u64::from(MAX_TTL));
55        Self::new_with_deadline(query, answers, valid_until)
56    }
57
58    /// Return a new instance with the given records and deadline.
59    pub fn new_with_deadline(
60        query: Query,
61        answers: impl IntoIterator<Item = Record>,
62        valid_until: Instant,
63    ) -> Self {
64        let mut message = Message::response(0, OpCode::Query);
65        message.add_query(query.clone());
66        message.add_answers(answers);
67
68        Self {
69            message,
70            valid_until,
71        }
72    }
73
74    /// Returns a reference to the `Query` that was used to produce this result.
75    pub fn query(&self) -> &Query {
76        self.message
77            .queries
78            .first()
79            .expect("Lookup message always has a query")
80    }
81
82    /// Returns a reference to the underlying DNS Message.
83    pub fn message(&self) -> &Message {
84        &self.message
85    }
86
87    /// Returns a reference to the answer records from the message.
88    pub fn answers(&self) -> &[Record] {
89        &self.message.answers
90    }
91
92    /// Returns a reference to the authority records from the message.
93    pub fn authorities(&self) -> &[Record] {
94        &self.message.authorities
95    }
96
97    /// Returns a reference to the additional records from the message.
98    pub fn additionals(&self) -> &[Record] {
99        &self.message.additionals
100    }
101
102    /// Returns the `Instant` at which this `Lookup` is no longer valid.
103    pub fn valid_until(&self) -> Instant {
104        self.valid_until
105    }
106
107    /// Combine two lookup results, preserving section structure
108    ///
109    /// Appends records from each section of `other` to the corresponding section of `self`.
110    pub(crate) fn append(&self, other: Self) -> Self {
111        // Clone self to get a mutable copy
112        let mut result = self.clone();
113
114        // Append each section separately to preserve structure
115        result.message.add_answers(other.answers().iter().cloned());
116        result
117            .message
118            .add_authorities(other.authorities().iter().cloned());
119        result
120            .message
121            .add_additionals(other.additionals().iter().cloned());
122
123        // Choose the sooner deadline of the two lookups
124        result.valid_until = min(self.valid_until(), other.valid_until());
125        result
126    }
127
128    #[doc(hidden)] // For use in server tests
129    pub fn extend_authorities(&mut self, records: impl IntoIterator<Item = Record>) {
130        self.message.add_authorities(records);
131    }
132
133    #[doc(hidden)] // For use in server tests
134    pub fn extend_additionals(&mut self, records: impl IntoIterator<Item = Record>) {
135        self.message.add_additionals(records);
136    }
137
138    /// Add new records to this lookup, without creating a new Lookup
139    ///
140    /// Records are added to the ANSWERS section while preserving existing section structure
141    #[cfg(test)]
142    fn extend_answers(&mut self, other: Vec<Record>) {
143        // Add new records to the answers section, preserving existing sections
144        self.message.add_answers(other);
145    }
146}
147
148impl From<Lookup> for Message {
149    fn from(lookup: Lookup) -> Self {
150        lookup.message
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use std::str::FromStr;
157
158    use crate::proto::op::Query;
159    use crate::proto::rr::rdata::{A, NS};
160    use crate::proto::rr::{Name, RData, Record, RecordType};
161
162    use super::*;
163
164    #[test]
165    #[cfg(feature = "__dnssec")]
166    fn test_dnssec_lookup() {
167        use hickory_proto::dnssec::Proof;
168
169        let mut a1 = Record::from_rdata(
170            Name::from_str("www.example.com.").unwrap(),
171            80,
172            RData::A(A::new(127, 0, 0, 1)),
173        );
174        a1.proof = Proof::Secure;
175
176        let mut a2 = Record::from_rdata(
177            Name::from_str("www.example.com.").unwrap(),
178            80,
179            RData::A(A::new(127, 0, 0, 2)),
180        );
181        a2.proof = Proof::Insecure;
182
183        let mut message = Message::response(0, OpCode::Query);
184        message.add_query(Query::default());
185        message.add_answers([a1.clone(), a2.clone()]);
186
187        let lookup = Lookup {
188            message,
189            valid_until: Instant::now(),
190        };
191
192        let mut lookup = lookup.message().dnssec_answers();
193
194        assert_eq!(*lookup.next().unwrap().require(Proof::Secure).unwrap(), a1);
195        assert_eq!(
196            *lookup.next().unwrap().require(Proof::Insecure).unwrap(),
197            a2
198        );
199        assert_eq!(lookup.next(), None);
200    }
201
202    #[test]
203    fn test_extend_answers_preserves_sections() {
204        // Create a message with records in different sections
205        let mut message = Message::response(0, OpCode::Query);
206        let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
207        message.add_query(query.clone());
208
209        message.add_answers(vec![Record::from_rdata(
210            Name::from_str("www.example.com.").unwrap(),
211            80,
212            RData::A(A::new(127, 0, 0, 1)),
213        )]);
214        message.add_authority(Record::from_rdata(
215            Name::from_str("example.com.").unwrap(),
216            80,
217            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
218        ));
219        message.add_additionals(vec![Record::from_rdata(
220            Name::from_str("ns1.example.com.").unwrap(),
221            80,
222            RData::A(A::new(192, 0, 2, 1)),
223        )]);
224
225        let mut lookup = Lookup {
226            message,
227            valid_until: Instant::now(),
228        };
229
230        // Extend with new answer record
231        let new_record = Record::from_rdata(
232            Name::from_str("www.example.com.").unwrap(),
233            80,
234            RData::A(A::new(127, 0, 0, 2)),
235        );
236        lookup.extend_answers(vec![new_record.clone()]);
237
238        // Verify that lookup.message was updated (not just a temporary reference)
239        assert_eq!(lookup.answers().len(), 2);
240        assert_eq!(lookup.answers()[1], new_record);
241
242        // Verify sections were preserved
243        assert_eq!(lookup.authorities().len(), 1);
244        assert_eq!(lookup.additionals().len(), 1);
245
246        // Verify the authority and additional records are intact
247        if let RData::NS(ns) = &lookup.authorities()[0].data {
248            assert_eq!(ns.0, Name::from_str("ns1.example.com.").unwrap());
249        } else {
250            panic!("Authority record should be NS");
251        }
252
253        if let RData::A(a) = lookup.additionals()[0].data {
254            assert_eq!(a, A::new(192, 0, 2, 1));
255        } else {
256            panic!("Additional record should be A");
257        }
258    }
259
260    #[test]
261    fn test_append_preserves_sections() {
262        // Create first lookup with records in all sections
263        let mut message1 = Message::response(0, OpCode::Query);
264        let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
265        message1.add_query(query.clone());
266        message1.add_answers(vec![Record::from_rdata(
267            Name::from_str("www.example.com.").unwrap(),
268            80,
269            RData::A(A::new(127, 0, 0, 1)),
270        )]);
271        message1.add_authority(Record::from_rdata(
272            Name::from_str("example.com.").unwrap(),
273            80,
274            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
275        ));
276        message1.add_additionals(vec![Record::from_rdata(
277            Name::from_str("ns1.example.com.").unwrap(),
278            80,
279            RData::A(A::new(192, 0, 2, 1)),
280        )]);
281
282        let lookup1 = Lookup {
283            message: message1,
284            valid_until: Instant::now(),
285        };
286
287        // Create second lookup with different records in all sections
288        let mut message2 = Message::response(0, OpCode::Query);
289        message2.add_query(query.clone());
290        message2.add_answers(vec![Record::from_rdata(
291            Name::from_str("www.example.com.").unwrap(),
292            80,
293            RData::A(A::new(127, 0, 0, 2)),
294        )]);
295        message2.add_authority(Record::from_rdata(
296            Name::from_str("example.com.").unwrap(),
297            80,
298            RData::NS(NS(Name::from_str("ns2.example.com.").unwrap())),
299        ));
300        message2.add_additionals(vec![Record::from_rdata(
301            Name::from_str("ns2.example.com.").unwrap(),
302            80,
303            RData::A(A::new(192, 0, 2, 2)),
304        )]);
305
306        let lookup2 = Lookup {
307            message: message2,
308            valid_until: Instant::now(),
309        };
310
311        // Append lookup2 to lookup1
312        let combined = lookup1.append(lookup2);
313
314        // Verify that sections were preserved and combined
315        assert_eq!(combined.answers().len(), 2);
316        assert_eq!(combined.authorities().len(), 2);
317        assert_eq!(combined.additionals().len(), 2);
318
319        // Verify answer records
320        if let RData::A(a) = combined.answers()[0].data {
321            assert_eq!(a, A::new(127, 0, 0, 1));
322        } else {
323            panic!("First answer should be A");
324        }
325        if let RData::A(a) = combined.answers()[1].data {
326            assert_eq!(a, A::new(127, 0, 0, 2));
327        } else {
328            panic!("Second answer should be A");
329        }
330
331        // Verify authority records
332        if let RData::NS(ns) = &combined.authorities()[0].data {
333            assert_eq!(ns.0, Name::from_str("ns1.example.com.").unwrap());
334        } else {
335            panic!("First authority should be NS");
336        }
337        if let RData::NS(ns) = &combined.authorities()[1].data {
338            assert_eq!(ns.0, Name::from_str("ns2.example.com.").unwrap());
339        } else {
340            panic!("Second authority should be NS");
341        }
342
343        // Verify additional records
344        if let RData::A(a) = combined.additionals()[0].data {
345            assert_eq!(a, A::new(192, 0, 2, 1));
346        } else {
347            panic!("First additional should be A");
348        }
349        if let RData::A(a) = combined.additionals()[1].data {
350            assert_eq!(a, A::new(192, 0, 2, 2));
351        } else {
352            panic!("Second additional should be A");
353        }
354    }
355}