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
148#[cfg(test)]
149mod tests {
150    use std::str::FromStr;
151
152    use crate::proto::op::Query;
153    use crate::proto::rr::rdata::{A, NS};
154    use crate::proto::rr::{Name, RData, Record, RecordType};
155
156    use super::*;
157
158    #[test]
159    #[cfg(feature = "__dnssec")]
160    fn test_dnssec_lookup() {
161        use hickory_proto::dnssec::Proof;
162
163        let mut a1 = Record::from_rdata(
164            Name::from_str("www.example.com.").unwrap(),
165            80,
166            RData::A(A::new(127, 0, 0, 1)),
167        );
168        a1.proof = Proof::Secure;
169
170        let mut a2 = Record::from_rdata(
171            Name::from_str("www.example.com.").unwrap(),
172            80,
173            RData::A(A::new(127, 0, 0, 2)),
174        );
175        a2.proof = Proof::Insecure;
176
177        let mut message = Message::response(0, OpCode::Query);
178        message.add_query(Query::default());
179        message.add_answers([a1.clone(), a2.clone()]);
180
181        let lookup = Lookup {
182            message,
183            valid_until: Instant::now(),
184        };
185
186        let mut lookup = lookup.message().dnssec_answers();
187
188        assert_eq!(*lookup.next().unwrap().require(Proof::Secure).unwrap(), a1);
189        assert_eq!(
190            *lookup.next().unwrap().require(Proof::Insecure).unwrap(),
191            a2
192        );
193        assert_eq!(lookup.next(), None);
194    }
195
196    #[test]
197    fn test_extend_answers_preserves_sections() {
198        // Create a message with records in different sections
199        let mut message = Message::response(0, OpCode::Query);
200        let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
201        message.add_query(query.clone());
202
203        message.add_answers(vec![Record::from_rdata(
204            Name::from_str("www.example.com.").unwrap(),
205            80,
206            RData::A(A::new(127, 0, 0, 1)),
207        )]);
208        message.add_authority(Record::from_rdata(
209            Name::from_str("example.com.").unwrap(),
210            80,
211            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
212        ));
213        message.add_additionals(vec![Record::from_rdata(
214            Name::from_str("ns1.example.com.").unwrap(),
215            80,
216            RData::A(A::new(192, 0, 2, 1)),
217        )]);
218
219        let mut lookup = Lookup {
220            message,
221            valid_until: Instant::now(),
222        };
223
224        // Extend with new answer record
225        let new_record = Record::from_rdata(
226            Name::from_str("www.example.com.").unwrap(),
227            80,
228            RData::A(A::new(127, 0, 0, 2)),
229        );
230        lookup.extend_answers(vec![new_record.clone()]);
231
232        // Verify that lookup.message was updated (not just a temporary reference)
233        assert_eq!(lookup.answers().len(), 2);
234        assert_eq!(lookup.answers()[1], new_record);
235
236        // Verify sections were preserved
237        assert_eq!(lookup.authorities().len(), 1);
238        assert_eq!(lookup.additionals().len(), 1);
239
240        // Verify the authority and additional records are intact
241        if let RData::NS(ns) = &lookup.authorities()[0].data {
242            assert_eq!(ns.0, Name::from_str("ns1.example.com.").unwrap());
243        } else {
244            panic!("Authority record should be NS");
245        }
246
247        if let RData::A(a) = lookup.additionals()[0].data {
248            assert_eq!(a, A::new(192, 0, 2, 1));
249        } else {
250            panic!("Additional record should be A");
251        }
252    }
253
254    #[test]
255    fn test_append_preserves_sections() {
256        // Create first lookup with records in all sections
257        let mut message1 = Message::response(0, OpCode::Query);
258        let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
259        message1.add_query(query.clone());
260        message1.add_answers(vec![Record::from_rdata(
261            Name::from_str("www.example.com.").unwrap(),
262            80,
263            RData::A(A::new(127, 0, 0, 1)),
264        )]);
265        message1.add_authority(Record::from_rdata(
266            Name::from_str("example.com.").unwrap(),
267            80,
268            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
269        ));
270        message1.add_additionals(vec![Record::from_rdata(
271            Name::from_str("ns1.example.com.").unwrap(),
272            80,
273            RData::A(A::new(192, 0, 2, 1)),
274        )]);
275
276        let lookup1 = Lookup {
277            message: message1,
278            valid_until: Instant::now(),
279        };
280
281        // Create second lookup with different records in all sections
282        let mut message2 = Message::response(0, OpCode::Query);
283        message2.add_query(query.clone());
284        message2.add_answers(vec![Record::from_rdata(
285            Name::from_str("www.example.com.").unwrap(),
286            80,
287            RData::A(A::new(127, 0, 0, 2)),
288        )]);
289        message2.add_authority(Record::from_rdata(
290            Name::from_str("example.com.").unwrap(),
291            80,
292            RData::NS(NS(Name::from_str("ns2.example.com.").unwrap())),
293        ));
294        message2.add_additionals(vec![Record::from_rdata(
295            Name::from_str("ns2.example.com.").unwrap(),
296            80,
297            RData::A(A::new(192, 0, 2, 2)),
298        )]);
299
300        let lookup2 = Lookup {
301            message: message2,
302            valid_until: Instant::now(),
303        };
304
305        // Append lookup2 to lookup1
306        let combined = lookup1.append(lookup2);
307
308        // Verify that sections were preserved and combined
309        assert_eq!(combined.answers().len(), 2);
310        assert_eq!(combined.authorities().len(), 2);
311        assert_eq!(combined.additionals().len(), 2);
312
313        // Verify answer records
314        if let RData::A(a) = combined.answers()[0].data {
315            assert_eq!(a, A::new(127, 0, 0, 1));
316        } else {
317            panic!("First answer should be A");
318        }
319        if let RData::A(a) = combined.answers()[1].data {
320            assert_eq!(a, A::new(127, 0, 0, 2));
321        } else {
322            panic!("Second answer should be A");
323        }
324
325        // Verify authority records
326        if let RData::NS(ns) = &combined.authorities()[0].data {
327            assert_eq!(ns.0, Name::from_str("ns1.example.com.").unwrap());
328        } else {
329            panic!("First authority should be NS");
330        }
331        if let RData::NS(ns) = &combined.authorities()[1].data {
332            assert_eq!(ns.0, Name::from_str("ns2.example.com.").unwrap());
333        } else {
334            panic!("Second authority should be NS");
335        }
336
337        // Verify additional records
338        if let RData::A(a) = combined.additionals()[0].data {
339            assert_eq!(a, A::new(192, 0, 2, 1));
340        } else {
341            panic!("First additional should be A");
342        }
343        if let RData::A(a) = combined.additionals()[1].data {
344            assert_eq!(a, A::new(192, 0, 2, 2));
345        } else {
346            panic!("Second additional should be A");
347        }
348    }
349}