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