Coverage Report

Created: 2025-12-28 06:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}