hickory_server/store/blocklist.rs
1// Copyright 2015-2022 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//! Blocklist resolver related types
9
10#![cfg(feature = "blocklist")]
11
12use std::{
13 collections::HashMap,
14 fs::File,
15 io,
16 io::{Error, Read},
17 net::{Ipv4Addr, Ipv6Addr},
18 path::Path,
19 str::FromStr,
20 time::{Duration, Instant},
21};
22
23use serde::Deserialize;
24use tracing::{error, info, trace};
25
26#[cfg(feature = "__dnssec")]
27use crate::{authority::Nsec3QueryInfo, dnssec::NxProofKind};
28use crate::{
29 authority::{
30 Authority, LookupControlFlow, LookupError, LookupObject, LookupOptions, MessageRequest,
31 UpdateResult, ZoneType,
32 },
33 proto::{
34 op::{Query, ResponseCode},
35 rr::{
36 LowerName, Name, RData, Record, RecordType,
37 rdata::{A, AAAA, TXT},
38 },
39 },
40 resolver::lookup::Lookup,
41 server::RequestInfo,
42};
43
44// TODO:
45// * Add (optional) support for logging the client IP address. This will require some Authority
46// trait changes to accomplish
47// * Add query-type specific results for non-address queries
48// * Add support for per-blocklist sinkhole IPs, block messages, actions
49// * Add support for an exclusion list: allow the user to configure a list of patterns that
50// will never be insert into the in-memory blocklist (such as their own domain)
51// * Add support for regex matching
52
53/// A conditional authority that will resolve queries against one or more block lists and return a
54/// forged response. The typical use case will be to use this in a chained configuration before a
55/// forwarding or recursive resolver in order to pre-emptively block queries for hosts that are on
56/// a block list. Refer to tests/test-data/test_configs/chained_blocklist.toml for an example
57/// of this configuration.
58///
59/// The blocklist authority also supports the consult interface, which allows an authority to review
60/// a query/response that has been processed by another authority, and, optionally, overwrite that
61/// response before returning it to the requestor. There is an example of this configuration in
62/// tests/test-data/test_configs/example_consulting_blocklist.toml. The main intended use of this
63/// feature is to allow log-only configurations, to allow administrators to see if blocklist domains
64/// are being queried. While this can be configured to overwrite responses, it is not recommended
65/// to do so - it is both more efficient, and more secure, to allow the blocklist to drop queries
66/// pre-emptively, as in the first example.
67pub struct BlocklistAuthority {
68 origin: LowerName,
69 blocklist: HashMap<LowerName, bool>,
70 wildcard_match: bool,
71 min_wildcard_depth: u8,
72 sinkhole_ipv4: Ipv4Addr,
73 sinkhole_ipv6: Ipv6Addr,
74 ttl: u32,
75 block_message: Option<String>,
76 consult_action: BlocklistConsultAction,
77}
78
79impl BlocklistAuthority {
80 /// Read the Authority for the origin from the specified configuration
81 pub async fn try_from_config(
82 origin: Name,
83 _zone_type: ZoneType,
84 config: &BlocklistConfig,
85 base_dir: Option<&Path>,
86 ) -> Result<Self, String> {
87 info!("loading blocklist config: {origin}");
88
89 let mut authority = Self {
90 origin: origin.into(),
91 blocklist: HashMap::new(),
92 wildcard_match: config.wildcard_match,
93 min_wildcard_depth: config.min_wildcard_depth,
94 sinkhole_ipv4: match config.sinkhole_ipv4 {
95 Some(ip) => ip,
96 None => Ipv4Addr::UNSPECIFIED,
97 },
98 sinkhole_ipv6: match config.sinkhole_ipv6 {
99 Some(ip) => ip,
100 None => Ipv6Addr::UNSPECIFIED,
101 },
102 ttl: config.ttl,
103 block_message: config.block_message.clone(),
104 consult_action: config.consult_action,
105 };
106
107 let base_dir = match base_dir {
108 Some(dir) => dir.display(),
109 None => {
110 return Err(format!(
111 "invalid blocklist (zone directory) base path specified: '{base_dir:?}'"
112 ));
113 }
114 };
115
116 // Load block lists into the block table cache for this authority.
117 for bl in &config.lists {
118 info!("adding blocklist {bl}");
119
120 match File::open(format!("{base_dir}/{bl}")) {
121 Ok(handle) => {
122 if let Err(e) = authority.add(handle) {
123 return Err(format!(
124 "unable to add data from blocklist {base_dir}/{bl}: {e:?}"
125 ));
126 }
127 }
128 Err(e) => {
129 return Err(format!(
130 "unable to open blocklist file {base_dir}/{bl}: {e:?}"
131 ));
132 }
133 }
134 }
135
136 Ok(authority)
137 }
138
139 /// Add the contents of a block list to the in-memory cache. This function is normally called
140 /// from try_from_config, but it can be invoked after the blocklist authority is created.
141 ///
142 /// # Arguments
143 ///
144 /// * `handle` - A source implementing `std::io::Read` that contains the blocklist entries
145 /// to insert into the in-memory cache.
146 ///
147 /// # Return value
148 ///
149 /// `Result<(), std::io::Error>`
150 ///
151 /// # Expected format of blocklist entries
152 ///
153 /// * One entry per line
154 /// * Any character after a '\#' will be treated as a comment and stripped out.
155 /// * Leading wildcard entries are supported when the user has wildcard_match set to true.
156 /// E.g., '\*.foo.com' will match any host in the foo.com domain. Intermediate wildcard
157 /// matches, such as 'www.\*.com' are not supported. **Note: when wildcard matching is enabled,
158 /// min_wildcard_depth (default: 2) controls how many static name labels must be present for a
159 /// wildcard entry to be valid. With the default value of 2, an entry for '\*.foo.com' would
160 /// be accepted, but an entry for '\*.com' would not.**
161 /// * All entries are treated as being fully-qualified. If an entry does not contain a trailing
162 /// '.', one will be added before insertion into the cache.
163 ///
164 /// # Example
165 /// ```
166 /// use std::{fs::File, net::{Ipv4Addr, Ipv6Addr}, path::Path, str::FromStr, sync::Arc};
167 /// use hickory_proto::rr::{LowerName, RecordType};
168 /// use hickory_resolver::Name;
169 /// use hickory_server::{authority::{AuthorityObject, LookupControlFlow, LookupOptions, ZoneType}, store::blocklist::*};
170 ///
171 /// #[tokio::main]
172 /// async fn main() {
173 /// let config = BlocklistConfig {
174 /// wildcard_match: true,
175 /// min_wildcard_depth: 2,
176 /// lists: vec!["default/blocklist.txt".to_string()],
177 /// sinkhole_ipv4: None,
178 /// sinkhole_ipv6: None,
179 /// block_message: None,
180 /// ttl: 86_400,
181 /// consult_action: BlocklistConsultAction::Disabled,
182 /// };
183 ///
184 /// let mut blocklist = BlocklistAuthority::try_from_config(
185 /// Name::root(),
186 /// ZoneType::External,
187 /// &config,
188 /// Some(Path::new("../../tests/test-data/test_configs")),
189 /// ).await.unwrap();
190 ///
191 /// let handle = File::open("../../tests/test-data/test_configs/default/blocklist2.txt").unwrap();
192 /// if let Err(e) = blocklist.add(handle) {
193 /// panic!("error adding blocklist: {e:?}");
194 /// }
195 ///
196 /// let origin = blocklist.origin().clone();
197 /// let authority = Arc::new(blocklist) as Arc<dyn AuthorityObject>;
198 ///
199 /// // In this example, malc0de.com only exists in the blocklist2.txt file we added to the
200 /// // authority after instantiating it. The following simulates a lookup against the blocklist
201 /// // authority, and checks for the expected response for a blocklist match.
202 /// use LookupControlFlow::*;
203 /// let Break(Ok(_res)) = authority.lookup(
204 /// &LowerName::from(Name::from_ascii("malc0de.com.").unwrap()),
205 /// RecordType::A,
206 /// LookupOptions::default(),
207 /// ).await else {
208 /// panic!("blocklist authority did not return expected match");
209 /// };
210 /// }
211 /// ```
212 pub fn add(&mut self, mut handle: impl Read) -> Result<(), Error> {
213 let mut contents = String::new();
214
215 if let Err(e) = handle.read_to_string(&mut contents) {
216 error!("unable to read blocklist data: {e:?}");
217 return Err(e);
218 }
219
220 for mut entry in contents.lines() {
221 // Strip comments
222 if let Some((item, _)) = entry.split_once('#') {
223 entry = item.trim();
224 }
225
226 if entry.is_empty() {
227 continue;
228 }
229
230 let mut str_entry = entry.to_string();
231 if !entry.ends_with('.') {
232 str_entry += ".";
233 }
234
235 let Ok(name) = LowerName::from_str(&str_entry[..]) else {
236 error!(
237 "unable to derive LowerName for blocklist entry '{str_entry}'; skipping entry"
238 );
239 continue;
240 };
241
242 trace!("inserting blocklist entry {str_entry}");
243
244 // The boolean value is not significant; only the key is used.
245 self.blocklist.insert(name, true);
246 }
247
248 Ok(())
249 }
250
251 /// Build a wildcard match list for a given host
252 fn wildcards(&self, host: &Name) -> Vec<LowerName> {
253 host.iter()
254 .enumerate()
255 .filter_map(|(i, _x)| {
256 if i > ((self.min_wildcard_depth - 1) as usize) {
257 Some(host.trim_to(i + 1).into_wildcard().into())
258 } else {
259 None
260 }
261 })
262 .collect()
263 }
264
265 /// Perform a blocklist lookup. Returns true on match, false on no match. This is also where
266 /// wildcard expansion is done, if wildcard support is enabled for the blocklist authority.
267 fn is_blocked(&self, name: &LowerName) -> bool {
268 let mut match_list = vec![name.to_owned()];
269
270 if self.wildcard_match {
271 match_list.append(&mut self.wildcards(name));
272 }
273
274 trace!("blocklist match list: {match_list:?}");
275
276 if match_list
277 .iter()
278 .any(|entry| self.blocklist.contains_key(entry))
279 {
280 info!("block list matched query {name}");
281 return true;
282 }
283
284 false
285 }
286
287 /// Generate a BlocklistLookup to return on a blocklist match. This will return a lookup with
288 /// either an A or AAAA record and, if the user has configured a block message, a TXT record
289 /// with the contents of that message.
290 fn blocklist_response(&self, name: Name, rtype: RecordType) -> BlocklistLookup {
291 let mut records = vec![];
292
293 match rtype {
294 RecordType::AAAA => records.push(Record::from_rdata(
295 name.clone(),
296 self.ttl,
297 RData::AAAA(AAAA(self.sinkhole_ipv6)),
298 )),
299 _ => records.push(Record::from_rdata(
300 name.clone(),
301 self.ttl,
302 RData::A(A(self.sinkhole_ipv4)),
303 )),
304 }
305
306 if let Some(block_message) = &self.block_message {
307 records.push(Record::from_rdata(
308 name.clone(),
309 self.ttl,
310 RData::TXT(TXT::new(vec![block_message.clone()])),
311 ));
312 }
313
314 BlocklistLookup(Lookup::new_with_deadline(
315 Query::query(name.clone(), rtype),
316 records.into(),
317 Instant::now() + Duration::from_secs(u64::from(self.ttl)),
318 ))
319 }
320}
321
322#[async_trait::async_trait]
323impl Authority for BlocklistAuthority {
324 type Lookup = BlocklistLookup;
325
326 fn zone_type(&self) -> ZoneType {
327 ZoneType::External
328 }
329
330 fn is_axfr_allowed(&self) -> bool {
331 false
332 }
333
334 async fn update(&self, _update: &MessageRequest) -> UpdateResult<bool> {
335 Err(ResponseCode::NotImp)
336 }
337
338 fn origin(&self) -> &LowerName {
339 &self.origin
340 }
341
342 /// Perform a blocklist lookup. This will return LookupControlFlow::Break(Ok) on a match, or
343 /// LookupControlFlow::Skip on no match.
344 async fn lookup(
345 &self,
346 name: &LowerName,
347 rtype: RecordType,
348 _lookup_options: LookupOptions,
349 ) -> LookupControlFlow<Self::Lookup> {
350 use LookupControlFlow::*;
351
352 trace!("blocklist lookup: {name} {rtype}");
353
354 if self.is_blocked(name) {
355 return Break(Ok(self.blocklist_response(Name::from(name), rtype)));
356 }
357
358 trace!("query '{name}' is not in blocklist; returning Skip...");
359 Skip
360 }
361
362 /// Optionally, perform a blocklist lookup after another authority has done a lookup for this
363 /// query.
364 async fn consult(
365 &self,
366 name: &LowerName,
367 rtype: RecordType,
368 lookup_options: LookupOptions,
369 last_result: LookupControlFlow<Box<dyn LookupObject>>,
370 ) -> LookupControlFlow<Box<dyn LookupObject>> {
371 match self.consult_action {
372 BlocklistConsultAction::Disabled => last_result,
373 BlocklistConsultAction::Log => {
374 self.is_blocked(name);
375 last_result
376 }
377 BlocklistConsultAction::Enforce => {
378 let lookup = self.lookup(name, rtype, lookup_options).await;
379
380 if lookup.is_break() {
381 lookup.map_dyn()
382 } else {
383 last_result
384 }
385 }
386 }
387 }
388
389 async fn search(
390 &self,
391 request_info: RequestInfo<'_>,
392 lookup_options: LookupOptions,
393 ) -> LookupControlFlow<Self::Lookup> {
394 self.lookup(
395 request_info.query.name(),
396 request_info.query.query_type(),
397 lookup_options,
398 )
399 .await
400 }
401
402 async fn get_nsec_records(
403 &self,
404 _name: &LowerName,
405 _lookup_options: LookupOptions,
406 ) -> LookupControlFlow<Self::Lookup> {
407 LookupControlFlow::Continue(Err(LookupError::from(io::Error::new(
408 io::ErrorKind::Other,
409 "Getting NSEC records is unimplemented for the blocklist",
410 ))))
411 }
412
413 #[cfg(feature = "__dnssec")]
414 async fn get_nsec3_records(
415 &self,
416 _info: Nsec3QueryInfo<'_>,
417 _lookup_options: LookupOptions,
418 ) -> LookupControlFlow<Self::Lookup> {
419 LookupControlFlow::Continue(Err(LookupError::from(io::Error::new(
420 io::ErrorKind::Other,
421 "getting NSEC3 records is unimplemented for the forwarder",
422 ))))
423 }
424
425 #[cfg(feature = "__dnssec")]
426 fn nx_proof_kind(&self) -> Option<&NxProofKind> {
427 None
428 }
429}
430
431/// Consult action enum. Controls how consult lookups are handled.
432#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
433pub enum BlocklistConsultAction {
434 /// Do not log or block any request when the blocklist is called via consult
435 #[default]
436 Disabled,
437 /// Log and block matching requests when the blocklist is called via consult
438 Enforce,
439 /// Log but do not block matching requests when the blocklist is called via consult
440 Log,
441}
442
443/// Configuration for file based zones
444#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
445#[serde(default, deny_unknown_fields)]
446pub struct BlocklistConfig {
447 /// Support wildcards? Defaults to true. If set to true, block list entries containing
448 /// asterisks will be expanded to match queries.
449 pub wildcard_match: bool,
450
451 /// Minimum wildcard depth. Defaults to 2. Any wildcard entries without at least this many
452 /// static elements will not be expanded (e.g., *.com has a depth of 1; *.example.com has a
453 /// depth of two.) This is meant as a safeguard against an errant block list entry, such as *
454 /// or *.com that might block many more hosts than intended.
455 pub min_wildcard_depth: u8,
456
457 /// Block lists to load. These should be specified as relative (to the server zone directory)
458 /// paths in the config file.
459 pub lists: Vec<String>,
460
461 /// IPv4 sinkhole IP. This is the IP that is returned when a blocklist entry is matched for an
462 /// A query. If unspecified, an implementation-provided default will be used.
463 pub sinkhole_ipv4: Option<Ipv4Addr>,
464
465 /// IPv6 sinkhole IP. This is the IP that is returned when a blocklist entry is matched for a
466 /// AAAA query. If unspecified, an implementation-provided default will be used.
467 pub sinkhole_ipv6: Option<Ipv6Addr>,
468
469 /// Block TTL. This is the length of time a block response should be stored in the requesting
470 /// resolvers cache, in seconds. Defaults to 86,400 seconds.
471 pub ttl: u32,
472
473 /// Block message to return to the user. This is an optional message that, if configured, will
474 /// be returned as a TXT record in the additionals section when a blocklist entry is matched for
475 /// a query.
476 pub block_message: Option<String>,
477
478 /// The consult action controls how the blocklist handles queries where another authority has
479 /// already provided an answer. By default, it ignores any such queries ("Disabled",) however
480 /// it can be configured to log blocklist matches for those queries ("Log",) or can be
481 /// configured to overwrite the previous responses ("Enforce".)
482 pub consult_action: BlocklistConsultAction,
483}
484
485impl Default for BlocklistConfig {
486 fn default() -> Self {
487 Self {
488 wildcard_match: true,
489 min_wildcard_depth: 2,
490 lists: vec![],
491 sinkhole_ipv4: None,
492 sinkhole_ipv6: None,
493 ttl: 86_400,
494 block_message: None,
495 consult_action: BlocklistConsultAction::default(),
496 }
497 }
498}
499
500/// A lookup object that is returned when a blocklist entry is matched.
501pub struct BlocklistLookup(Lookup);
502
503impl LookupObject for BlocklistLookup {
504 fn is_empty(&self) -> bool {
505 self.0.is_empty()
506 }
507
508 fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Record> + Send + 'a> {
509 Box::new(self.0.record_iter())
510 }
511
512 fn take_additionals(&mut self) -> Option<Box<dyn LookupObject>> {
513 None
514 }
515}
516
517#[cfg(test)]
518mod test {
519 use std::{
520 net::{Ipv4Addr, Ipv6Addr},
521 path::Path,
522 str::FromStr,
523 sync::Arc,
524 };
525 use tracing::error;
526
527 use crate::{
528 authority::{AuthorityObject, LookupOptions, ZoneType},
529 proto::rr::domain::Name,
530 proto::rr::{
531 LowerName, RData, RecordType,
532 rdata::{A, AAAA},
533 },
534 store::blocklist::BlocklistConsultAction,
535 };
536 use test_support::subscribe;
537
538 enum TestResult {
539 Break,
540 Skip,
541 }
542
543 #[tokio::test]
544 async fn test_blocklist_basic() {
545 subscribe();
546 let config = super::BlocklistConfig {
547 wildcard_match: true,
548 min_wildcard_depth: 2,
549 lists: vec!["default/blocklist.txt".to_string()],
550 sinkhole_ipv4: None,
551 sinkhole_ipv6: None,
552 block_message: None,
553 ttl: 86_400,
554 consult_action: BlocklistConsultAction::Disabled,
555 };
556
557 let blocklist = super::BlocklistAuthority::try_from_config(
558 Name::root(),
559 ZoneType::External,
560 &config,
561 Some(Path::new("../../tests/test-data/test_configs/")),
562 );
563
564 let authority = blocklist.await;
565
566 // Test: verify the blocklist authority was successfully created.
567 match authority {
568 Ok(ref _authority) => {}
569 Err(e) => {
570 panic!("Unable to create blocklist authority: {e}");
571 }
572 }
573
574 let ao = Arc::new(authority.unwrap()) as Arc<dyn AuthorityObject>;
575
576 let v4 = A::new(0, 0, 0, 0);
577 let v6 = AAAA::new(0, 0, 0, 0, 0, 0, 0, 0);
578
579 use RecordType::{A as Rec_A, AAAA as Rec_AAAA};
580 use TestResult::*;
581 // Test: lookup a record that is in the blocklist and that should match without a wildcard.
582 basic_test(&ao, "foo.com.", Rec_A, Break, Some(v4), None, None).await;
583
584 // test: lookup a record that is not in the blocklist. This test should fail.
585 basic_test(&ao, "test.com.", Rec_A, Skip, None, None, None).await;
586
587 // Test: lookup a record that will match a wildcard that is in the blocklist.
588 basic_test(&ao, "www.foo.com.", Rec_A, Break, Some(v4), None, None).await;
589
590 // Test: lookup a record that will match a wildcard that is in the blocklist.
591 basic_test(&ao, "www.com.foo.com.", Rec_A, Break, Some(v4), None, None).await;
592
593 // Test: lookup a record that is in the blocklist and that should match without a wildcard.
594 basic_test(&ao, "foo.com.", Rec_AAAA, Break, None, Some(v6), None).await;
595
596 // test: lookup a record that is not in the blocklist. This test should fail.
597 basic_test(&ao, "test.com.", Rec_AAAA, Skip, None, None, None).await;
598
599 // Test: lookup a record that will match a wildcard that is in the blocklist.
600 basic_test(&ao, "www.foo.com.", Rec_AAAA, Break, None, Some(v6), None).await;
601
602 // Test: lookup a record that will match a wildcard that is in the blocklist.
603 basic_test(&ao, "ab.cd.foo.com.", Rec_AAAA, Break, None, Some(v6), None).await;
604 }
605
606 #[tokio::test]
607 async fn test_blocklist_wildcard_disabled() {
608 subscribe();
609 let config = super::BlocklistConfig {
610 min_wildcard_depth: 2,
611 wildcard_match: false,
612 lists: vec!["default/blocklist.txt".to_string()],
613 sinkhole_ipv4: Some(Ipv4Addr::new(192, 0, 2, 1)),
614 sinkhole_ipv6: Some(Ipv6Addr::new(0, 0, 0, 0, 0xc0, 0, 2, 1)),
615 block_message: Some(String::from("blocked")),
616 ttl: 86_400,
617 consult_action: BlocklistConsultAction::Disabled,
618 };
619
620 let blocklist = super::BlocklistAuthority::try_from_config(
621 Name::root(),
622 ZoneType::External,
623 &config,
624 Some(Path::new("../../tests/test-data/test_configs/")),
625 );
626
627 let authority = blocklist.await;
628
629 // Test: verify the blocklist authority was successfully created.
630 match authority {
631 Ok(ref _authority) => {}
632 Err(e) => {
633 panic!("Unable to create blocklist authority: {e}");
634 }
635 }
636
637 let ao = Arc::new(authority.unwrap()) as Arc<dyn AuthorityObject>;
638
639 let v4 = A::new(192, 0, 2, 1);
640 let v6 = AAAA::new(0, 0, 0, 0, 0xc0, 0, 2, 1);
641 let msg = config.block_message;
642
643 use RecordType::{A as Rec_A, AAAA as Rec_AAAA};
644 use TestResult::*;
645
646 // Test: lookup a record that is in the blocklist and that should match without a wildcard.
647 basic_test(&ao, "foo.com.", Rec_A, Break, Some(v4), None, msg.clone()).await;
648
649 // Test: lookup a record that is not in the blocklist, but would match a wildcard; this
650 // should fail.
651 basic_test(&ao, "www.foo.com.", Rec_A, Skip, None, None, msg.clone()).await;
652
653 // Test: lookup a record that is in the blocklist and that should match without a wildcard.
654 basic_test(&ao, "foo.com.", Rec_AAAA, Break, None, Some(v6), msg).await;
655 }
656
657 #[tokio::test]
658 #[should_panic]
659 async fn test_blocklist_wrong_block_message() {
660 subscribe();
661 let config = super::BlocklistConfig {
662 min_wildcard_depth: 2,
663 wildcard_match: false,
664 lists: vec!["default/blocklist.txt".to_string()],
665 sinkhole_ipv4: Some(Ipv4Addr::new(192, 0, 2, 1)),
666 sinkhole_ipv6: Some(Ipv6Addr::new(0, 0, 0, 0, 0xc0, 0, 2, 1)),
667 block_message: Some(String::from("blocked")),
668 ttl: 86_400,
669 consult_action: BlocklistConsultAction::Disabled,
670 };
671
672 let blocklist = super::BlocklistAuthority::try_from_config(
673 Name::root(),
674 ZoneType::External,
675 &config,
676 Some(Path::new("../../tests/test-data/test_configs/")),
677 );
678
679 let authority = blocklist.await;
680
681 // Test: verify the blocklist authority was successfully created.
682 match authority {
683 Ok(ref _authority) => {}
684 Err(e) => {
685 error!("Unable to create blocklist authority: {e}");
686 return;
687 }
688 }
689
690 let ao = Arc::new(authority.unwrap()) as Arc<dyn AuthorityObject>;
691
692 let sinkhole_v4 = A::new(192, 0, 2, 1);
693
694 // Test: lookup a record that is in the blocklist, but specify an incorrect block message to
695 // match.
696 basic_test(
697 &ao,
698 "foo.com.",
699 RecordType::A,
700 TestResult::Break,
701 Some(sinkhole_v4),
702 None,
703 Some(String::from("wrong message")),
704 )
705 .await;
706 }
707
708 #[allow(clippy::borrowed_box)]
709 async fn basic_test(
710 ao: &Arc<dyn AuthorityObject>,
711 query: &'static str,
712 q_type: RecordType,
713 r_type: TestResult,
714 ipv4: Option<A>,
715 ipv6: Option<AAAA>,
716 msg: Option<String>,
717 ) {
718 let res = ao
719 .lookup(
720 &LowerName::from_str(query).unwrap(),
721 q_type,
722 LookupOptions::default(),
723 )
724 .await;
725
726 use super::LookupControlFlow::*;
727
728 match r_type {
729 TestResult::Break => match res {
730 Break(Ok(l)) => {
731 if !l.iter().all(|x| match x.record_type() {
732 RecordType::TXT => {
733 if let Some(msg) = &msg {
734 x.data().to_string() == *msg
735 } else {
736 false
737 }
738 }
739 RecordType::AAAA => {
740 let Some(rec_ip) = ipv6 else {
741 panic!("expected to validate record IPv6, but None was passed");
742 };
743
744 x.name() == &Name::from_str(query).unwrap()
745 && x.data() == &RData::AAAA(rec_ip)
746 }
747 _ => {
748 let Some(rec_ip) = ipv4 else {
749 panic!("expected to validate record IPv4, but None was passed");
750 };
751
752 x.name() == &Name::from_str(query).unwrap()
753 && x.data() == &RData::A(rec_ip)
754 }
755 }) {
756 panic!("{query} lookup data is incorrect.");
757 }
758 }
759 _ => panic!("Unexpected result for {query}: {res}"),
760 },
761 TestResult::Skip => match res {
762 Skip => {}
763 _ => {
764 panic!("unexpected result for {query}; expected Skip, found {res}");
765 }
766 },
767 }
768 }
769}