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::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts};
25use crate::error::ResolveResult;
26use crate::proto::rr::Name;
27
28const DEFAULT_PORT: u16 = 53;
29
30pub fn read_system_conf() -> ResolveResult<(ResolverConfig, ResolverOpts)> {
31    read_resolv_conf("/etc/resolv.conf")
32}
33
34fn read_resolv_conf<P: AsRef<Path>>(path: P) -> ResolveResult<(ResolverConfig, ResolverOpts)> {
35    let mut data = String::new();
36    let mut file = File::open(path)?;
37    file.read_to_string(&mut data)?;
38    parse_resolv_conf(&data)
39}
40
41pub fn parse_resolv_conf<T: AsRef<[u8]>>(data: T) -> ResolveResult<(ResolverConfig, ResolverOpts)> {
42    let parsed_conf = resolv_conf::Config::parse(&data).map_err(|e| {
43        io::Error::new(
44            io::ErrorKind::Other,
45            format!("Error parsing resolv.conf: {e}"),
46        )
47    })?;
48    into_resolver_config(parsed_conf)
49}
50
51// TODO: use a custom parsing error type maybe?
52fn into_resolver_config(
53    parsed_config: resolv_conf::Config,
54) -> ResolveResult<(ResolverConfig, ResolverOpts)> {
55    let domain = if let Some(domain) = parsed_config.get_system_domain() {
56        // The system domain name maybe appear to be valid to the resolv_conf
57        // crate but actually be invalid. For example, if the hostname is "matt.schulte's computer"
58        // In order to prevent a hostname which macOS or Windows would consider
59        // valid from returning an error here we turn parse errors to options
60        Name::from_str(domain.as_str()).ok()
61    } else {
62        None
63    };
64
65    // nameservers
66    let mut nameservers = Vec::<NameServerConfig>::with_capacity(parsed_config.nameservers.len());
67    for ip in &parsed_config.nameservers {
68        nameservers.push(NameServerConfig {
69            socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
70            protocol: Protocol::Udp,
71            tls_dns_name: None,
72            trust_negative_responses: false,
73            #[cfg(feature = "dns-over-rustls")]
74            tls_config: None,
75            bind_addr: None,
76        });
77        nameservers.push(NameServerConfig {
78            socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
79            protocol: Protocol::Tcp,
80            tls_dns_name: None,
81            trust_negative_responses: false,
82            #[cfg(feature = "dns-over-rustls")]
83            tls_config: None,
84            bind_addr: None,
85        });
86    }
87    if nameservers.is_empty() {
88        tracing::warn!("no nameservers found in config");
89    }
90
91    // search
92    let mut search = vec![];
93    for search_domain in parsed_config.get_last_search_or_domain() {
94        // Ignore invalid search domains
95        if search_domain == "--" {
96            continue;
97        }
98
99        search.push(Name::from_str_relaxed(search_domain).map_err(|e| {
100            io::Error::new(
101                io::ErrorKind::Other,
102                format!("Error parsing resolv.conf: {e}"),
103            )
104        })?);
105    }
106
107    let config = ResolverConfig::from_parts(domain, search, nameservers);
108
109    let options = ResolverOpts {
110        ndots: parsed_config.ndots as usize,
111        timeout: Duration::from_secs(u64::from(parsed_config.timeout)),
112        attempts: parsed_config.attempts as usize,
113        ..ResolverOpts::default()
114    };
115
116    Ok((config, options))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use proto::rr::Name;
123    use std::env;
124    use std::net::*;
125    use std::str::FromStr;
126
127    fn empty_config() -> ResolverConfig {
128        ResolverConfig::from_parts(None, vec![], vec![])
129    }
130
131    fn nameserver_config(ip: &str) -> [NameServerConfig; 2] {
132        let addr = SocketAddr::new(IpAddr::from_str(ip).unwrap(), 53);
133        [
134            NameServerConfig {
135                socket_addr: addr,
136                protocol: Protocol::Udp,
137                tls_dns_name: None,
138                trust_negative_responses: false,
139                #[cfg(feature = "dns-over-rustls")]
140                tls_config: None,
141                bind_addr: None,
142            },
143            NameServerConfig {
144                socket_addr: addr,
145                protocol: Protocol::Tcp,
146                tls_dns_name: None,
147                trust_negative_responses: false,
148                #[cfg(feature = "dns-over-rustls")]
149                tls_config: None,
150                bind_addr: None,
151            },
152        ]
153    }
154
155    fn tests_dir() -> String {
156        let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
157        format!("{server_path}/crates/resolver/tests")
158    }
159
160    #[test]
161    #[allow(clippy::redundant_clone)]
162    fn test_name_server() {
163        let parsed = parse_resolv_conf("nameserver 127.0.0.1").expect("failed");
164        let mut cfg = empty_config();
165        let nameservers = nameserver_config("127.0.0.1");
166        cfg.add_name_server(nameservers[0].clone());
167        cfg.add_name_server(nameservers[1].clone());
168        assert_eq!(cfg.name_servers(), parsed.0.name_servers());
169        assert_eq!(ResolverOpts::default(), parsed.1);
170    }
171
172    #[test]
173    fn test_search() {
174        let parsed = parse_resolv_conf("search localnet.").expect("failed");
175        let mut cfg = empty_config();
176        cfg.add_search(Name::from_str("localnet.").unwrap());
177        assert_eq!(cfg.search(), parsed.0.search());
178        assert_eq!(ResolverOpts::default(), parsed.1);
179    }
180
181    #[test]
182    fn test_skips_invalid_search() {
183        let parsed =
184            parse_resolv_conf("\n\nnameserver 127.0.0.53\noptions edns0 trust-ad\nsearch -- lan\n")
185                .expect("failed");
186        let mut cfg = empty_config();
187
188        {
189            let nameservers = nameserver_config("127.0.0.53");
190            cfg.add_name_server(nameservers[0].clone());
191            cfg.add_name_server(nameservers[1].clone());
192            assert_eq!(cfg.name_servers(), parsed.0.name_servers());
193            assert_eq!(ResolverOpts::default(), parsed.1);
194        }
195
196        // This is the important part, that the invalid `--` is skipped during parsing
197        {
198            cfg.add_search(Name::from_str("lan").unwrap());
199            assert_eq!(cfg.search(), parsed.0.search());
200            assert_eq!(ResolverOpts::default(), parsed.1);
201        }
202    }
203
204    #[test]
205    fn test_underscore_in_search() {
206        let parsed = parse_resolv_conf("search Speedport_000").expect("failed");
207        let mut cfg = empty_config();
208        cfg.add_search(Name::from_str_relaxed("Speedport_000.").unwrap());
209        assert_eq!(cfg.search(), parsed.0.search());
210        assert_eq!(ResolverOpts::default(), parsed.1);
211    }
212
213    #[test]
214    fn test_domain() {
215        let parsed = parse_resolv_conf("domain example.com").expect("failed");
216        let mut cfg = empty_config();
217        cfg.set_domain(Name::from_str("example.com").unwrap());
218        assert_eq!(cfg, parsed.0);
219        assert_eq!(ResolverOpts::default(), parsed.1);
220    }
221
222    #[test]
223    fn test_read_resolv_conf() {
224        read_resolv_conf(format!("{}/resolv.conf-simple", tests_dir())).expect("simple failed");
225        read_resolv_conf(format!("{}/resolv.conf-macos", tests_dir())).expect("macos failed");
226        read_resolv_conf(format!("{}/resolv.conf-linux", tests_dir())).expect("linux failed");
227    }
228}