hickory_resolver/system_conf/
unix.rs

1// Copyright 2015-2017 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! System configuration loading
9//!
10//! This module is responsible for parsing and returning the configuration from
11//!  the host system. It will read from the default location on each operating
12//!  system, e.g. most Unixes have this written to `/etc/resolv.conf`
13
14use std::fs::File;
15use std::io;
16use std::io::Read;
17use std::net::SocketAddr;
18use std::path::Path;
19use std::str::FromStr;
20use std::time::Duration;
21
22use resolv_conf;
23
24use crate::ResolveError;
25use crate::config::{NameServerConfig, ResolverConfig, ResolverOpts};
26use crate::proto::rr::Name;
27use crate::proto::xfer::Protocol;
28
29const DEFAULT_PORT: u16 = 53;
30
31pub fn read_system_conf() -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
32    read_resolv_conf("/etc/resolv.conf")
33}
34
35fn read_resolv_conf<P: AsRef<Path>>(
36    path: P,
37) -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
38    let mut data = String::new();
39    let mut file = File::open(path)?;
40    file.read_to_string(&mut data)?;
41    parse_resolv_conf(&data)
42}
43
44pub fn parse_resolv_conf<T: AsRef<[u8]>>(
45    data: T,
46) -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
47    let parsed_conf = resolv_conf::Config::parse(&data).map_err(|e| {
48        io::Error::new(
49            io::ErrorKind::Other,
50            format!("Error parsing resolv.conf: {e}"),
51        )
52    })?;
53    into_resolver_config(parsed_conf)
54}
55
56// TODO: use a custom parsing error type maybe?
57fn into_resolver_config(
58    parsed_config: resolv_conf::Config,
59) -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
60    let domain = if let Some(domain) = parsed_config.get_system_domain() {
61        // The system domain name maybe appear to be valid to the resolv_conf
62        // crate but actually be invalid. For example, if the hostname is "matt.schulte's computer"
63        // In order to prevent a hostname which macOS or Windows would consider
64        // valid from returning an error here we turn parse errors to options
65        Name::from_str(domain.as_str()).ok()
66    } else {
67        None
68    };
69
70    // nameservers
71    let mut nameservers = Vec::<NameServerConfig>::with_capacity(parsed_config.nameservers.len());
72    for ip in &parsed_config.nameservers {
73        nameservers.push(NameServerConfig {
74            socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
75            protocol: Protocol::Udp,
76            tls_dns_name: None,
77            http_endpoint: None,
78            trust_negative_responses: false,
79            bind_addr: None,
80        });
81        nameservers.push(NameServerConfig {
82            socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
83            protocol: Protocol::Tcp,
84            tls_dns_name: None,
85            http_endpoint: None,
86            trust_negative_responses: false,
87            bind_addr: None,
88        });
89    }
90    if nameservers.is_empty() {
91        Err(io::Error::new(
92            io::ErrorKind::Other,
93            "no nameservers found in config",
94        ))?;
95    }
96
97    // search
98    let mut search = vec![];
99    for search_domain in parsed_config.get_last_search_or_domain() {
100        // Ignore invalid search domains
101        if search_domain == "--" {
102            continue;
103        }
104
105        search.push(Name::from_str_relaxed(search_domain).map_err(|e| {
106            io::Error::new(
107                io::ErrorKind::Other,
108                format!("Error parsing resolv.conf: {e}"),
109            )
110        })?);
111    }
112
113    let config = ResolverConfig::from_parts(domain, search, nameservers);
114
115    let options = ResolverOpts {
116        ndots: parsed_config.ndots as usize,
117        timeout: Duration::from_secs(u64::from(parsed_config.timeout)),
118        attempts: parsed_config.attempts as usize,
119        edns0: parsed_config.edns0,
120        ..ResolverOpts::default()
121    };
122
123    Ok((config, options))
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::proto::rr::Name;
130    use std::env;
131    use std::net::*;
132    use std::str::FromStr;
133
134    fn empty_config(name_servers: Vec<NameServerConfig>) -> ResolverConfig {
135        ResolverConfig::from_parts(None, vec![], name_servers)
136    }
137
138    fn nameserver_config(ip: &str) -> [NameServerConfig; 2] {
139        let addr = SocketAddr::new(IpAddr::from_str(ip).unwrap(), 53);
140        [
141            NameServerConfig {
142                socket_addr: addr,
143                protocol: Protocol::Udp,
144                tls_dns_name: None,
145                http_endpoint: None,
146                trust_negative_responses: false,
147                bind_addr: None,
148            },
149            NameServerConfig {
150                socket_addr: addr,
151                protocol: Protocol::Tcp,
152                tls_dns_name: None,
153                http_endpoint: None,
154                trust_negative_responses: false,
155                bind_addr: None,
156            },
157        ]
158    }
159
160    fn tests_dir() -> String {
161        let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
162        format!("{server_path}/crates/resolver/tests")
163    }
164
165    #[test]
166    #[allow(clippy::redundant_clone)]
167    fn test_name_server() {
168        let parsed = parse_resolv_conf("nameserver 127.0.0.1").expect("failed");
169        let cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
170        assert_eq!(
171            cfg.name_servers()[0].socket_addr,
172            parsed.0.name_servers()[0].socket_addr
173        );
174        is_default_opts(parsed.1);
175    }
176
177    #[test]
178    fn test_search() {
179        let parsed = parse_resolv_conf("search localnet.\nnameserver 127.0.0.1").expect("failed");
180        let mut cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
181        cfg.add_search(Name::from_str("localnet.").unwrap());
182        assert_eq!(cfg.search(), parsed.0.search());
183        is_default_opts(parsed.1);
184    }
185
186    #[test]
187    fn test_skips_invalid_search() {
188        let parsed =
189            parse_resolv_conf("\n\nnameserver 127.0.0.53\noptions edns0 trust-ad\nsearch -- lan\n")
190                .expect("failed");
191        let mut cfg = empty_config(nameserver_config("127.0.0.53").to_vec());
192
193        {
194            assert_eq!(
195                cfg.name_servers()[0].socket_addr,
196                parsed.0.name_servers()[0].socket_addr
197            );
198            is_default_opts(parsed.1);
199        }
200
201        // This is the important part, that the invalid `--` is skipped during parsing
202        {
203            cfg.add_search(Name::from_str("lan").unwrap());
204            assert_eq!(cfg.search(), parsed.0.search());
205        }
206    }
207
208    #[test]
209    fn test_underscore_in_search() {
210        let parsed =
211            parse_resolv_conf("search Speedport_000\nnameserver 127.0.0.1").expect("failed");
212        let mut cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
213        cfg.add_search(Name::from_str_relaxed("Speedport_000").unwrap());
214        assert_eq!(cfg.search(), parsed.0.search());
215        is_default_opts(parsed.1);
216    }
217
218    #[test]
219    fn test_domain() {
220        let parsed = parse_resolv_conf("domain example.com\nnameserver 127.0.0.1").expect("failed");
221        let mut cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
222        cfg.set_domain(Name::from_str("example.com").unwrap());
223        assert_eq!(
224            cfg.name_servers()[0].socket_addr,
225            parsed.0.name_servers()[0].socket_addr
226        );
227        assert_eq!(cfg.domain(), parsed.0.domain());
228        is_default_opts(parsed.1);
229    }
230
231    #[test]
232    fn test_read_resolv_conf() {
233        read_resolv_conf(format!("{}/resolv.conf-simple", tests_dir())).expect("simple failed");
234        read_resolv_conf(format!("{}/resolv.conf-macos", tests_dir())).expect("macos failed");
235        read_resolv_conf(format!("{}/resolv.conf-linux", tests_dir())).expect("linux failed");
236    }
237
238    /// Validate that all options set in `into_resolver_config()` are at default values
239    fn is_default_opts(opts: ResolverOpts) {
240        assert_eq!(opts.ndots, 1);
241        assert_eq!(opts.timeout, Duration::from_secs(5));
242        assert_eq!(opts.attempts, 2);
243    }
244}