hickory_resolver/system_conf/
unix.rs
1use 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
51fn 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 Name::from_str(domain.as_str()).ok()
61 } else {
62 None
63 };
64
65 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 let mut search = vec![];
93 for search_domain in parsed_config.get_last_search_or_domain() {
94 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 {
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}