Skip to main content

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;
15use std::io;
16use std::path::Path;
17use std::str::FromStr;
18use std::time::Duration;
19
20use tracing::warn;
21
22use crate::config::{NameServerConfig, ResolverConfig, ResolverOpts};
23use crate::net::NetError;
24use crate::proto::rr::Name;
25
26pub fn read_system_conf() -> Result<(ResolverConfig, ResolverOpts), NetError> {
27    read_resolv_conf("/etc/resolv.conf")
28}
29
30fn read_resolv_conf<P: AsRef<Path>>(path: P) -> Result<(ResolverConfig, ResolverOpts), NetError> {
31    parse_resolv_conf(fs::read(path)?)
32}
33
34pub fn parse_resolv_conf(
35    data: impl AsRef<[u8]>,
36) -> Result<(ResolverConfig, ResolverOpts), NetError> {
37    let (parsed_conf, errors) = resolv_conf::Config::parse_with_errors(data.as_ref());
38    for error in errors {
39        warn!(%error, "error parsing resolv.conf");
40    }
41    into_resolver_config(parsed_conf)
42}
43
44// TODO: use a custom parsing error type maybe?
45fn into_resolver_config(
46    parsed_config: resolv_conf::Config,
47) -> Result<(ResolverConfig, ResolverOpts), NetError> {
48    let domain = if let Some(domain) = parsed_config.get_system_domain() {
49        // The system domain name maybe appear to be valid to the resolv_conf
50        // crate but actually be invalid. For example, if the hostname is "matt.schulte's computer"
51        // In order to prevent a hostname which macOS or Windows would consider
52        // valid from returning an error here we turn parse errors to options
53        Name::from_str(domain.as_str()).ok()
54    } else {
55        None
56    };
57
58    // nameservers
59    let nameservers = parsed_config
60        .nameservers
61        .iter()
62        .map(|ip| NameServerConfig::udp_and_tcp(ip.into()))
63        .collect::<Vec<_>>();
64    if nameservers.is_empty() {
65        Err(io::Error::other("no nameservers found in config"))?;
66    }
67
68    // search
69    let mut search = vec![];
70    for search_domain in parsed_config.get_last_search_or_domain() {
71        // Ignore invalid search domains
72        if search_domain == "--" {
73            continue;
74        }
75
76        search.push(
77            Name::from_str_relaxed(search_domain)
78                .map_err(|e| io::Error::other(format!("Error parsing resolv.conf: {e}")))?,
79        );
80    }
81
82    let config = ResolverConfig::from_parts(domain, search, nameservers);
83
84    let options = ResolverOpts {
85        ndots: parsed_config.ndots as usize,
86        timeout: Duration::from_secs(u64::from(parsed_config.timeout)),
87        attempts: parsed_config.attempts as usize,
88        edns0: parsed_config.edns0,
89        ..ResolverOpts::default()
90    };
91
92    Ok((config, options))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::proto::rr::Name;
99    use std::env;
100    use std::net::*;
101    use std::str::FromStr;
102
103    fn empty_config(name_servers: NameServerConfig) -> ResolverConfig {
104        ResolverConfig::from_parts(None, vec![], vec![name_servers])
105    }
106
107    fn nameserver_config(ip: &str) -> NameServerConfig {
108        let mut server = NameServerConfig::udp_and_tcp(IpAddr::from_str(ip).unwrap());
109        server.trust_negative_responses = false;
110        server
111    }
112
113    fn tests_dir() -> String {
114        let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
115        format!("{server_path}/crates/resolver/tests")
116    }
117
118    #[test]
119    fn test_name_server() {
120        let parsed = parse_resolv_conf("nameserver 127.0.0.1").expect("failed");
121        let cfg = empty_config(nameserver_config("127.0.0.1"));
122        assert_eq!(cfg.name_servers()[0].ip, parsed.0.name_servers()[0].ip);
123        is_default_opts(parsed.1);
124    }
125
126    #[test]
127    fn test_search() {
128        let parsed = parse_resolv_conf("search localnet.\nnameserver 127.0.0.1").expect("failed");
129        let mut cfg = empty_config(nameserver_config("127.0.0.1"));
130        cfg.add_search(Name::from_str("localnet.").unwrap());
131        assert_eq!(cfg.search(), parsed.0.search());
132        is_default_opts(parsed.1);
133    }
134
135    #[test]
136    fn test_skips_invalid_search() {
137        let parsed =
138            parse_resolv_conf("\n\nnameserver 127.0.0.53\noptions edns0 trust-ad\nsearch -- lan\n")
139                .expect("failed");
140        let mut cfg = empty_config(nameserver_config("127.0.0.53"));
141
142        {
143            assert_eq!(cfg.name_servers()[0].ip, parsed.0.name_servers()[0].ip);
144            is_default_opts(parsed.1);
145        }
146
147        // This is the important part, that the invalid `--` is skipped during parsing
148        {
149            cfg.add_search(Name::from_str("lan").unwrap());
150            assert_eq!(cfg.search(), parsed.0.search());
151        }
152    }
153
154    #[test]
155    fn test_underscore_in_search() {
156        let parsed =
157            parse_resolv_conf("search Speedport_000\nnameserver 127.0.0.1").expect("failed");
158        let mut cfg = empty_config(nameserver_config("127.0.0.1"));
159        cfg.add_search(Name::from_str_relaxed("Speedport_000").unwrap());
160        assert_eq!(cfg.search(), parsed.0.search());
161        is_default_opts(parsed.1);
162    }
163
164    #[test]
165    fn test_domain() {
166        let parsed = parse_resolv_conf("domain example.com\nnameserver 127.0.0.1").expect("failed");
167        let mut cfg = empty_config(nameserver_config("127.0.0.1"));
168        cfg.set_domain(Name::from_str("example.com").unwrap());
169        assert_eq!(cfg.name_servers()[0].ip, parsed.0.name_servers()[0].ip);
170        assert_eq!(cfg.domain(), parsed.0.domain());
171        is_default_opts(parsed.1);
172    }
173
174    #[test]
175    fn test_read_resolv_conf() {
176        read_resolv_conf(format!("{}/resolv.conf-simple", tests_dir())).expect("simple failed");
177        read_resolv_conf(format!("{}/resolv.conf-macos", tests_dir())).expect("macos failed");
178        read_resolv_conf(format!("{}/resolv.conf-linux", tests_dir())).expect("linux failed");
179    }
180
181    /// Validate that all options set in `into_resolver_config()` are at default values
182    fn is_default_opts(opts: ResolverOpts) {
183        assert_eq!(opts.ndots, 1);
184        assert_eq!(opts.timeout, Duration::from_secs(5));
185        assert_eq!(opts.attempts, 2);
186    }
187}