/rust/registry/src/index.crates.io-1949cf8c6b5b557f/hickory-resolver-0.25.1/src/hosts.rs
Line | Count | Source |
1 | | //! Hosts result from a configuration of the system hosts file |
2 | | |
3 | | use std::collections::HashMap; |
4 | | use std::fs::File; |
5 | | use std::io; |
6 | | use std::net::IpAddr; |
7 | | use std::path::Path; |
8 | | use std::str::FromStr; |
9 | | use std::sync::Arc; |
10 | | |
11 | | use crate::proto::op::Query; |
12 | | use crate::proto::rr::rdata::PTR; |
13 | | use crate::proto::rr::{Name, RecordType}; |
14 | | use crate::proto::rr::{RData, Record}; |
15 | | use tracing::warn; |
16 | | |
17 | | use crate::dns_lru; |
18 | | use crate::lookup::Lookup; |
19 | | |
20 | | #[derive(Debug, Default)] |
21 | | struct LookupType { |
22 | | /// represents the A record type |
23 | | a: Option<Lookup>, |
24 | | /// represents the AAAA record type |
25 | | aaaa: Option<Lookup>, |
26 | | } |
27 | | |
28 | | /// Configuration for the local hosts file |
29 | | #[derive(Debug, Default)] |
30 | | pub struct Hosts { |
31 | | /// Name -> RDatas map |
32 | | by_name: HashMap<Name, LookupType>, |
33 | | } |
34 | | |
35 | | impl Hosts { |
36 | | /// Creates a new configuration from the system hosts file, |
37 | | /// only works for Windows and Unix-like OSes, |
38 | | /// will return empty configuration on others |
39 | | #[cfg(any(unix, windows))] |
40 | 0 | pub fn from_system() -> io::Result<Self> { |
41 | 0 | Self::from_file(hosts_path()) |
42 | 0 | } |
43 | | |
44 | | /// Creates a default configuration for non Windows or Unix-like OSes |
45 | | #[cfg(not(any(unix, windows)))] |
46 | | pub fn from_system() -> io::Result<Self> { |
47 | | Ok(Hosts::default()) |
48 | | } |
49 | | |
50 | | /// parse configuration from `path` |
51 | | #[cfg(any(unix, windows))] |
52 | 0 | pub(crate) fn from_file(path: impl AsRef<Path>) -> io::Result<Self> { |
53 | 0 | let file = File::open(path)?; |
54 | 0 | let mut hosts = Self::default(); |
55 | 0 | hosts.read_hosts_conf(file)?; |
56 | 0 | Ok(hosts) |
57 | 0 | } |
58 | | |
59 | | /// Look up the addresses for the given host from the system hosts file. |
60 | 0 | pub fn lookup_static_host(&self, query: &Query) -> Option<Lookup> { |
61 | 0 | if self.by_name.is_empty() { |
62 | 0 | return None; |
63 | 0 | } |
64 | | |
65 | 0 | let mut name = query.name().clone(); |
66 | 0 | name.set_fqdn(true); |
67 | 0 | match query.query_type() { |
68 | | RecordType::A | RecordType::AAAA => { |
69 | 0 | let val = self.by_name.get(&name)?; |
70 | | |
71 | 0 | match query.query_type() { |
72 | 0 | RecordType::A => val.a.clone(), |
73 | 0 | RecordType::AAAA => val.aaaa.clone(), |
74 | 0 | _ => None, |
75 | | } |
76 | | } |
77 | | RecordType::PTR => { |
78 | 0 | let ip = name.parse_arpa_name().ok()?; |
79 | | |
80 | 0 | let ip_addr = ip.addr(); |
81 | 0 | let records = self |
82 | 0 | .by_name |
83 | 0 | .iter() |
84 | 0 | .filter(|(_, v)| match ip_addr { |
85 | 0 | IpAddr::V4(ip) => match v.a.as_ref() { |
86 | 0 | Some(lookup) => lookup |
87 | 0 | .iter() |
88 | 0 | .any(|r| r.ip_addr().map(|it| it == ip).unwrap_or_default()), |
89 | 0 | None => false, |
90 | | }, |
91 | 0 | IpAddr::V6(ip) => match v.aaaa.as_ref() { |
92 | 0 | Some(lookup) => lookup |
93 | 0 | .iter() |
94 | 0 | .any(|r| r.ip_addr().map(|it| it == ip).unwrap_or_default()), |
95 | 0 | None => false, |
96 | | }, |
97 | 0 | }) |
98 | 0 | .map(|(n, _)| { |
99 | 0 | Record::from_rdata( |
100 | 0 | name.clone(), |
101 | | dns_lru::MAX_TTL, |
102 | 0 | RData::PTR(PTR(n.clone())), |
103 | | ) |
104 | 0 | }) |
105 | 0 | .collect::<Arc<[Record]>>(); |
106 | | |
107 | 0 | if records.is_empty() { |
108 | 0 | return None; |
109 | 0 | } |
110 | | |
111 | 0 | Some(Lookup::new_with_max_ttl(query.clone(), records)) |
112 | | } |
113 | 0 | _ => None, |
114 | | } |
115 | 0 | } |
116 | | |
117 | | /// Insert a new Lookup for the associated `Name` and `RecordType` |
118 | 0 | pub fn insert(&mut self, mut name: Name, record_type: RecordType, lookup: Lookup) { |
119 | 0 | assert!(record_type == RecordType::A || record_type == RecordType::AAAA); |
120 | | |
121 | 0 | name.set_fqdn(true); |
122 | 0 | let lookup_type = self.by_name.entry(name.clone()).or_default(); |
123 | | |
124 | 0 | let new_lookup = { |
125 | 0 | let old_lookup = match record_type { |
126 | 0 | RecordType::A => lookup_type.a.get_or_insert_with(|| { |
127 | 0 | let query = Query::query(name.clone(), record_type); |
128 | 0 | Lookup::new_with_max_ttl(query, Arc::from([])) |
129 | 0 | }), |
130 | 0 | RecordType::AAAA => lookup_type.aaaa.get_or_insert_with(|| { |
131 | 0 | let query = Query::query(name.clone(), record_type); |
132 | 0 | Lookup::new_with_max_ttl(query, Arc::from([])) |
133 | 0 | }), |
134 | | _ => { |
135 | 0 | tracing::warn!("unsupported IP type from Hosts file: {:#?}", record_type); |
136 | 0 | return; |
137 | | } |
138 | | }; |
139 | | |
140 | 0 | old_lookup.append(lookup) |
141 | | }; |
142 | | |
143 | | // replace the appended version |
144 | 0 | match record_type { |
145 | 0 | RecordType::A => lookup_type.a = Some(new_lookup), |
146 | 0 | RecordType::AAAA => lookup_type.aaaa = Some(new_lookup), |
147 | 0 | _ => tracing::warn!("unsupported IP type from Hosts file"), |
148 | | } |
149 | 0 | } |
150 | | |
151 | | /// parse configuration from `src` |
152 | 0 | pub fn read_hosts_conf(&mut self, src: impl io::Read) -> io::Result<()> { |
153 | | use std::io::{BufRead, BufReader}; |
154 | | |
155 | | // lines in the src should have the form `addr host1 host2 host3 ...` |
156 | | // line starts with `#` will be regarded with comments and ignored, |
157 | | // also empty line also will be ignored, |
158 | | // if line only include `addr` without `host` will be ignored, |
159 | | // the src will be parsed to map in the form `Name -> LookUp`. |
160 | | |
161 | 0 | for line in BufReader::new(src).lines() { |
162 | | // Remove comments from the line |
163 | 0 | let line = line?; |
164 | 0 | let line = match line.split_once('#') { |
165 | 0 | Some((line, _)) => line, |
166 | 0 | None => &line, |
167 | | } |
168 | 0 | .trim(); |
169 | | |
170 | 0 | if line.is_empty() { |
171 | 0 | continue; |
172 | 0 | } |
173 | | |
174 | 0 | let mut iter = line.split_whitespace(); |
175 | 0 | let addr = match iter.next() { |
176 | 0 | Some(addr) => match IpAddr::from_str(addr) { |
177 | 0 | Ok(addr) => RData::from(addr), |
178 | | Err(_) => { |
179 | 0 | warn!("could not parse an IP from hosts file ({addr:?})"); |
180 | 0 | continue; |
181 | | } |
182 | | }, |
183 | 0 | None => continue, |
184 | | }; |
185 | | |
186 | 0 | for domain in iter { |
187 | 0 | let domain = domain.to_lowercase(); |
188 | 0 | let Ok(mut name) = Name::from_str(&domain) else { |
189 | 0 | continue; |
190 | | }; |
191 | | |
192 | 0 | name.set_fqdn(true); |
193 | 0 | let record = Record::from_rdata(name.clone(), dns_lru::MAX_TTL, addr.clone()); |
194 | 0 | match addr { |
195 | 0 | RData::A(..) => { |
196 | 0 | let query = Query::query(name.clone(), RecordType::A); |
197 | 0 | let lookup = Lookup::new_with_max_ttl(query, Arc::from([record])); |
198 | 0 | self.insert(name.clone(), RecordType::A, lookup); |
199 | 0 | } |
200 | 0 | RData::AAAA(..) => { |
201 | 0 | let query = Query::query(name.clone(), RecordType::AAAA); |
202 | 0 | let lookup = Lookup::new_with_max_ttl(query, Arc::from([record])); |
203 | 0 | self.insert(name.clone(), RecordType::AAAA, lookup); |
204 | 0 | } |
205 | | _ => { |
206 | 0 | warn!("unsupported IP type from Hosts file: {:#?}", addr); |
207 | 0 | continue; |
208 | | } |
209 | | }; |
210 | | |
211 | | // TODO: insert reverse lookup as well. |
212 | | } |
213 | | } |
214 | | |
215 | 0 | Ok(()) |
216 | 0 | } |
217 | | } |
218 | | |
219 | | #[cfg(unix)] |
220 | 0 | fn hosts_path() -> &'static str { |
221 | 0 | "/etc/hosts" |
222 | 0 | } |
223 | | |
224 | | #[cfg(windows)] |
225 | | fn hosts_path() -> std::path::PathBuf { |
226 | | let system_root = |
227 | | std::env::var_os("SystemRoot").expect("Environment variable SystemRoot not found"); |
228 | | let system_root = Path::new(&system_root); |
229 | | system_root.join("System32\\drivers\\etc\\hosts") |
230 | | } |
231 | | |
232 | | #[cfg(any(unix, windows))] |
233 | | #[cfg(test)] |
234 | | mod tests { |
235 | | use super::*; |
236 | | use std::env; |
237 | | use std::net::{Ipv4Addr, Ipv6Addr}; |
238 | | |
239 | | fn tests_dir() -> String { |
240 | | let server_path = env::var("TDNS_WORKSPACE_ROOT").unwrap_or_else(|_| "../..".to_owned()); |
241 | | format! {"{server_path}/crates/resolver/tests"} |
242 | | } |
243 | | |
244 | | #[test] |
245 | | fn test_read_hosts_conf() { |
246 | | let path = format!("{}/hosts", tests_dir()); |
247 | | let hosts = Hosts::from_file(path).unwrap(); |
248 | | |
249 | | let name = Name::from_str("localhost.").unwrap(); |
250 | | let rdatas = hosts |
251 | | .lookup_static_host(&Query::query(name.clone(), RecordType::A)) |
252 | | .unwrap() |
253 | | .iter() |
254 | | .map(ToOwned::to_owned) |
255 | | .collect::<Vec<RData>>(); |
256 | | |
257 | | assert_eq!(rdatas, vec![RData::A(Ipv4Addr::LOCALHOST.into())]); |
258 | | |
259 | | let rdatas = hosts |
260 | | .lookup_static_host(&Query::query(name, RecordType::AAAA)) |
261 | | .unwrap() |
262 | | .iter() |
263 | | .map(ToOwned::to_owned) |
264 | | .collect::<Vec<RData>>(); |
265 | | |
266 | | assert_eq!( |
267 | | rdatas, |
268 | | vec![RData::AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into())] |
269 | | ); |
270 | | |
271 | | let name = Name::from_str("broadcasthost").unwrap(); |
272 | | let rdatas = hosts |
273 | | .lookup_static_host(&Query::query(name, RecordType::A)) |
274 | | .unwrap() |
275 | | .iter() |
276 | | .map(ToOwned::to_owned) |
277 | | .collect::<Vec<RData>>(); |
278 | | assert_eq!( |
279 | | rdatas, |
280 | | vec![RData::A(Ipv4Addr::new(255, 255, 255, 255).into())] |
281 | | ); |
282 | | |
283 | | let name = Name::from_str("example.com").unwrap(); |
284 | | let rdatas = hosts |
285 | | .lookup_static_host(&Query::query(name, RecordType::A)) |
286 | | .unwrap() |
287 | | .iter() |
288 | | .map(ToOwned::to_owned) |
289 | | .collect::<Vec<RData>>(); |
290 | | assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(10, 0, 1, 102).into())]); |
291 | | |
292 | | let name = Name::from_str("a.example.com").unwrap(); |
293 | | let rdatas = hosts |
294 | | .lookup_static_host(&Query::query(name, RecordType::A)) |
295 | | .unwrap() |
296 | | .iter() |
297 | | .map(ToOwned::to_owned) |
298 | | .collect::<Vec<RData>>(); |
299 | | assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(10, 0, 1, 111).into())]); |
300 | | |
301 | | let name = Name::from_str("b.example.com").unwrap(); |
302 | | let rdatas = hosts |
303 | | .lookup_static_host(&Query::query(name, RecordType::A)) |
304 | | .unwrap() |
305 | | .iter() |
306 | | .map(ToOwned::to_owned) |
307 | | .collect::<Vec<RData>>(); |
308 | | assert_eq!(rdatas, vec![RData::A(Ipv4Addr::new(10, 0, 1, 111).into())]); |
309 | | |
310 | | let name = Name::from_str("111.1.0.10.in-addr.arpa.").unwrap(); |
311 | | let mut rdatas = hosts |
312 | | .lookup_static_host(&Query::query(name, RecordType::PTR)) |
313 | | .unwrap() |
314 | | .iter() |
315 | | .map(ToOwned::to_owned) |
316 | | .collect::<Vec<RData>>(); |
317 | | rdatas.sort_by_key(|r| r.as_ptr().as_ref().map(|p| p.0.clone())); |
318 | | assert_eq!( |
319 | | rdatas, |
320 | | vec![ |
321 | | RData::PTR(PTR("a.example.com.".parse().unwrap())), |
322 | | RData::PTR(PTR("b.example.com.".parse().unwrap())) |
323 | | ] |
324 | | ); |
325 | | |
326 | | let name = Name::from_str( |
327 | | "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.", |
328 | | ) |
329 | | .unwrap(); |
330 | | let rdatas = hosts |
331 | | .lookup_static_host(&Query::query(name, RecordType::PTR)) |
332 | | .unwrap() |
333 | | .iter() |
334 | | .map(ToOwned::to_owned) |
335 | | .collect::<Vec<RData>>(); |
336 | | assert_eq!( |
337 | | rdatas, |
338 | | vec![RData::PTR(PTR("localhost.".parse().unwrap())),] |
339 | | ); |
340 | | } |
341 | | } |