resolv_conf/config.rs
1use std::iter::Iterator;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3use std::slice::Iter;
4use std::{fmt, mem, 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. (latter requires ``system`` feature enabled)
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 #[link(name = "c")]
288 /*unsafe*/
289 extern "C" {
290 fn gethostname(hostname: *mut u8, size: usize) -> i32;
291 }
292
293 // This buffer is far larger than what most systems will ever allow, eg.
294 // linux uses 64 via _SC_HOST_NAME_MAX even though POSIX says the size
295 // must be _at least_ _POSIX_HOST_NAME_MAX (255), but other systems can
296 // be larger, so we just use a sufficiently sized buffer so we can defer
297 // a heap allocation until the last possible moment.
298 let mut hostname = [0u8; 1024];
299 unsafe {
300 if gethostname(hostname.as_mut_ptr(), mem::size_of_val(&hostname)) < 0 {
301 return None;
302 }
303 }
304
305 domain_from_host(&hostname).map(|s| s.to_owned())
306 }
307}
308
309impl fmt::Display for Config {
310 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
311 for nameserver in self.nameservers.iter() {
312 writeln!(fmt, "nameserver {nameserver}")?;
313 }
314
315 if self.last_search != LastSearch::Domain {
316 if let Some(domain) = &self.domain {
317 writeln!(fmt, "domain {domain}")?;
318 }
319 }
320
321 if let Some(search) = &self.search {
322 if !search.is_empty() {
323 write!(fmt, "search")?;
324 for suffix in search.iter() {
325 write!(fmt, " {suffix}")?;
326 }
327 writeln!(fmt)?;
328 }
329 }
330
331 if self.last_search == LastSearch::Domain {
332 if let Some(domain) = &self.domain {
333 writeln!(fmt, "domain {domain}")?;
334 }
335 }
336
337 if !self.sortlist.is_empty() {
338 write!(fmt, "sortlist")?;
339 for network in self.sortlist.iter() {
340 write!(fmt, " {network}")?;
341 }
342 writeln!(fmt)?;
343 }
344
345 if self.debug {
346 writeln!(fmt, "options debug")?;
347 }
348 if self.ndots != 1 {
349 writeln!(fmt, "options ndots:{}", self.ndots)?;
350 }
351 if self.timeout != 5 {
352 writeln!(fmt, "options timeout:{}", self.timeout)?;
353 }
354 if self.attempts != 2 {
355 writeln!(fmt, "options attempts:{}", self.attempts)?;
356 }
357 if self.rotate {
358 writeln!(fmt, "options rotate")?;
359 }
360 if self.no_check_names {
361 writeln!(fmt, "options no-check-names")?;
362 }
363 if self.inet6 {
364 writeln!(fmt, "options inet6")?;
365 }
366 if self.ip6_bytestring {
367 writeln!(fmt, "options ip6-bytestring")?;
368 }
369 if self.ip6_dotint {
370 writeln!(fmt, "options ip6-dotint")?;
371 }
372 if self.edns0 {
373 writeln!(fmt, "options edns0")?;
374 }
375 if self.single_request {
376 writeln!(fmt, "options single-request")?;
377 }
378 if self.single_request_reopen {
379 writeln!(fmt, "options single-request-reopen")?;
380 }
381 if self.no_tld_query {
382 writeln!(fmt, "options no-tld-query")?;
383 }
384 if self.use_vc {
385 writeln!(fmt, "options use-vc")?;
386 }
387 if self.no_reload {
388 writeln!(fmt, "options no-reload")?;
389 }
390 if self.trust_ad {
391 writeln!(fmt, "options trust-ad")?;
392 }
393
394 Ok(())
395 }
396}
397
398/// An iterator returned by [`Config.get_last_search_or_domain`](struct.Config.html#method.get_last_search_or_domain)
399#[derive(Debug, Clone)]
400pub struct DomainIter<'a>(DomainIterInternal<'a>);
401
402impl<'a> Iterator for DomainIter<'a> {
403 type Item = &'a String;
404
405 fn next(&mut self) -> Option<Self::Item> {
406 self.0.next()
407 }
408}
409
410#[derive(Debug, Clone)]
411enum DomainIterInternal<'a> {
412 Search(Option<Iter<'a, String>>),
413 Domain(Option<&'a String>),
414 None,
415}
416
417impl<'a> Iterator for DomainIterInternal<'a> {
418 type Item = &'a String;
419
420 fn next(&mut self) -> Option<Self::Item> {
421 match self {
422 DomainIterInternal::Search(Some(domains)) => domains.next(),
423 DomainIterInternal::Domain(domain) => domain.take(),
424 _ => None,
425 }
426 }
427}
428
429/// The databases that should be searched during a lookup.
430/// This option is commonly found on openbsd.
431#[derive(Clone, Debug, PartialEq, Eq)]
432pub enum Lookup {
433 /// Search for entries in /etc/hosts
434 File,
435 /// Query a domain name server
436 Bind,
437 /// A database we don't know yet
438 Extra(String),
439}
440
441/// The internet protocol family that is prefered.
442/// This option is commonly found on openbsd.
443#[derive(Clone, Debug, PartialEq, Eq)]
444pub enum Family {
445 /// A A lookup for an ipv4 address
446 Inet4,
447 /// A AAAA lookup for an ipv6 address
448 Inet6,
449}
450
451/// Parses the domain name from a hostname, if available
452fn domain_from_host(hostname: &[u8]) -> Option<&str> {
453 let mut start = None;
454 for (i, b) in hostname.iter().copied().enumerate() {
455 if b == b'.' && start.is_none() {
456 start = Some(i);
457 continue;
458 } else if b > 0 {
459 continue;
460 }
461
462 return match start? {
463 // Avoid empty domains
464 start if i - start < 2 => None,
465 start => str::from_utf8(&hostname[start + 1..i]).ok(),
466 };
467 }
468
469 None
470}
471
472#[cfg(test)]
473mod test {
474 use super::domain_from_host;
475 #[test]
476 fn parses_domain_name() {
477 assert!(domain_from_host(b"regular-hostname\0").is_none());
478
479 assert_eq!(domain_from_host(b"with.domain-name\0"), Some("domain-name"));
480 assert_eq!(
481 domain_from_host(b"with.multiple.dots\0"),
482 Some("multiple.dots")
483 );
484
485 assert!(domain_from_host(b"hostname.\0").is_none());
486 assert_eq!(domain_from_host(b"host.a\0"), Some("a"));
487 assert_eq!(domain_from_host(b"host.au\0"), Some("au"));
488 }
489}