resolv_conf/lib.rs
1//! The crate simply parses `/etc/resolv.conf` file and creates a config object
2//!
3//! # Examples
4//!
5//! ## Parsing a config from a string
6//! ```rust
7//! extern crate resolv_conf;
8//!
9//! use std::net::{Ipv4Addr, Ipv6Addr};
10//! use resolv_conf::{ScopedIp, Config, Network};
11//!
12//! fn main() {
13//! let config_str = "
14//! options ndots:8 timeout:8 attempts:8
15//!
16//! domain example.com
17//! search example.com sub.example.com
18//!
19//! nameserver 2001:4860:4860::8888
20//! nameserver 2001:4860:4860::8844
21//! nameserver 8.8.8.8
22//! nameserver 8.8.4.4
23//!
24//! options rotate
25//! options inet6 no-tld-query
26//!
27//! sortlist 130.155.160.0/255.255.240.0 130.155.0.0";
28//!
29//! // Parse the config.
30//! let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
31//!
32//! // We can build configs manually as well, either directly or with Config::new()
33//! let mut expected_config = Config::new();
34//! expected_config.nameservers = vec![
35//! ScopedIp::V6(Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888), None),
36//! ScopedIp::V6(Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8844), None),
37//! ScopedIp::V4(Ipv4Addr::new(8, 8, 8, 8)),
38//! ScopedIp::V4(Ipv4Addr::new(8, 8, 4, 4)),
39//! ];
40//! expected_config.sortlist = vec![
41//! Network::V4(Ipv4Addr::new(130, 155, 160, 0), Ipv4Addr::new(255, 255, 240, 0)),
42//! Network::V4(Ipv4Addr::new(130, 155, 0, 0), Ipv4Addr::new(255, 255, 0, 0)),
43//! ];
44//! expected_config.debug = false;
45//! expected_config.ndots = 8;
46//! expected_config.timeout = 8;
47//! expected_config.attempts = 8;
48//! expected_config.rotate = true;
49//! expected_config.no_check_names = false;
50//! expected_config.inet6 = true;
51//! expected_config.ip6_bytestring = false;
52//! expected_config.ip6_dotint = false;
53//! expected_config.edns0 = false;
54//! expected_config.single_request = false;
55//! expected_config.single_request_reopen = false;
56//! expected_config.no_tld_query = true;
57//! expected_config.use_vc = false;
58//! expected_config.set_domain(String::from("example.com"));
59//! expected_config.set_search(vec![
60//! String::from("example.com"),
61//! String::from("sub.example.com")
62//! ]);
63//!
64//! // We can compare configurations, since resolv_conf::Config implements Eq
65//! assert_eq!(parsed_config, expected_config);
66//! }
67//! ```
68//!
69//! ## Parsing a file
70//!
71//! ```rust
72//! use std::io::Read;
73//! use std::fs::File;
74//!
75//! extern crate resolv_conf;
76//!
77//! fn main() {
78//! // Read the file
79//! let mut buf = Vec::with_capacity(4096);
80//! let mut f = File::open("/etc/resolv.conf").unwrap();
81//! f.read_to_end(&mut buf).unwrap();
82//!
83//! // Parse the buffer
84//! let cfg = resolv_conf::Config::parse(&buf).unwrap();
85//!
86//! // Print the config
87//! println!("---- Parsed /etc/resolv.conf -----\n{:#?}\n", cfg);
88//! }
89//! ```
90
91#![warn(missing_debug_implementations, missing_docs, unreachable_pub)]
92#![warn(clippy::use_self)]
93
94use std::fmt;
95use std::iter::Iterator;
96use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
97use std::slice::Iter;
98use std::str::{self, from_utf8, FromStr, Utf8Error};
99
100mod ip;
101pub use ip::{AddrParseError, Network, ScopedIp};
102
103/// Represent a resolver configuration, as described in `man 5 resolv.conf`.
104/// The options and defaults match those in the linux `man` page.
105///
106/// Note: while most fields in the structure are public the `search` and
107/// `domain` fields must be accessed via methods. This is because there are
108/// few different ways to treat `domain` field. In GNU libc `search` and
109/// `domain` replace each other ([`get_last_search_or_domain`]).
110/// In MacOS `/etc/resolve/*` files `domain` is treated in entirely different
111/// way.
112///
113/// Also consider using [`glibc_normalize`] and [`get_system_domain`] to match
114/// behavior of GNU libc.
115///
116/// ```rust
117/// extern crate resolv_conf;
118///
119/// use std::net::Ipv4Addr;
120/// use resolv_conf::{Config, ScopedIp};
121///
122/// fn main() {
123/// // Create a new config
124/// let mut config = Config::new();
125/// config.nameservers.push(ScopedIp::V4(Ipv4Addr::new(8, 8, 8, 8)));
126/// config.set_search(vec!["example.com".into()]);
127///
128/// // Parse a config
129/// let parsed = Config::parse("nameserver 8.8.8.8\nsearch example.com").unwrap();
130/// assert_eq!(parsed, config);
131/// }
132/// ```
133///
134/// [`glibc_normalize`]: #method.glibc_normalize
135/// [`get_last_search_or_domain`]: #method.get_last_search_or_domain
136/// [`get_system_domain`]: #method.get_system_domain
137#[derive(Clone, Debug, PartialEq, Eq)]
138pub struct Config {
139 /// List of nameservers
140 pub nameservers: Vec<ScopedIp>,
141 /// Indicated whether the last line that has been parsed is a "domain" directive or a "search"
142 /// directive. This is important for compatibility with glibc, since in glibc's implementation,
143 /// "search" and "domain" are mutually exclusive, and only the last directive is taken into
144 /// consideration.
145 last_search: LastSearch,
146 /// Domain to append to name when it doesn't contain ndots
147 domain: Option<String>,
148 /// List of suffixes to append to name when it doesn't contain ndots
149 search: Option<Vec<String>>,
150 /// List of preferred addresses
151 pub sortlist: Vec<Network>,
152 /// Enable DNS resolve debugging
153 pub debug: bool,
154 /// Number of dots in name to try absolute resolving first (default 1)
155 pub ndots: u32,
156 /// Dns query timeout (default 5 [sec])
157 pub timeout: u32,
158 /// Number of attempts to resolve name if server is inaccesible (default 2)
159 pub attempts: u32,
160 /// Round-robin selection of servers (default false)
161 pub rotate: bool,
162 /// Don't check names for validity (default false)
163 pub no_check_names: bool,
164 /// Try AAAA query before A
165 pub inet6: bool,
166 /// Use reverse lookup of ipv6 using bit-label format described instead
167 /// of nibble format
168 pub ip6_bytestring: bool,
169 /// Do ipv6 reverse lookups in ip6.int zone instead of ip6.arpa
170 /// (default false)
171 pub ip6_dotint: bool,
172 /// Enable dns extensions described in RFC 2671
173 pub edns0: bool,
174 /// Don't make ipv4 and ipv6 requests simultaneously
175 pub single_request: bool,
176 /// Use same socket for the A and AAAA requests
177 pub single_request_reopen: bool,
178 /// Don't resolve unqualified name as top level domain
179 pub no_tld_query: bool,
180 /// Force using TCP for DNS resolution
181 pub use_vc: bool,
182 /// Disable the automatic reloading of a changed configuration file
183 pub no_reload: bool,
184 /// Optionally send the AD (authenticated data) bit in queries
185 pub trust_ad: bool,
186 /// The order in which databases should be searched during a lookup
187 /// **(openbsd-only)**
188 pub lookup: Vec<Lookup>,
189 /// The order in which internet protocol families should be prefered
190 /// **(openbsd-only)**
191 pub family: Vec<Family>,
192 /// Suppress AAAA queries made by the stub resolver
193 pub no_aaaa: bool,
194}
195
196impl Config {
197 /// Create a new `Config` object with default values.
198 ///
199 /// ```rust
200 /// # extern crate resolv_conf;
201 /// use resolv_conf::Config;
202 /// # fn main() {
203 /// let config = Config::new();
204 /// assert_eq!(config.nameservers, vec![]);
205 /// assert!(config.get_domain().is_none());
206 /// assert!(config.get_search().is_none());
207 /// assert_eq!(config.sortlist, vec![]);
208 /// assert_eq!(config.debug, false);
209 /// assert_eq!(config.ndots, 1);
210 /// assert_eq!(config.timeout, 5);
211 /// assert_eq!(config.attempts, 2);
212 /// assert_eq!(config.rotate, false);
213 /// assert_eq!(config.no_check_names, false);
214 /// assert_eq!(config.inet6, false);
215 /// assert_eq!(config.ip6_bytestring, false);
216 /// assert_eq!(config.ip6_dotint, false);
217 /// assert_eq!(config.edns0, false);
218 /// assert_eq!(config.single_request, false);
219 /// assert_eq!(config.single_request_reopen, false);
220 /// assert_eq!(config.no_tld_query, false);
221 /// assert_eq!(config.use_vc, false);
222 /// # }
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 /// Parse a buffer and return the corresponding `Config` object.
228 ///
229 /// ```rust
230 /// # extern crate resolv_conf;
231 /// use resolv_conf::{ScopedIp, Config};
232 /// # fn main() {
233 /// let config_str = "# /etc/resolv.conf
234 /// nameserver 8.8.8.8
235 /// nameserver 8.8.4.4
236 /// search example.com sub.example.com
237 /// options ndots:8 attempts:8";
238 ///
239 /// // Parse the config
240 /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
241 ///
242 /// // Print the config
243 /// println!("{:?}", parsed_config);
244 /// # }
245 /// ```
246 pub fn parse<T: AsRef<[u8]>>(buf: T) -> Result<Self, ParseError> {
247 let (new, mut errors) = Self::parse_with_errors(buf.as_ref());
248 let mut iter = errors.drain(..);
249 match iter.next() {
250 Some(err) => Err(err),
251 None => Ok(new),
252 }
253 }
254
255 /// Parse a buffer and return a best-effort parsed `Config` object along with any errors.
256 pub fn parse_with_errors(bytes: &[u8]) -> (Self, Vec<ParseError>) {
257 use ParseError::*;
258 let mut cfg = Self::new();
259 let mut errors = Vec::new();
260 'lines: for (lineno, line) in bytes.split(|&x| x == b'\n').enumerate() {
261 for &c in line.iter() {
262 if c != b'\t' && c != b' ' {
263 if c == b';' || c == b'#' {
264 continue 'lines;
265 } else {
266 break;
267 }
268 }
269 }
270
271 // All that dances above to allow invalid utf-8 inside the comments
272 let str = match from_utf8(line) {
273 Ok(str) => str,
274 Err(e) => {
275 errors.push(InvalidUtf8(lineno, e));
276 continue;
277 }
278 };
279
280 // ignore everything after ';' or '#'
281 let text = match str.split([';', '#']).next() {
282 Some(text) => text,
283 None => {
284 errors.push(InvalidValue(lineno));
285 continue;
286 }
287 };
288
289 let mut words = text.split_whitespace();
290 let keyword = match words.next() {
291 Some(x) => x,
292 None => continue,
293 };
294
295 match keyword {
296 "nameserver" => {
297 let srv = match words.next() {
298 Some(srv) => srv,
299 None => {
300 errors.push(InvalidValue(lineno));
301 continue;
302 }
303 };
304
305 match ScopedIp::from_str(srv) {
306 Ok(addr) => cfg.nameservers.push(addr),
307 Err(e) => errors.push(InvalidIp(lineno, e)),
308 }
309
310 if words.next().is_some() {
311 errors.push(ExtraData(lineno));
312 }
313 }
314 "domain" => {
315 let domain = match words.next() {
316 Some(domain) => domain,
317 None => {
318 errors.push(InvalidValue(lineno));
319 continue;
320 }
321 };
322
323 cfg.set_domain(domain.to_owned());
324 if words.next().is_some() {
325 errors.push(ExtraData(lineno));
326 }
327 }
328 "search" => {
329 let mut search = Vec::new();
330 for word in words {
331 if !word.contains(',') {
332 // Easy to get confused between spaces and commas here
333 // https://github.com/hickory-dns/resolv-conf/issues/58
334 search.push(word.to_owned());
335 }
336 }
337 cfg.set_search(search);
338 }
339 "sortlist" => {
340 cfg.sortlist.clear();
341 for pair in words {
342 match Network::from_str(pair) {
343 Ok(network) => cfg.sortlist.push(network),
344 Err(e) => errors.push(InvalidIp(lineno, e)),
345 }
346 }
347 }
348 "options" => {
349 for pair in words {
350 let mut iter = pair.splitn(2, ':');
351 let key = match iter.next() {
352 Some(key) => key,
353 None => {
354 errors.push(InvalidValue(lineno));
355 continue 'lines;
356 }
357 };
358
359 let value = iter.next();
360 if iter.next().is_some() {
361 errors.push(ExtraData(lineno));
362 continue 'lines;
363 }
364
365 match (key, value) {
366 // TODO(tailhook) ensure that values are None?
367 ("debug", _) => cfg.debug = true,
368 ("ndots", Some(x)) => match u32::from_str(x) {
369 Ok(ndots) => cfg.ndots = ndots,
370 Err(_) => errors.push(InvalidOptionValue(lineno)),
371 },
372 ("timeout", Some(x)) => match u32::from_str(x) {
373 Ok(timeout) => cfg.timeout = timeout,
374 Err(_) => errors.push(InvalidOptionValue(lineno)),
375 },
376 ("attempts", Some(x)) => match u32::from_str(x) {
377 Ok(attempts) => cfg.attempts = attempts,
378 Err(_) => errors.push(InvalidOptionValue(lineno)),
379 },
380 ("rotate", _) => cfg.rotate = true,
381 ("no-check-names", _) => cfg.no_check_names = true,
382 ("inet6", _) => cfg.inet6 = true,
383 ("ip6-bytestring", _) => cfg.ip6_bytestring = true,
384 ("ip6-dotint", _) => cfg.ip6_dotint = true,
385 ("no-ip6-dotint", _) => cfg.ip6_dotint = false,
386 ("edns0", _) => cfg.edns0 = true,
387 ("single-request", _) => cfg.single_request = true,
388 ("single-request-reopen", _) => cfg.single_request_reopen = true,
389 ("no-reload", _) => cfg.no_reload = true,
390 ("trust-ad", _) => cfg.trust_ad = true,
391 ("no-tld-query", _) => cfg.no_tld_query = true,
392 ("use-vc", _) => cfg.use_vc = true,
393 ("no-aaaa", _) => cfg.no_aaaa = true,
394 _ => errors.push(InvalidOption(lineno)),
395 }
396 }
397 }
398 "lookup" => {
399 for word in words {
400 match word {
401 "file" => cfg.lookup.push(Lookup::File),
402 "bind" => cfg.lookup.push(Lookup::Bind),
403 extra => cfg.lookup.push(Lookup::Extra(extra.to_string())),
404 }
405 }
406 }
407 "family" => {
408 for word in words {
409 match word {
410 "inet4" => cfg.family.push(Family::Inet4),
411 "inet6" => cfg.family.push(Family::Inet6),
412 _ => errors.push(InvalidValue(lineno)),
413 }
414 }
415 }
416 _ => errors.push(InvalidDirective(lineno)),
417 }
418 }
419
420 (cfg, errors)
421 }
422
423 /// Return the suffixes declared in the last "domain" or "search" directive.
424 ///
425 /// ```rust
426 /// # extern crate resolv_conf;
427 /// use resolv_conf::{ScopedIp, Config};
428 /// # fn main() {
429 /// let config_str = "search example.com sub.example.com\ndomain localdomain";
430 /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
431 /// let domains = parsed_config.get_last_search_or_domain()
432 /// .map(|domain| domain.clone())
433 /// .collect::<Vec<String>>();
434 /// assert_eq!(domains, vec![String::from("localdomain")]);
435 ///
436 /// let config_str = "domain localdomain\nsearch example.com sub.example.com";
437 /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
438 /// let domains = parsed_config.get_last_search_or_domain()
439 /// .map(|domain| domain.clone())
440 /// .collect::<Vec<String>>();
441 /// assert_eq!(domains, vec![String::from("example.com"), String::from("sub.example.com")]);
442 /// # }
443 pub fn get_last_search_or_domain(&self) -> DomainIter<'_> {
444 let domain_iter = match self.last_search {
445 LastSearch::Search => {
446 DomainIterInternal::Search(self.get_search().map(|domains| domains.iter()))
447 }
448 LastSearch::Domain => DomainIterInternal::Domain(self.get_domain()),
449 LastSearch::None => DomainIterInternal::None,
450 };
451 DomainIter(domain_iter)
452 }
453
454 /// Return the domain declared in the last "domain" directive.
455 pub fn get_domain(&self) -> Option<&String> {
456 self.domain.as_ref()
457 }
458
459 /// Return the domains declared in the last "search" directive.
460 pub fn get_search(&self) -> Option<&Vec<String>> {
461 self.search.as_ref()
462 }
463
464 /// Set the domain corresponding to the "domain" directive.
465 pub fn set_domain(&mut self, domain: String) {
466 self.domain = Some(domain);
467 self.last_search = LastSearch::Domain;
468 }
469
470 /// Set the domains corresponding the "search" directive.
471 pub fn set_search(&mut self, search: Vec<String>) {
472 self.search = Some(search);
473 self.last_search = LastSearch::Search;
474 }
475
476 /// Normalize config according to glibc rulees
477 ///
478 /// Currently this method does the following things:
479 ///
480 /// 1. Truncates list of nameservers to 3 at max
481 /// 2. Truncates search list to 6 at max
482 ///
483 /// Other normalizations may be added in future as long as they hold true
484 /// for a particular GNU libc implementation.
485 ///
486 /// Note: this method is not called after parsing, because we think it's
487 /// not forward-compatible to rely on such small and ugly limits. Still,
488 /// it's useful to keep implementation as close to glibc as possible.
489 pub fn glibc_normalize(&mut self) {
490 self.nameservers.truncate(NAMESERVER_LIMIT);
491 self.search = self.search.take().map(|mut s| {
492 s.truncate(SEARCH_LIMIT);
493 s
494 });
495 }
496
497 /// Get nameserver or on the local machine
498 pub fn get_nameservers_or_local(&self) -> Vec<ScopedIp> {
499 if self.nameservers.is_empty() {
500 vec![
501 ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
502 ScopedIp::from(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))),
503 ]
504 } else {
505 self.nameservers.to_vec()
506 }
507 }
508
509 /// Get domain from config or fallback to the suffix of a hostname
510 ///
511 /// This is how glibc finds out a hostname.
512 pub fn get_system_domain(&self) -> Option<String> {
513 if self.domain.is_some() {
514 return self.domain.clone();
515 }
516
517 // This buffer is far larger than what most systems will ever allow, eg.
518 // linux uses 64 via _SC_HOST_NAME_MAX even though POSIX says the size
519 // must be _at least_ _POSIX_HOST_NAME_MAX (255), but other systems can
520 // be larger, so we just use a sufficiently sized buffer so we can defer
521 // a heap allocation until the last possible moment.
522 let mut hostname = [0u8; 1024];
523
524 #[cfg(all(target_os = "linux", target_feature = "crt-static"))]
525 {
526 use std::{fs::File, io::Read};
527 let mut file = File::open("/proc/sys/kernel/hostname").ok()?;
528 let read_bytes = file.read(&mut hostname).ok()?;
529
530 // According to Linux kernel's proc_dostring handler, user-space reads
531 // of /proc/sys entries which have a string value are terminated by
532 // a newline character. While libc gethostname() terminates the hostname
533 // with a null character. Hence, to match the behavior of gethostname()
534 // it is necessary to replace the newline with a null character.
535 if read_bytes == hostname.len() && hostname[read_bytes - 1] != b'\n' {
536 // In this case the string read from /proc/sys/kernel/hostname is
537 // truncated and cannot be terminated by a null character
538 return None;
539 }
540 // Since any non-truncated string read from /proc/sys/kernel/hostname
541 // ends with a newline character, read_bytes > 0.
542 hostname[read_bytes - 1] = 0;
543 }
544
545 #[cfg(not(all(target_os = "linux", target_feature = "crt-static")))]
546 {
547 /*unsafe*/
548 extern "C" {
549 fn gethostname(hostname: *mut u8, size: usize) -> i32;
550 }
551
552 unsafe {
553 if gethostname(hostname.as_mut_ptr(), hostname.len()) < 0 {
554 return None;
555 }
556 }
557 }
558
559 domain_from_host(&hostname).map(|s| s.to_owned())
560 }
561}
562
563impl Default for Config {
564 fn default() -> Self {
565 Self {
566 nameservers: Vec::new(),
567 domain: None,
568 search: None,
569 last_search: LastSearch::None,
570 sortlist: Vec::new(),
571 debug: false,
572 ndots: 1,
573 timeout: 5,
574 attempts: 2,
575 rotate: false,
576 no_check_names: false,
577 inet6: false,
578 ip6_bytestring: false,
579 ip6_dotint: false,
580 edns0: false,
581 single_request: false,
582 single_request_reopen: false,
583 no_tld_query: false,
584 use_vc: false,
585 no_reload: false,
586 trust_ad: false,
587 lookup: Vec::new(),
588 family: Vec::new(),
589 no_aaaa: false,
590 }
591 }
592}
593
594impl fmt::Display for Config {
595 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
596 let Self {
597 nameservers,
598 last_search,
599 domain,
600 search,
601 sortlist,
602 debug,
603 ndots,
604 timeout,
605 attempts,
606 rotate,
607 no_check_names,
608 inet6,
609 ip6_bytestring,
610 ip6_dotint,
611 edns0,
612 single_request,
613 single_request_reopen,
614 no_tld_query,
615 use_vc,
616 no_reload,
617 trust_ad,
618 lookup,
619 family,
620 no_aaaa,
621 } = self;
622
623 for nameserver in nameservers.iter() {
624 writeln!(fmt, "nameserver {nameserver}")?;
625 }
626
627 if last_search != &LastSearch::Domain {
628 if let Some(domain) = domain {
629 writeln!(fmt, "domain {domain}")?;
630 }
631 }
632
633 if let Some(search) = search {
634 if !search.is_empty() {
635 write!(fmt, "search")?;
636 for suffix in search.iter() {
637 write!(fmt, " {suffix}")?;
638 }
639 writeln!(fmt)?;
640 }
641 }
642
643 if last_search == &LastSearch::Domain {
644 if let Some(domain) = &self.domain {
645 writeln!(fmt, "domain {domain}")?;
646 }
647 }
648
649 if !sortlist.is_empty() {
650 write!(fmt, "sortlist")?;
651 for network in sortlist.iter() {
652 write!(fmt, " {network}")?;
653 }
654 writeln!(fmt)?;
655 }
656
657 if !lookup.is_empty() {
658 write!(fmt, "lookup")?;
659 for db in lookup.iter() {
660 match db {
661 Lookup::File => write!(fmt, " file")?,
662 Lookup::Bind => write!(fmt, " bind")?,
663 Lookup::Extra(extra) => write!(fmt, " {extra}")?,
664 }
665 }
666 writeln!(fmt)?;
667 }
668
669 if !family.is_empty() {
670 write!(fmt, "family")?;
671 for fam in family.iter() {
672 match fam {
673 Family::Inet4 => write!(fmt, " inet4")?,
674 Family::Inet6 => write!(fmt, " inet6")?,
675 }
676 }
677 writeln!(fmt)?;
678 }
679
680 if *debug {
681 writeln!(fmt, "options debug")?;
682 }
683 if *ndots != 1 {
684 writeln!(fmt, "options ndots:{}", self.ndots)?;
685 }
686 if *timeout != 5 {
687 writeln!(fmt, "options timeout:{}", self.timeout)?;
688 }
689 if *attempts != 2 {
690 writeln!(fmt, "options attempts:{}", self.attempts)?;
691 }
692 if *rotate {
693 writeln!(fmt, "options rotate")?;
694 }
695 if *no_check_names {
696 writeln!(fmt, "options no-check-names")?;
697 }
698 if *inet6 {
699 writeln!(fmt, "options inet6")?;
700 }
701 if *ip6_bytestring {
702 writeln!(fmt, "options ip6-bytestring")?;
703 }
704 if *ip6_dotint {
705 writeln!(fmt, "options ip6-dotint")?;
706 }
707 if *edns0 {
708 writeln!(fmt, "options edns0")?;
709 }
710 if *single_request {
711 writeln!(fmt, "options single-request")?;
712 }
713 if *single_request_reopen {
714 writeln!(fmt, "options single-request-reopen")?;
715 }
716 if *no_tld_query {
717 writeln!(fmt, "options no-tld-query")?;
718 }
719 if *use_vc {
720 writeln!(fmt, "options use-vc")?;
721 }
722 if *no_reload {
723 writeln!(fmt, "options no-reload")?;
724 }
725 if *trust_ad {
726 writeln!(fmt, "options trust-ad")?;
727 }
728 if *no_aaaa {
729 writeln!(fmt, "options no-aaaa")?;
730 }
731
732 Ok(())
733 }
734}
735
736/// Error while parsing resolv.conf file
737#[derive(Debug)]
738pub enum ParseError {
739 /// Error that may be returned when the string to parse contains invalid UTF-8 sequences
740 InvalidUtf8(usize, Utf8Error),
741 /// Error returned a value for a given directive is invalid.
742 /// This can also happen when the value is missing, if the directive requires a value.
743 InvalidValue(usize),
744 /// Error returned when a value for a given option is invalid.
745 /// This can also happen when the value is missing, if the option requires a value.
746 InvalidOptionValue(usize),
747 /// Error returned when a invalid option is found.
748 InvalidOption(usize),
749 /// Error returned when a invalid directive is found.
750 InvalidDirective(usize),
751 /// Error returned when a value cannot be parsed an an IP address.
752 InvalidIp(usize, AddrParseError),
753 /// Error returned when there is extra data at the end of a line.
754 ExtraData(usize),
755}
756
757impl std::fmt::Display for ParseError {
758 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
759 match self {
760 Self::InvalidUtf8(line, err) => write!(f, "bad unicode at line {line}: {err}"),
761 Self::InvalidValue(line) => write!(
762 f,
763 "directive at line {line} is improperly formatted or contains invalid value",
764 ),
765 Self::InvalidOptionValue(line) => write!(
766 f,
767 "directive options at line {line} contains invalid value of some option",
768 ),
769 Self::InvalidOption(line) => {
770 write!(f, "option at line {line} is not recognized")
771 }
772 Self::InvalidDirective(line) => {
773 write!(f, "directive at line {line} is not recognized")
774 }
775 Self::InvalidIp(line, err) => {
776 write!(f, "directive at line {line} contains invalid IP: {err}")
777 }
778 Self::ExtraData(line) => write!(f, "extra data at the end of line {line}"),
779 }
780 }
781}
782
783impl std::error::Error for ParseError {
784 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
785 match self {
786 Self::InvalidUtf8(_, err) => Some(err),
787 _ => None,
788 }
789 }
790}
791
792/// An iterator returned by [`Config.get_last_search_or_domain`](struct.Config.html#method.get_last_search_or_domain)
793#[derive(Debug, Clone)]
794pub struct DomainIter<'a>(DomainIterInternal<'a>);
795
796impl<'a> Iterator for DomainIter<'a> {
797 type Item = &'a String;
798
799 fn next(&mut self) -> Option<Self::Item> {
800 self.0.next()
801 }
802}
803
804#[derive(Debug, Clone)]
805enum DomainIterInternal<'a> {
806 Search(Option<Iter<'a, String>>),
807 Domain(Option<&'a String>),
808 None,
809}
810
811impl<'a> Iterator for DomainIterInternal<'a> {
812 type Item = &'a String;
813
814 fn next(&mut self) -> Option<Self::Item> {
815 match self {
816 DomainIterInternal::Search(Some(domains)) => domains.next(),
817 DomainIterInternal::Domain(domain) => domain.take(),
818 _ => None,
819 }
820 }
821}
822
823/// The databases that should be searched during a lookup.
824/// This option is commonly found on openbsd.
825#[derive(Clone, Debug, PartialEq, Eq)]
826pub enum Lookup {
827 /// Search for entries in /etc/hosts
828 File,
829 /// Query a domain name server
830 Bind,
831 /// A database we don't know yet
832 Extra(String),
833}
834
835/// The internet protocol family that is prefered.
836/// This option is commonly found on openbsd.
837#[derive(Clone, Debug, PartialEq, Eq)]
838pub enum Family {
839 /// A A lookup for an ipv4 address
840 Inet4,
841 /// A AAAA lookup for an ipv6 address
842 Inet6,
843}
844
845/// Parses the domain name from a hostname, if available
846fn domain_from_host(hostname: &[u8]) -> Option<&str> {
847 let mut start = None;
848 for (i, b) in hostname.iter().copied().enumerate() {
849 if b == b'.' && start.is_none() {
850 start = Some(i);
851 continue;
852 } else if b > 0 {
853 continue;
854 }
855
856 return match start? {
857 // Avoid empty domains
858 start if i - start < 2 => None,
859 start => str::from_utf8(&hostname[start + 1..i]).ok(),
860 };
861 }
862
863 None
864}
865
866#[derive(Copy, Clone, PartialEq, Eq, Debug)]
867enum LastSearch {
868 None,
869 Domain,
870 Search,
871}
872
873const NAMESERVER_LIMIT: usize = 3;
874const SEARCH_LIMIT: usize = 6;
875
876#[cfg(test)]
877mod test {
878 use super::domain_from_host;
879 use super::Config;
880
881 #[test]
882 fn parses_domain_name() {
883 assert!(domain_from_host(b"regular-hostname\0").is_none());
884
885 assert_eq!(domain_from_host(b"with.domain-name\0"), Some("domain-name"));
886 assert_eq!(
887 domain_from_host(b"with.multiple.dots\0"),
888 Some("multiple.dots")
889 );
890
891 assert!(domain_from_host(b"hostname.\0").is_none());
892 assert_eq!(domain_from_host(b"host.a\0"), Some("a"));
893 assert_eq!(domain_from_host(b"host.au\0"), Some("au"));
894 }
895
896 #[test]
897 fn comma_search() {
898 let config = Config::parse(COMMA_SEARCH).unwrap();
899 assert_eq!(
900 config.get_search().unwrap(),
901 &["another.example.com".to_string(),]
902 );
903 }
904
905 const COMMA_SEARCH: &str = r#"search example.com,sub.example.com another.example.com"#;
906}