resolv_conf/
config.rs

1use std::iter::Iterator;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3use std::slice::Iter;
4use std::{fmt, str};
5
6use crate::{grammar, Network, ParseError, ScopedIp};
7
8const NAMESERVER_LIMIT: usize = 3;
9const SEARCH_LIMIT: usize = 6;
10
11#[derive(Copy, Clone, PartialEq, Eq, Debug)]
12enum LastSearch {
13    None,
14    Domain,
15    Search,
16}
17
18/// Represent a resolver configuration, as described in `man 5 resolv.conf`.
19/// The options and defaults match those in the linux `man` page.
20///
21/// Note: while most fields in the structure are public the `search` and
22/// `domain` fields must be accessed via methods. This is because there are
23/// few different ways to treat `domain` field. In GNU libc `search` and
24/// `domain` replace each other ([`get_last_search_or_domain`]).
25/// In MacOS `/etc/resolve/*` files `domain` is treated in entirely different
26/// way.
27///
28/// Also consider using [`glibc_normalize`] and [`get_system_domain`] to match
29/// behavior of GNU libc.
30///
31/// ```rust
32/// extern crate resolv_conf;
33///
34/// use std::net::Ipv4Addr;
35/// use resolv_conf::{Config, ScopedIp};
36///
37/// fn main() {
38///     // Create a new config
39///     let mut config = Config::new();
40///     config.nameservers.push(ScopedIp::V4(Ipv4Addr::new(8, 8, 8, 8)));
41///     config.set_search(vec!["example.com".into()]);
42///
43///     // Parse a config
44///     let parsed = Config::parse("nameserver 8.8.8.8\nsearch example.com").unwrap();
45///     assert_eq!(parsed, config);
46/// }
47/// ```
48///
49/// [`glibc_normalize`]: #method.glibc_normalize
50/// [`get_last_search_or_domain`]: #method.get_last_search_or_domain
51/// [`get_system_domain`]: #method.get_system_domain
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Config {
54    /// List of nameservers
55    pub nameservers: Vec<ScopedIp>,
56    /// Indicated whether the last line that has been parsed is a "domain" directive or a "search"
57    /// directive. This is important for compatibility with glibc, since in glibc's implementation,
58    /// "search" and "domain" are mutually exclusive, and only the last directive is taken into
59    /// consideration.
60    last_search: LastSearch,
61    /// Domain to append to name when it doesn't contain ndots
62    domain: Option<String>,
63    /// List of suffixes to append to name when it doesn't contain ndots
64    search: Option<Vec<String>>,
65    /// List of preferred addresses
66    pub sortlist: Vec<Network>,
67    /// Enable DNS resolve debugging
68    pub debug: bool,
69    /// Number of dots in name to try absolute resolving first (default 1)
70    pub ndots: u32,
71    /// Dns query timeout (default 5 [sec])
72    pub timeout: u32,
73    /// Number of attempts to resolve name if server is inaccesible (default 2)
74    pub attempts: u32,
75    /// Round-robin selection of servers (default false)
76    pub rotate: bool,
77    /// Don't check names for validity (default false)
78    pub no_check_names: bool,
79    /// Try AAAA query before A
80    pub inet6: bool,
81    /// Use reverse lookup of ipv6 using bit-label format described instead
82    /// of nibble format
83    pub ip6_bytestring: bool,
84    /// Do ipv6 reverse lookups in ip6.int zone instead of ip6.arpa
85    /// (default false)
86    pub ip6_dotint: bool,
87    /// Enable dns extensions described in RFC 2671
88    pub edns0: bool,
89    /// Don't make ipv4 and ipv6 requests simultaneously
90    pub single_request: bool,
91    /// Use same socket for the A and AAAA requests
92    pub single_request_reopen: bool,
93    /// Don't resolve unqualified name as top level domain
94    pub no_tld_query: bool,
95    /// Force using TCP for DNS resolution
96    pub use_vc: bool,
97    /// Disable the automatic reloading of a changed configuration file
98    pub no_reload: bool,
99    /// Optionally send the AD (authenticated data) bit in queries
100    pub trust_ad: bool,
101    /// The order in which databases should be searched during a lookup
102    /// **(openbsd-only)**
103    pub lookup: Vec<Lookup>,
104    /// The order in which internet protocol families should be prefered
105    /// **(openbsd-only)**
106    pub family: Vec<Family>,
107}
108
109impl Default for Config {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl Config {
116    /// Create a new `Config` object with default values.
117    ///
118    /// ```rust
119    /// # extern crate resolv_conf;
120    /// use resolv_conf::Config;
121    /// # fn main() {
122    /// let config = Config::new();
123    /// assert_eq!(config.nameservers, vec![]);
124    /// assert!(config.get_domain().is_none());
125    /// assert!(config.get_search().is_none());
126    /// assert_eq!(config.sortlist, vec![]);
127    /// assert_eq!(config.debug, false);
128    /// assert_eq!(config.ndots, 1);
129    /// assert_eq!(config.timeout, 5);
130    /// assert_eq!(config.attempts, 2);
131    /// assert_eq!(config.rotate, false);
132    /// assert_eq!(config.no_check_names, false);
133    /// assert_eq!(config.inet6, false);
134    /// assert_eq!(config.ip6_bytestring, false);
135    /// assert_eq!(config.ip6_dotint, false);
136    /// assert_eq!(config.edns0, false);
137    /// assert_eq!(config.single_request, false);
138    /// assert_eq!(config.single_request_reopen, false);
139    /// assert_eq!(config.no_tld_query, false);
140    /// assert_eq!(config.use_vc, false);
141    /// # }
142    pub fn new() -> Config {
143        Config {
144            nameservers: Vec::new(),
145            domain: None,
146            search: None,
147            last_search: LastSearch::None,
148            sortlist: Vec::new(),
149            debug: false,
150            ndots: 1,
151            timeout: 5,
152            attempts: 2,
153            rotate: false,
154            no_check_names: false,
155            inet6: false,
156            ip6_bytestring: false,
157            ip6_dotint: false,
158            edns0: false,
159            single_request: false,
160            single_request_reopen: false,
161            no_tld_query: false,
162            use_vc: false,
163            no_reload: false,
164            trust_ad: false,
165            lookup: Vec::new(),
166            family: Vec::new(),
167        }
168    }
169
170    /// Parse a buffer and return the corresponding `Config` object.
171    ///
172    /// ```rust
173    /// # extern crate resolv_conf;
174    /// use resolv_conf::{ScopedIp, Config};
175    /// # fn main() {
176    /// let config_str = "# /etc/resolv.conf
177    /// nameserver  8.8.8.8
178    /// nameserver  8.8.4.4
179    /// search      example.com sub.example.com
180    /// options     ndots:8 attempts:8";
181    ///
182    /// // Parse the config
183    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
184    ///
185    /// // Print the config
186    /// println!("{:?}", parsed_config);
187    /// # }
188    /// ```
189    pub fn parse<T: AsRef<[u8]>>(buf: T) -> Result<Config, ParseError> {
190        grammar::parse(buf.as_ref())
191    }
192
193    /// Return the suffixes declared in the last "domain" or "search" directive.
194    ///
195    /// ```rust
196    /// # extern crate resolv_conf;
197    /// use resolv_conf::{ScopedIp, Config};
198    /// # fn main() {
199    /// let config_str = "search example.com sub.example.com\ndomain localdomain";
200    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
201    /// let domains = parsed_config.get_last_search_or_domain()
202    ///                            .map(|domain| domain.clone())
203    ///                            .collect::<Vec<String>>();
204    /// assert_eq!(domains, vec![String::from("localdomain")]);
205    ///
206    /// let config_str = "domain localdomain\nsearch example.com sub.example.com";
207    /// let parsed_config = Config::parse(&config_str).expect("Failed to parse config");
208    /// let domains = parsed_config.get_last_search_or_domain()
209    ///                            .map(|domain| domain.clone())
210    ///                            .collect::<Vec<String>>();
211    /// assert_eq!(domains, vec![String::from("example.com"), String::from("sub.example.com")]);
212    /// # }
213    pub fn get_last_search_or_domain(&self) -> DomainIter<'_> {
214        let domain_iter = match self.last_search {
215            LastSearch::Search => {
216                DomainIterInternal::Search(self.get_search().map(|domains| domains.iter()))
217            }
218            LastSearch::Domain => DomainIterInternal::Domain(self.get_domain()),
219            LastSearch::None => DomainIterInternal::None,
220        };
221        DomainIter(domain_iter)
222    }
223
224    /// Return the domain declared in the last "domain" directive.
225    pub fn get_domain(&self) -> Option<&String> {
226        self.domain.as_ref()
227    }
228
229    /// Return the domains declared in the last "search" directive.
230    pub fn get_search(&self) -> Option<&Vec<String>> {
231        self.search.as_ref()
232    }
233
234    /// Set the domain corresponding to the "domain" directive.
235    pub fn set_domain(&mut self, domain: String) {
236        self.domain = Some(domain);
237        self.last_search = LastSearch::Domain;
238    }
239
240    /// Set the domains corresponding the "search" directive.
241    pub fn set_search(&mut self, search: Vec<String>) {
242        self.search = Some(search);
243        self.last_search = LastSearch::Search;
244    }
245
246    /// Normalize config according to glibc rulees
247    ///
248    /// Currently this method does the following things:
249    ///
250    /// 1. Truncates list of nameservers to 3 at max
251    /// 2. Truncates search list to 6 at max
252    ///
253    /// Other normalizations may be added in future as long as they hold true
254    /// for a particular GNU libc implementation.
255    ///
256    /// Note: this method is not called after parsing, because we think it's
257    /// not forward-compatible to rely on such small and ugly limits. Still,
258    /// it's useful to keep implementation as close to glibc as possible.
259    pub fn glibc_normalize(&mut self) {
260        self.nameservers.truncate(NAMESERVER_LIMIT);
261        self.search = self.search.take().map(|mut s| {
262            s.truncate(SEARCH_LIMIT);
263            s
264        });
265    }
266
267    /// Get nameserver or on the local machine
268    pub fn get_nameservers_or_local(&self) -> Vec<ScopedIp> {
269        if self.nameservers.is_empty() {
270            vec![
271                ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
272                ScopedIp::from(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))),
273            ]
274        } else {
275            self.nameservers.to_vec()
276        }
277    }
278
279    /// Get domain from config or fallback to the suffix of a hostname
280    ///
281    /// This is how glibc finds out a hostname.
282    pub fn get_system_domain(&self) -> Option<String> {
283        if self.domain.is_some() {
284            return self.domain.clone();
285        }
286
287        // This buffer is far larger than what most systems will ever allow, eg.
288        // linux uses 64 via _SC_HOST_NAME_MAX even though POSIX says the size
289        // must be _at least_ _POSIX_HOST_NAME_MAX (255), but other systems can
290        // be larger, so we just use a sufficiently sized buffer so we can defer
291        // a heap allocation until the last possible moment.
292        let mut hostname = [0u8; 1024];
293
294        #[cfg(all(target_os = "linux", target_feature = "crt-static"))]
295        {
296            use std::{fs::File, io::Read};
297            let mut file = File::open("/proc/sys/kernel/hostname").ok()?;
298            let read_bytes = file.read(&mut hostname).ok()?;
299
300            // According to Linux kernel's proc_dostring handler, user-space reads
301            // of /proc/sys entries which have a string value are terminated by
302            // a newline character. While libc gethostname() terminates the hostname
303            // with a null character. Hence, to match the behavior of gethostname()
304            // it is necessary to replace the newline with a null character.
305            if read_bytes == hostname.len() && hostname[read_bytes - 1] != b'\n' {
306                // In this case the string read from /proc/sys/kernel/hostname is
307                // truncated and cannot be terminated by a null character
308                return None;
309            }
310            // Since any non-truncated string read from /proc/sys/kernel/hostname
311            // ends with a newline character, read_bytes > 0.
312            hostname[read_bytes - 1] = 0;
313        }
314
315        #[cfg(not(all(target_os = "linux", target_feature = "crt-static")))]
316        {
317            #[link(name = "c")]
318            /*unsafe*/
319            extern "C" {
320                fn gethostname(hostname: *mut u8, size: usize) -> i32;
321            }
322
323            unsafe {
324                if gethostname(hostname.as_mut_ptr(), hostname.len()) < 0 {
325                    return None;
326                }
327            }
328        }
329
330        domain_from_host(&hostname).map(|s| s.to_owned())
331    }
332}
333
334impl fmt::Display for Config {
335    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
336        for nameserver in self.nameservers.iter() {
337            writeln!(fmt, "nameserver {nameserver}")?;
338        }
339
340        if self.last_search != LastSearch::Domain {
341            if let Some(domain) = &self.domain {
342                writeln!(fmt, "domain {domain}")?;
343            }
344        }
345
346        if let Some(search) = &self.search {
347            if !search.is_empty() {
348                write!(fmt, "search")?;
349                for suffix in search.iter() {
350                    write!(fmt, " {suffix}")?;
351                }
352                writeln!(fmt)?;
353            }
354        }
355
356        if self.last_search == LastSearch::Domain {
357            if let Some(domain) = &self.domain {
358                writeln!(fmt, "domain {domain}")?;
359            }
360        }
361
362        if !self.sortlist.is_empty() {
363            write!(fmt, "sortlist")?;
364            for network in self.sortlist.iter() {
365                write!(fmt, " {network}")?;
366            }
367            writeln!(fmt)?;
368        }
369
370        if self.debug {
371            writeln!(fmt, "options debug")?;
372        }
373        if self.ndots != 1 {
374            writeln!(fmt, "options ndots:{}", self.ndots)?;
375        }
376        if self.timeout != 5 {
377            writeln!(fmt, "options timeout:{}", self.timeout)?;
378        }
379        if self.attempts != 2 {
380            writeln!(fmt, "options attempts:{}", self.attempts)?;
381        }
382        if self.rotate {
383            writeln!(fmt, "options rotate")?;
384        }
385        if self.no_check_names {
386            writeln!(fmt, "options no-check-names")?;
387        }
388        if self.inet6 {
389            writeln!(fmt, "options inet6")?;
390        }
391        if self.ip6_bytestring {
392            writeln!(fmt, "options ip6-bytestring")?;
393        }
394        if self.ip6_dotint {
395            writeln!(fmt, "options ip6-dotint")?;
396        }
397        if self.edns0 {
398            writeln!(fmt, "options edns0")?;
399        }
400        if self.single_request {
401            writeln!(fmt, "options single-request")?;
402        }
403        if self.single_request_reopen {
404            writeln!(fmt, "options single-request-reopen")?;
405        }
406        if self.no_tld_query {
407            writeln!(fmt, "options no-tld-query")?;
408        }
409        if self.use_vc {
410            writeln!(fmt, "options use-vc")?;
411        }
412        if self.no_reload {
413            writeln!(fmt, "options no-reload")?;
414        }
415        if self.trust_ad {
416            writeln!(fmt, "options trust-ad")?;
417        }
418
419        Ok(())
420    }
421}
422
423/// An iterator returned by [`Config.get_last_search_or_domain`](struct.Config.html#method.get_last_search_or_domain)
424#[derive(Debug, Clone)]
425pub struct DomainIter<'a>(DomainIterInternal<'a>);
426
427impl<'a> Iterator for DomainIter<'a> {
428    type Item = &'a String;
429
430    fn next(&mut self) -> Option<Self::Item> {
431        self.0.next()
432    }
433}
434
435#[derive(Debug, Clone)]
436enum DomainIterInternal<'a> {
437    Search(Option<Iter<'a, String>>),
438    Domain(Option<&'a String>),
439    None,
440}
441
442impl<'a> Iterator for DomainIterInternal<'a> {
443    type Item = &'a String;
444
445    fn next(&mut self) -> Option<Self::Item> {
446        match self {
447            DomainIterInternal::Search(Some(domains)) => domains.next(),
448            DomainIterInternal::Domain(domain) => domain.take(),
449            _ => None,
450        }
451    }
452}
453
454/// The databases that should be searched during a lookup.
455/// This option is commonly found on openbsd.
456#[derive(Clone, Debug, PartialEq, Eq)]
457pub enum Lookup {
458    /// Search for entries in /etc/hosts
459    File,
460    /// Query a domain name server
461    Bind,
462    /// A database we don't know yet
463    Extra(String),
464}
465
466/// The internet protocol family that is prefered.
467/// This option is commonly found on openbsd.
468#[derive(Clone, Debug, PartialEq, Eq)]
469pub enum Family {
470    /// A A lookup for an ipv4 address
471    Inet4,
472    /// A AAAA lookup for an ipv6 address
473    Inet6,
474}
475
476/// Parses the domain name from a hostname, if available
477fn domain_from_host(hostname: &[u8]) -> Option<&str> {
478    let mut start = None;
479    for (i, b) in hostname.iter().copied().enumerate() {
480        if b == b'.' && start.is_none() {
481            start = Some(i);
482            continue;
483        } else if b > 0 {
484            continue;
485        }
486
487        return match start? {
488            // Avoid empty domains
489            start if i - start < 2 => None,
490            start => str::from_utf8(&hostname[start + 1..i]).ok(),
491        };
492    }
493
494    None
495}
496
497#[cfg(test)]
498mod test {
499    use super::domain_from_host;
500    #[test]
501    fn parses_domain_name() {
502        assert!(domain_from_host(b"regular-hostname\0").is_none());
503
504        assert_eq!(domain_from_host(b"with.domain-name\0"), Some("domain-name"));
505        assert_eq!(
506            domain_from_host(b"with.multiple.dots\0"),
507            Some("multiple.dots")
508        );
509
510        assert!(domain_from_host(b"hostname.\0").is_none());
511        assert_eq!(domain_from_host(b"host.a\0"), Some("a"));
512        assert_eq!(domain_from_host(b"host.au\0"), Some("au"));
513    }
514}