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}