domain/resolv/stub/conf.rs
1//! Resolver configuration
2//!
3//! There are two parts to this module: Query options that allow you to
4//! modify the behaviour of the resolver on a query by query basis and
5//! the global resolver configuration (normally read from the system’s
6//! `/etc/resolv.conf`) that contains things like the name servers to query
7//! and a set of default options.
8//!
9//! Both parts are modeled along the lines of glibc’s resolver.
10
11use crate::base::name::{self, Dname};
12use smallvec::SmallVec;
13use std::cmp::Ordering;
14use std::default::Default;
15use std::io::Read;
16use std::net::{IpAddr, Ipv4Addr, SocketAddr};
17use std::path::Path;
18use std::slice::SliceIndex;
19use std::str::{self, FromStr, SplitWhitespace};
20use std::time::Duration;
21use std::vec::Vec;
22use std::{convert, error, fmt, fs, io, ops};
23
24//------------ ResolvOptions ------------------------------------------------
25
26/// Options for the resolver configuration.
27///
28/// This type contains a lot of options that influence the resolver
29/// configuration. It collects all server-indpendent options that glibc’s
30/// resolver supports. Not all of them are currently supported by this
31/// implementation.
32#[derive(Clone, Debug)]
33pub struct ResolvOptions {
34 /// Search list for host-name lookup.
35 pub search: SearchList,
36
37 /// TODO Sortlist
38 /// sortlist: ??
39
40 /// Number of dots before an initial absolute query is made.
41 pub ndots: usize,
42
43 /// Timeout to wait for a response.
44 pub timeout: Duration,
45
46 /// Number of retries before giving up.
47 pub attempts: usize,
48
49 /// Accept authoritative answers only.
50 ///
51 /// Only responses with the AA bit set will be considered. If there
52 /// aren’t any, the query will fail.
53 ///
54 /// This option is not currently implemented. It is likely to be
55 /// eventually implemented by the query.
56 pub aa_only: bool,
57
58 /// Always use TCP.
59 ///
60 /// This option is implemented by the query.
61 pub use_vc: bool,
62
63 /// Query primary name servers only.
64 ///
65 /// This option is not currently implemented. It is unclear what exactly
66 /// it is supposed to mean.
67 pub primary: bool,
68
69 /// Ignore trunactions errors, don’t retry with TCP.
70 ///
71 /// This option is implemented by the query.
72 pub ign_tc: bool,
73
74 /// Set the recursion desired bit in queries.
75 ///
76 /// Enabled by default.
77 ///
78 /// Implemented by the query request.
79 pub recurse: bool,
80
81 /// Append the default domain name to single component names.
82 ///
83 /// Enabled by default.
84 ///
85 /// This is not currently implemented. Instead, the resolver config’s
86 /// `search` and `ndots` fields govern resolution of relative names of
87 /// all kinds.
88 pub default_names: bool,
89
90 /// Keep TCP connections open between queries.
91 ///
92 /// This is not currently implemented.
93 pub stay_open: bool,
94
95 /// Search hostnames in the current domain and parent domains.
96 ///
97 /// Enabled by default.
98 ///
99 /// This options is not currently implemented. Instead, the resolver
100 /// config’s `search` and `ndots` fields govern resolution of relative
101 /// names.
102 pub dn_search: bool,
103
104 /// Try AAAA query before A query and map IPv4 responses to tunnel form.
105 ///
106 /// This option is not currently implemented. It is only relevant for
107 /// `lookup_host`.
108 pub use_inet6: bool,
109
110 /// Use round-robin selection of name servers.
111 ///
112 /// This option is implemented by the query.
113 pub rotate: bool,
114
115 /// Disable checking of incoming hostname and mail names.
116 ///
117 /// This is not currently implemented. Or rather, this is currently
118 /// always on—there is no name checking as yet.
119 pub no_check_name: bool,
120
121 /// Do not strip TSIG records.
122 ///
123 /// This is not currently implemented. Or rather, no records are stripped
124 /// at all.
125 pub keep_tsig: bool,
126
127 /// Send each query simultaneously to all name servers.
128 ///
129 /// This is not currently implemented. It would be a query option.
130 pub blast: bool,
131
132 /// Use bit-label format for IPv6 reverse lookups.
133 ///
134 /// Bit labels have been deprecated and consequently, this option is not
135 /// implemented.
136 pub use_bstring: bool,
137
138 /// Use ip6.int instead of the recommended ip6.arpa.
139 ///
140 /// (This option is the reverse of glibc’s `RES_NOIP6DOTINT` option).
141 ///
142 /// This option is only relevant for `lookup_addr()` and is implemented
143 /// there already.
144 pub use_ip6dotint: bool,
145
146 /// Use EDNS0.
147 ///
148 /// EDNS is not yet supported.
149 pub use_edns0: bool,
150
151 /// Perform IPv4 and IPv6 lookups sequentially instead of in parallel.
152 ///
153 /// This is not yet implemented but would be an option for
154 /// `lookup_host()`.
155 pub single_request: bool,
156
157 /// Open a new socket for each request.
158 ///
159 /// This is not currently implemented.
160 pub single_request_reopen: bool,
161
162 /// Don’t look up unqualified names as top-level-domain.
163 ///
164 /// This is not currently implemented. Instead, the resolver config’s
165 /// `search` and `ndots` fields govern resolution of relative names of
166 /// all kinds.
167 pub no_tld_query: bool,
168}
169
170impl Default for ResolvOptions {
171 fn default() -> Self {
172 ResolvOptions {
173 // non-flags:
174 search: SearchList::new(),
175 //sortlist,
176 ndots: 1,
177 timeout: Duration::new(5, 0),
178 attempts: 2,
179
180 // enabled by default:
181 recurse: true,
182 default_names: true,
183 dn_search: true,
184
185 // everthing else is not:
186 aa_only: false,
187 use_vc: false,
188 primary: false,
189 ign_tc: false,
190 stay_open: false,
191 use_inet6: false,
192 rotate: false,
193 no_check_name: false,
194 keep_tsig: false,
195 blast: false,
196 use_bstring: false,
197 use_ip6dotint: false,
198 use_edns0: false,
199 single_request: false,
200 single_request_reopen: false,
201 no_tld_query: false,
202 }
203 }
204}
205
206//------------ Transport -----------------------------------------------------
207
208/// The transport protocol to be used for a server.
209#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
210pub enum Transport {
211 /// Unencrypted UDP transport.
212 Udp,
213
214 /// Unencrypted TCP transport.
215 Tcp,
216}
217
218impl Transport {
219 /// Returns whether the transport is a preferred transport.
220 ///
221 /// Only preferred transports are considered initially. Only if a
222 /// truncated answer comes back will we consider streaming protocols
223 /// instead.
224 pub fn is_preferred(self) -> bool {
225 match self {
226 Transport::Udp => true,
227 Transport::Tcp => false,
228 }
229 }
230
231 /// Returns whether the transport is a streaming protocol.
232 pub fn is_stream(self) -> bool {
233 match self {
234 Transport::Udp => false,
235 Transport::Tcp => true,
236 }
237 }
238}
239
240//------------ ServerConf ----------------------------------------------------
241
242/// Configuration for one upstream DNS server.
243///
244/// The server is identified by a socket address, ie., an address/port pair.
245/// For each server you can set how it should operate on all supported
246/// transport protocols, including not at all, and two timeouts for each
247/// request and sockets. The timeouts are used for all transports. If you
248/// need different timeouts for, say, UDP and TCP, you can always use two
249/// server entries with the same address.
250#[derive(Clone, Debug)]
251pub struct ServerConf {
252 /// Server address.
253 pub addr: SocketAddr,
254
255 /// Transport protocol.
256 pub transport: Transport,
257
258 /// How long to wait for a response before returning a timeout error.
259 ///
260 /// This field defaults to 2 seconds.
261 pub request_timeout: Duration,
262
263 /// Size of the message receive buffer in bytes.
264 ///
265 /// This is used for datagram transports only. It defaults to 1232 bytes
266 /// for both IPv6 and IPv4.
267 ///
268 /// (Note: While 1372 bytes works for IPv4 in most scenarios, there has
269 /// been [research] indicating that sometimes 1232 bytes is the limit here,
270 /// sometimes too.)
271 ///
272 /// [research]: https://rp.delaat.net/2019-2020/p78/report.pdf
273 pub recv_size: usize,
274
275 /// Advertised UDP payload size.
276 ///
277 /// This values will be announced in request if EDNS is supported by the
278 /// server. It will be included both for datagram and streaming transport
279 /// but really only matters for UDP.
280 pub udp_payload_size: u16,
281}
282
283impl ServerConf {
284 /// Returns a new default server config for a given address and transport.
285 ///
286 /// The function sets default values as described in the field
287 /// descriptions above.
288 pub fn new(addr: SocketAddr, transport: Transport) -> Self {
289 ServerConf {
290 addr,
291 transport,
292 request_timeout: Duration::from_secs(2),
293 recv_size: 1232,
294 udp_payload_size: 1232,
295 }
296 }
297}
298
299//------------ ResolvConf ---------------------------------------------------
300
301/// Resolver configuration.
302///
303/// This type collects all information necessary to configure how a stub
304/// resolver talks to its upstream resolvers.
305///
306/// The type follows the builder pattern. After creating a value with
307/// `ResolvConf::new()` you can manipulate the members. Once you are happy
308/// with them, you call `finalize()` to make sure the configuration is valid.
309/// It mostly just fixes the `servers`.
310///
311/// Additionally, the type can parse a glibc-style configuration file,
312/// commonly known as `/etc/resolv.conf` through the `parse()` and
313/// `parse_file()` methods. You still need to call `finalize()` after
314/// parsing.
315///
316/// The easiest way, however, to get the system resolver configuration is
317/// through `ResolvConf::system_default()`. This will parse the configuration
318/// file or return a default configuration if that fails.
319///
320#[derive(Clone, Debug)]
321pub struct ResolvConf {
322 /// Addresses of servers to query.
323 pub servers: Vec<ServerConf>,
324
325 /// Default options.
326 pub options: ResolvOptions,
327}
328
329/// # Management
330///
331impl ResolvConf {
332 /// Creates a new, empty configuration.
333 ///
334 /// Using an empty configuration will fail since it does not contain
335 /// any name servers. Call `self.finalize()` to make it usable.
336 pub fn new() -> Self {
337 ResolvConf {
338 servers: Vec::new(),
339 options: ResolvOptions::default(),
340 }
341 }
342
343 /// Finalizes the configuration for actual use.
344 ///
345 /// The function does two things. If `servers` is empty, it adds
346 /// `127.0.0.1:53`. This is exactly what glibc does. If `search` is
347 /// empty, it adds the root domain `"."`. This differs from what
348 /// glibc does which considers the machine’s host name.
349 pub fn finalize(&mut self) {
350 if self.servers.is_empty() {
351 // glibc just simply uses 127.0.0.1:53. Let's do that, too,
352 // and claim it is for compatibility.
353 let addr =
354 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 53);
355 self.servers.push(ServerConf::new(addr, Transport::Udp));
356 self.servers.push(ServerConf::new(addr, Transport::Tcp));
357 }
358 if self.options.search.is_empty() {
359 self.options.search.push(Dname::root())
360 }
361 for server in &mut self.servers {
362 server.request_timeout = self.options.timeout
363 }
364 }
365}
366
367/// # Parsing Configuration File
368///
369impl ResolvConf {
370 /// Parses the configuration from a file.
371 pub fn parse_file<P: AsRef<Path>>(
372 &mut self,
373 path: P,
374 ) -> Result<(), Error> {
375 let mut file = fs::File::open(path)?;
376 self.parse(&mut file)
377 }
378
379 /// Parses the configuration from a reader.
380 ///
381 /// The format is that of the /etc/resolv.conf file.
382 pub fn parse<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
383 use std::io::BufRead;
384
385 for line in io::BufReader::new(reader).lines() {
386 let line = line?;
387 let line = line.trim_end();
388
389 if line.is_empty()
390 || line.starts_with(';')
391 || line.starts_with('#')
392 {
393 continue;
394 }
395
396 let mut words = line.split_whitespace();
397 let keyword = words.next();
398 match keyword {
399 Some("nameserver") => self.parse_nameserver(words)?,
400 Some("domain") => self.parse_domain(words)?,
401 Some("search") => self.parse_search(words)?,
402 Some("sortlist") => { /* TODO: self.parse_sortlist(words)? */
403 }
404 Some("options") => self.parse_options(words)?,
405 _ => return Err(Error::ParseError),
406 }
407 }
408 Ok(())
409 }
410
411 fn parse_nameserver(
412 &mut self,
413 mut words: SplitWhitespace,
414 ) -> Result<(), Error> {
415 use std::net::ToSocketAddrs;
416
417 for addr in (next_word(&mut words)?, 53).to_socket_addrs()? {
418 self.servers.push(ServerConf::new(addr, Transport::Udp));
419 self.servers.push(ServerConf::new(addr, Transport::Tcp));
420 }
421 no_more_words(words)
422 }
423
424 fn parse_domain(
425 &mut self,
426 mut words: SplitWhitespace,
427 ) -> Result<(), Error> {
428 let domain = SearchSuffix::from_str(next_word(&mut words)?)?;
429 self.options.search = domain.into();
430 no_more_words(words)
431 }
432
433 fn parse_search(&mut self, words: SplitWhitespace) -> Result<(), Error> {
434 let mut search = SearchList::new();
435 let mut root = false;
436 for word in words {
437 let name = SearchSuffix::from_str(word)?;
438 if name.is_root() {
439 root = true
440 }
441 search.push(name);
442 }
443 if !root {
444 search.push(SearchSuffix::root());
445 }
446 self.options.search = search;
447 Ok(())
448 }
449
450 /*
451 fn parse_sortlist(
452 &mut self,
453 _words: SplitWhitespace
454 ) -> Result<(), Error> {
455 // XXX TODO
456 }
457 */
458
459 #[allow(clippy::match_same_arms)]
460 fn parse_options(&mut self, words: SplitWhitespace) -> Result<(), Error> {
461 for word in words {
462 match split_arg(word)? {
463 ("debug", None) => {}
464 ("ndots", Some(n)) => self.options.ndots = n,
465 ("timeout", Some(n)) => {
466 self.options.timeout = Duration::new(n as u64, 0)
467 }
468 ("attempts", Some(n)) => self.options.attempts = n,
469 ("rotate", None) => self.options.rotate = true,
470 ("no-check-names", None) => self.options.no_check_name = true,
471 ("inet6", None) => self.options.use_inet6 = true,
472 ("ip6-bytestring", None) => self.options.use_bstring = true,
473 ("ip6-dotint", None) => self.options.use_ip6dotint = true,
474 ("no-ip6-dotint", None) => self.options.use_ip6dotint = false,
475 ("edns0", None) => self.options.use_edns0 = true,
476 ("single-request", None) => {
477 self.options.single_request = true
478 }
479 ("single-request-reopen", None) => {
480 self.options.single_request_reopen = true
481 }
482 ("no-tld-query", None) => self.options.no_tld_query = true,
483 ("use-vc", None) => self.options.use_vc = true,
484 // Ignore unknown or misformated options.
485 _ => {}
486 }
487 }
488 Ok(())
489 }
490}
491
492//--- Default
493
494impl Default for ResolvConf {
495 /// Creates a default configuration for this system.
496 ///
497 /// XXX This currently only works for Unix-y systems.
498 fn default() -> Self {
499 let mut res = ResolvConf::new();
500 let _ = res.parse_file("/etc/resolv.conf");
501 res.finalize();
502 res
503 }
504}
505
506//--- Display
507
508impl fmt::Display for ResolvConf {
509 #[allow(clippy::cognitive_complexity)]
510 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
511 for server in &self.servers {
512 let server = server.addr;
513 f.write_str("nameserver ")?;
514 if server.port() == 53 {
515 server.ip().fmt(f)?;
516 } else {
517 server.fmt(f)?;
518 }
519 "\n".fmt(f)?;
520 }
521 match self.options.search.len().cmp(&1) {
522 Ordering::Equal => {
523 writeln!(f, "domain {}", self.options.search[0])?;
524 }
525 Ordering::Greater => {
526 "search".fmt(f)?;
527 for name in self.options.search.as_slice() {
528 write!(f, " {}", name)?;
529 }
530 "\n".fmt(f)?;
531 }
532 Ordering::Less => {}
533 }
534
535 // Collect options so we only print them if there are any non-default
536 // ones.
537 let mut options = Vec::new();
538
539 if self.options.ndots != 1 {
540 options.push(format!("ndots:{}", self.options.ndots));
541 }
542 if self.options.timeout != Duration::new(5, 0) {
543 // XXX This ignores fractional seconds.
544 options
545 .push(format!("timeout:{}", self.options.timeout.as_secs()));
546 }
547 if self.options.attempts != 2 {
548 options.push(format!("attempts:{}", self.options.attempts));
549 }
550 if self.options.aa_only {
551 options.push("aa-only".into())
552 }
553 if self.options.use_vc {
554 options.push("use-vc".into())
555 }
556 if self.options.primary {
557 options.push("primary".into())
558 }
559 if self.options.ign_tc {
560 options.push("ign-tc".into())
561 }
562 if !self.options.recurse {
563 options.push("no-recurse".into())
564 }
565 if !self.options.default_names {
566 options.push("no-default-names".into())
567 }
568 if self.options.stay_open {
569 options.push("stay-open".into())
570 }
571 if !self.options.dn_search {
572 options.push("no-dn-search".into())
573 }
574 if self.options.use_inet6 {
575 options.push("use-inet6".into())
576 }
577 if self.options.rotate {
578 options.push("rotate".into())
579 }
580 if self.options.no_check_name {
581 options.push("no-check-name".into())
582 }
583 if self.options.keep_tsig {
584 options.push("keep-tsig".into())
585 }
586 if self.options.blast {
587 options.push("blast".into())
588 }
589 if self.options.use_bstring {
590 options.push("use-bstring".into())
591 }
592 if self.options.use_ip6dotint {
593 options.push("ip6dotint".into())
594 }
595 if self.options.use_edns0 {
596 options.push("use-edns0".into())
597 }
598 if self.options.single_request {
599 options.push("single-request".into())
600 }
601 if self.options.single_request_reopen {
602 options.push("single-request-reopen".into())
603 }
604 if self.options.no_tld_query {
605 options.push("no-tld-query".into())
606 }
607
608 if !options.is_empty() {
609 "options".fmt(f)?;
610 for option in options {
611 write!(f, " {}", option)?;
612 }
613 "\n".fmt(f)?;
614 }
615
616 Ok(())
617 }
618}
619
620//------------ SearchSuffix --------------------------------------------------
621
622pub type SearchSuffix = Dname<SmallVec<[u8; 24]>>;
623
624//------------ SearchList ----------------------------------------------------
625
626#[derive(Clone, Debug, Default)]
627pub struct SearchList {
628 search: Vec<SearchSuffix>,
629}
630
631impl SearchList {
632 pub fn new() -> Self {
633 Self::default()
634 }
635
636 pub fn push(&mut self, name: SearchSuffix) {
637 self.search.push(name)
638 }
639
640 pub fn push_root(&mut self) {
641 self.search.push(Dname::root())
642 }
643
644 pub fn len(&self) -> usize {
645 self.search.len()
646 }
647
648 pub fn is_empty(&self) -> bool {
649 self.search.is_empty()
650 }
651
652 pub fn get(&self, pos: usize) -> Option<&SearchSuffix> {
653 self.search.get(pos)
654 }
655
656 pub fn as_slice(&self) -> &[SearchSuffix] {
657 self.as_ref()
658 }
659}
660
661impl From<SearchSuffix> for SearchList {
662 fn from(name: SearchSuffix) -> Self {
663 let mut res = Self::new();
664 res.push(name);
665 res
666 }
667}
668
669impl<I: SliceIndex<[SearchSuffix]>> ops::Index<I> for SearchList {
670 type Output = <I as SliceIndex<[SearchSuffix]>>::Output;
671
672 fn index(&self, index: I) -> &<I as SliceIndex<[SearchSuffix]>>::Output {
673 self.search.index(index)
674 }
675}
676
677//--- AsRef
678
679impl AsRef<[SearchSuffix]> for SearchList {
680 fn as_ref(&self) -> &[SearchSuffix] {
681 self.search.as_ref()
682 }
683}
684
685//------------ Private Helpers -----------------------------------------------
686//
687// These are here to wrap stuff into Results.
688
689/// Returns a reference to the next word or an error.
690fn next_word<'a>(
691 words: &'a mut str::SplitWhitespace,
692) -> Result<&'a str, Error> {
693 match words.next() {
694 Some(word) => Ok(word),
695 None => Err(Error::ParseError),
696 }
697}
698
699/// Returns nothing but errors out if there are words left.
700fn no_more_words(mut words: str::SplitWhitespace) -> Result<(), Error> {
701 match words.next() {
702 Some(..) => Err(Error::ParseError),
703 None => Ok(()),
704 }
705}
706
707/// Splits the name and argument from an option with arguments.
708///
709/// These options consist of a name followed by a colon followed by a
710/// value, which so far is only `usize`, so we do that.
711fn split_arg(s: &str) -> Result<(&str, Option<usize>), Error> {
712 match s.find(':') {
713 Some(idx) => {
714 let (left, right) = s.split_at(idx);
715 Ok((left, Some(right[1..].parse()?)))
716 }
717 None => Ok((s, None)),
718 }
719}
720
721//------------ Error --------------------------------------------------------
722
723/// The error that can happen when parsing `resolv.conf`.
724#[derive(Debug)]
725pub enum Error {
726 /// The file is not a proper file.
727 ParseError,
728
729 /// Something happend while reading.
730 Io(io::Error),
731}
732
733impl error::Error for Error {}
734
735impl convert::From<io::Error> for Error {
736 fn from(error: io::Error) -> Error {
737 Error::Io(error)
738 }
739}
740
741impl convert::From<name::FromStrError> for Error {
742 fn from(_: name::FromStrError) -> Error {
743 Error::ParseError
744 }
745}
746
747impl convert::From<::std::num::ParseIntError> for Error {
748 fn from(_: ::std::num::ParseIntError) -> Error {
749 Error::ParseError
750 }
751}
752
753impl fmt::Display for Error {
754 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
755 match *self {
756 Error::ParseError => write!(f, "error parsing configuration"),
757 Error::Io(ref e) => e.fmt(f),
758 }
759 }
760}
761
762//============ Testing ======================================================
763
764#[cfg(test)]
765mod test {
766 use super::*;
767 use std::io;
768 use std::string::ToString;
769
770 #[test]
771 fn parse_resolv_conf() {
772 let mut conf = ResolvConf::new();
773 let data = "nameserver 192.0.2.0\n\
774 nameserver 192.0.2.1\n\
775 options use-vc ndots:122\n"
776 .to_string();
777 assert!(conf.parse(&mut io::Cursor::new(data)).is_ok());
778 assert!(conf.options.use_vc);
779 assert_eq!(conf.options.ndots, 122);
780 }
781}