sync_resolve/
resolv_conf.rs

1//! Partial Unix `resolv.conf(5)` parser
2
3use std::cmp::min;
4use std::fs::File;
5use std::io::{self, BufRead, BufReader};
6use std::net::{IpAddr, SocketAddr};
7use std::time::Duration;
8
9use crate::config::DnsConfig;
10use crate::hostname::get_hostname;
11
12/// port for DNS communication
13const DNS_PORT: u16 = 53;
14
15/// Maximum number of name servers loaded from `resolv.conf`
16pub const MAX_NAME_SERVERS: usize = 3;
17
18/// Default value of `"options attempts:n"`
19pub const DEFAULT_ATTEMPTS: u32 = 2;
20
21/// Default value of `"options ndots:n"`
22pub const DEFAULT_N_DOTS: u32 = 1;
23
24/// Default value of `"options timeout:n"`
25pub const DEFAULT_TIMEOUT: u64 = 5;
26
27/// Maximum allowed value of `"options attempts:n"`
28pub const MAX_ATTEMPTS: u32 = 5;
29
30/// Maximum allowed value of `"options ndots:n"`
31pub const MAX_N_DOTS: u32 = 15;
32
33/// Maximum allowed value of `"options timeout:n"`
34pub const MAX_TIMEOUT: u64 = 30;
35
36/// Path to system `resolv.conf`
37pub const RESOLV_CONF_PATH: &str = "/etc/resolv.conf";
38
39fn default_config() -> DnsConfig {
40    DnsConfig {
41        name_servers: Vec::new(),
42        search: Vec::new(),
43
44        n_dots: DEFAULT_N_DOTS,
45        attempts: DEFAULT_ATTEMPTS,
46        timeout: Duration::from_secs(DEFAULT_TIMEOUT),
47
48        rotate: false,
49        use_inet6: false,
50    }
51}
52
53/// Examines system `resolv.conf` and returns a configuration loosely based
54/// on its contents. If the file cannot be read or lacks required directives,
55/// an error is returned.
56pub fn load() -> io::Result<DnsConfig> {
57    parse(BufReader::new(File::open(RESOLV_CONF_PATH)?))
58}
59
60fn parse<R: BufRead>(r: R) -> io::Result<DnsConfig> {
61    let mut cfg = default_config();
62
63    for line in r.lines() {
64        let line = line?;
65
66        if line.is_empty() || line.starts_with(|c| c == '#' || c == ';') {
67            continue;
68        }
69
70        let mut words = line.split_whitespace();
71
72        let name = match words.next() {
73            Some(name) => name,
74            None => continue,
75        };
76
77        match name {
78            "nameserver" => {
79                if let Some(ip) = words.next() {
80                    if cfg.name_servers.len() < MAX_NAME_SERVERS {
81                        if let Ok(ip) = ip.parse::<IpAddr>() {
82                            cfg.name_servers.push(SocketAddr::new(ip, DNS_PORT))
83                        }
84                    }
85                }
86            }
87            "domain" => {
88                if let Some(domain) = words.next() {
89                    cfg.search = vec![domain.to_owned()];
90                }
91            }
92            "search" => {
93                cfg.search = words.map(|s| s.to_owned()).collect();
94            }
95            "options" => {
96                for opt in words {
97                    let (opt, value) = match opt.find(':') {
98                        Some(pos) => (&opt[..pos], &opt[pos + 1..]),
99                        None => (opt, ""),
100                    };
101
102                    match opt {
103                        "ndots" => {
104                            if let Ok(n) = value.parse() {
105                                cfg.n_dots = min(n, MAX_N_DOTS);
106                            }
107                        }
108                        "timeout" => {
109                            if let Ok(n) = value.parse() {
110                                cfg.timeout = Duration::from_secs(min(n, MAX_TIMEOUT));
111                            }
112                        }
113                        "attempts" => {
114                            if let Ok(n) = value.parse() {
115                                cfg.attempts = min(n, MAX_ATTEMPTS);
116                            }
117                        }
118                        "rotate" => cfg.rotate = true,
119                        "inet6" => cfg.use_inet6 = true,
120                        _ => (),
121                    }
122                }
123            }
124            _ => (),
125        }
126    }
127
128    if cfg.name_servers.is_empty() {
129        return Err(io::Error::new(
130            io::ErrorKind::Other,
131            "no nameserver directives in resolv.conf",
132        ));
133    }
134
135    if cfg.search.is_empty() {
136        let host = get_hostname()?;
137
138        if let Some(pos) = host.find('.') {
139            cfg.search = vec![host[pos + 1..].to_owned()];
140        }
141    }
142
143    Ok(cfg)
144}
145
146#[cfg(test)]
147mod test {
148    use super::{parse, MAX_TIMEOUT};
149    use std::io::Cursor;
150
151    const TEST_CONFIG: &'static str = "\
152        nameserver 127.0.0.1
153        search foo.com bar.com
154        options timeout:99 ndots:2 rotate";
155
156    #[test]
157    fn test_parse() {
158        let r = Cursor::new(TEST_CONFIG.as_bytes());
159        let cfg = parse(r).unwrap();
160
161        assert_eq!(cfg.name_servers, ["127.0.0.1:53".parse().unwrap()]);
162        assert_eq!(cfg.search, ["foo.com", "bar.com"]);
163        assert_eq!(cfg.timeout.as_secs(), MAX_TIMEOUT);
164        assert_eq!(cfg.n_dots, 2);
165        assert_eq!(cfg.rotate, true);
166    }
167}