hickory_resolver/system_conf/
unix.rs1use 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::ResolveError;
25use crate::config::{NameServerConfig, ResolverConfig, ResolverOpts};
26use crate::proto::rr::Name;
27use crate::proto::xfer::Protocol;
28
29const DEFAULT_PORT: u16 = 53;
30
31pub fn read_system_conf() -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
32 read_resolv_conf("/etc/resolv.conf")
33}
34
35fn read_resolv_conf<P: AsRef<Path>>(
36 path: P,
37) -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
38 let mut data = String::new();
39 let mut file = File::open(path)?;
40 file.read_to_string(&mut data)?;
41 parse_resolv_conf(&data)
42}
43
44pub fn parse_resolv_conf<T: AsRef<[u8]>>(
45 data: T,
46) -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
47 let parsed_conf = resolv_conf::Config::parse(&data).map_err(|e| {
48 io::Error::new(
49 io::ErrorKind::Other,
50 format!("Error parsing resolv.conf: {e}"),
51 )
52 })?;
53 into_resolver_config(parsed_conf)
54}
55
56fn into_resolver_config(
58 parsed_config: resolv_conf::Config,
59) -> Result<(ResolverConfig, ResolverOpts), ResolveError> {
60 let domain = if let Some(domain) = parsed_config.get_system_domain() {
61 Name::from_str(domain.as_str()).ok()
66 } else {
67 None
68 };
69
70 let mut nameservers = Vec::<NameServerConfig>::with_capacity(parsed_config.nameservers.len());
72 for ip in &parsed_config.nameservers {
73 nameservers.push(NameServerConfig {
74 socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
75 protocol: Protocol::Udp,
76 tls_dns_name: None,
77 http_endpoint: None,
78 trust_negative_responses: false,
79 bind_addr: None,
80 });
81 nameservers.push(NameServerConfig {
82 socket_addr: SocketAddr::new(ip.into(), DEFAULT_PORT),
83 protocol: Protocol::Tcp,
84 tls_dns_name: None,
85 http_endpoint: None,
86 trust_negative_responses: false,
87 bind_addr: None,
88 });
89 }
90 if nameservers.is_empty() {
91 Err(io::Error::new(
92 io::ErrorKind::Other,
93 "no nameservers found in config",
94 ))?;
95 }
96
97 let mut search = vec![];
99 for search_domain in parsed_config.get_last_search_or_domain() {
100 if search_domain == "--" {
102 continue;
103 }
104
105 search.push(Name::from_str_relaxed(search_domain).map_err(|e| {
106 io::Error::new(
107 io::ErrorKind::Other,
108 format!("Error parsing resolv.conf: {e}"),
109 )
110 })?);
111 }
112
113 let config = ResolverConfig::from_parts(domain, search, nameservers);
114
115 let options = ResolverOpts {
116 ndots: parsed_config.ndots as usize,
117 timeout: Duration::from_secs(u64::from(parsed_config.timeout)),
118 attempts: parsed_config.attempts as usize,
119 edns0: parsed_config.edns0,
120 ..ResolverOpts::default()
121 };
122
123 Ok((config, options))
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::proto::rr::Name;
130 use std::env;
131 use std::net::*;
132 use std::str::FromStr;
133
134 fn empty_config(name_servers: Vec<NameServerConfig>) -> ResolverConfig {
135 ResolverConfig::from_parts(None, vec![], name_servers)
136 }
137
138 fn nameserver_config(ip: &str) -> [NameServerConfig; 2] {
139 let addr = SocketAddr::new(IpAddr::from_str(ip).unwrap(), 53);
140 [
141 NameServerConfig {
142 socket_addr: addr,
143 protocol: Protocol::Udp,
144 tls_dns_name: None,
145 http_endpoint: None,
146 trust_negative_responses: false,
147 bind_addr: None,
148 },
149 NameServerConfig {
150 socket_addr: addr,
151 protocol: Protocol::Tcp,
152 tls_dns_name: None,
153 http_endpoint: None,
154 trust_negative_responses: false,
155 bind_addr: None,
156 },
157 ]
158 }
159
160 fn tests_dir() -> String {
161 let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned());
162 format!("{server_path}/crates/resolver/tests")
163 }
164
165 #[test]
166 #[allow(clippy::redundant_clone)]
167 fn test_name_server() {
168 let parsed = parse_resolv_conf("nameserver 127.0.0.1").expect("failed");
169 let cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
170 assert_eq!(
171 cfg.name_servers()[0].socket_addr,
172 parsed.0.name_servers()[0].socket_addr
173 );
174 is_default_opts(parsed.1);
175 }
176
177 #[test]
178 fn test_search() {
179 let parsed = parse_resolv_conf("search localnet.\nnameserver 127.0.0.1").expect("failed");
180 let mut cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
181 cfg.add_search(Name::from_str("localnet.").unwrap());
182 assert_eq!(cfg.search(), parsed.0.search());
183 is_default_opts(parsed.1);
184 }
185
186 #[test]
187 fn test_skips_invalid_search() {
188 let parsed =
189 parse_resolv_conf("\n\nnameserver 127.0.0.53\noptions edns0 trust-ad\nsearch -- lan\n")
190 .expect("failed");
191 let mut cfg = empty_config(nameserver_config("127.0.0.53").to_vec());
192
193 {
194 assert_eq!(
195 cfg.name_servers()[0].socket_addr,
196 parsed.0.name_servers()[0].socket_addr
197 );
198 is_default_opts(parsed.1);
199 }
200
201 {
203 cfg.add_search(Name::from_str("lan").unwrap());
204 assert_eq!(cfg.search(), parsed.0.search());
205 }
206 }
207
208 #[test]
209 fn test_underscore_in_search() {
210 let parsed =
211 parse_resolv_conf("search Speedport_000\nnameserver 127.0.0.1").expect("failed");
212 let mut cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
213 cfg.add_search(Name::from_str_relaxed("Speedport_000").unwrap());
214 assert_eq!(cfg.search(), parsed.0.search());
215 is_default_opts(parsed.1);
216 }
217
218 #[test]
219 fn test_domain() {
220 let parsed = parse_resolv_conf("domain example.com\nnameserver 127.0.0.1").expect("failed");
221 let mut cfg = empty_config(nameserver_config("127.0.0.1").to_vec());
222 cfg.set_domain(Name::from_str("example.com").unwrap());
223 assert_eq!(
224 cfg.name_servers()[0].socket_addr,
225 parsed.0.name_servers()[0].socket_addr
226 );
227 assert_eq!(cfg.domain(), parsed.0.domain());
228 is_default_opts(parsed.1);
229 }
230
231 #[test]
232 fn test_read_resolv_conf() {
233 read_resolv_conf(format!("{}/resolv.conf-simple", tests_dir())).expect("simple failed");
234 read_resolv_conf(format!("{}/resolv.conf-macos", tests_dir())).expect("macos failed");
235 read_resolv_conf(format!("{}/resolv.conf-linux", tests_dir())).expect("linux failed");
236 }
237
238 fn is_default_opts(opts: ResolverOpts) {
240 assert_eq!(opts.ndots, 1);
241 assert_eq!(opts.timeout, Duration::from_secs(5));
242 assert_eq!(opts.attempts, 2);
243 }
244}