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