1use std::{
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex},
6 time::Duration,
7};
8
9use pharos_corefile::{Cursor, Error, Fallthrough, origins_from_args_or_server_block};
10use pharos_proto::{Class, MessageBuilder, Name, NameRef, Opcode, Rcode, Section, Type};
11use pharos_server::{Context, Next, Outcome, Protocol};
12
13mod data;
14
15#[derive(Debug, Default)]
16struct State {
17 entries: arc_swap::ArcSwap<data::Map>,
18 modified: Mutex<Option<(std::time::SystemTime, u64)>>,
19 task: Mutex<Option<tokio::task::JoinHandle<()>>>,
20 metrics: Mutex<Option<pharos_server::metrics::HostsMetrics>>,
21 inline_len: Mutex<usize>,
22}
23
24#[derive(Debug)]
25pub struct Hosts {
26 pub path: PathBuf,
27 pub domains: Vec<String>,
28 pub fallthrough: Fallthrough,
29 pub fallthrough_unsupported: bool,
30 pub ttl: u32,
31 pub reload: Duration,
32 pub auto_reverse: bool,
33 pub inline: Vec<String>,
34 domain_names: Vec<Name>,
35 fallthrough_names: Vec<Name>,
36 state: Arc<State>,
37 inline_entries: data::Map,
38}
39
40impl Hosts {
41 pub(crate) fn set_metrics(&self, registry: &pharos_server::metrics::Registry) {
42 *self.state.metrics.lock().unwrap() = Some(registry.hosts(&self.path.to_string_lossy()));
43 *self.state.inline_len.lock().unwrap() = self.inline_entries.len();
44 }
45 pub(crate) fn register_hooks(&self, lifecycle: &mut pharos_server::Lifecycle) {
46 let state = Arc::clone(&self.state);
47 let path = self.path.clone();
48 let domains = self.domain_names.clone();
49 let auto_reverse = self.auto_reverse;
50 let interval = self.reload;
51 let start = move || {
52 let state = Arc::clone(&state);
53 let path = path.clone();
54 let domains = domains.clone();
55 async move {
56 state.read(&path, &domains, auto_reverse);
57 if !interval.is_zero() {
58 let task_state = Arc::clone(&state);
59 let task = tokio::spawn(async move {
60 loop {
61 tokio::time::sleep(interval).await;
62 task_state.read(&path, &domains, auto_reverse);
63 }
64 });
65 if let Some(old) = state.task.lock().unwrap().replace(task) {
66 old.abort();
67 }
68 }
69 Ok(())
70 }
71 };
72 lifecycle.register(pharos_server::Phase::Startup, start.clone());
73 lifecycle.register(pharos_server::Phase::RestartFailed, start);
74 let state = Arc::clone(&self.state);
75 let stop = move || {
76 let task = state.task.lock().unwrap().take();
77 async move {
78 if let Some(task) = task {
79 task.abort();
80 let _ = task.await;
81 }
82 Ok(())
83 }
84 };
85 lifecycle.register(pharos_server::Phase::Restart, stop.clone());
86 lifecycle.register(pharos_server::Phase::Shutdown, stop);
87 }
88
89 pub fn parse(cursor: &mut Cursor<'_>, server_domains: &[&str]) -> Result<Self, Error> {
90 cursor.next();
91 let args = cursor.remaining_args();
92 let path = PathBuf::from(args.first().map_or("/etc/hosts", String::as_str));
94 if !args.is_empty() {
95 match std::fs::metadata(&path) {
96 Ok(metadata) if metadata.is_dir() => pharos_server::logging::log(
97 "WARN",
98 "hosts",
99 &format!("Hosts file {:?} is a directory", path),
100 ),
101 Ok(_) => {}
102 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
103 pharos_server::logging::log(
104 "WARN",
105 "hosts",
106 &format!("File does not exist: {}", path.display()),
107 )
108 }
109 Err(error) => {
110 return Err(cursor.error(format!(
111 "unable to access hosts file '{}': {error}",
112 path.display()
113 )));
114 }
115 }
116 }
117 let zones = args
118 .get(1..)
119 .unwrap_or_default()
120 .iter()
121 .map(String::as_str)
122 .collect::<Vec<_>>();
123 let mut hosts = Self {
124 path,
125 domains: origins_from_args_or_server_block(&zones, server_domains),
126 fallthrough: Fallthrough::default(),
127 fallthrough_unsupported: false,
128 ttl: 3600,
129 reload: Duration::from_secs(5),
130 auto_reverse: true,
131 inline: Vec::new(),
132 domain_names: Vec::new(),
133 fallthrough_names: Vec::new(),
134 state: Arc::new(State::default()),
135 inline_entries: data::Map::default(),
136 };
137 while cursor.next_block() {
138 let option = cursor.val().to_owned();
139 let args = cursor.remaining_args();
140 match option.as_str() {
141 "fallthrough" => hosts
142 .fallthrough
143 .set_domains_from_args(&args.iter().map(String::as_str).collect::<Vec<_>>()),
144 "fallthrough_unsupported" if args.is_empty() => {
145 hosts.fallthrough_unsupported = true
146 }
147 "fallthrough_unsupported" => return Err(cursor.arg_err()),
148 "no_reverse" => hosts.auto_reverse = false,
149 "ttl" => {
150 let value = args
151 .first()
152 .ok_or_else(|| cursor.error("ttl needs a time in seconds"))?;
153 let value = value
154 .parse::<u32>()
155 .map_err(|_| cursor.error("invalid ttl"))?;
156 if !(1..=65535).contains(&value) {
157 return Err(cursor.error("invalid ttl"));
158 }
159 hosts.ttl = value;
160 }
161 "reload" => {
162 if args.len() != 1 {
163 return Err(cursor.arg_err());
164 }
165 let value = &args[0];
166 let magnitude = value.strip_prefix('-').unwrap_or(value);
167 if value.starts_with('-') && magnitude.starts_with(['-', '+']) {
168 return Err(cursor.error("invalid reload duration"));
169 }
170 let duration = crate::health::parse_duration(magnitude)
171 .ok_or_else(|| cursor.error("invalid reload duration"))?;
172 if value.starts_with('-') && !duration.is_zero() {
173 return Err(cursor.error("negative reload duration"));
174 }
175 hosts.reload = duration;
176 }
177 _ if hosts.fallthrough.domains.is_empty() => hosts.inline.push(
178 std::iter::once(option)
179 .chain(args)
180 .collect::<Vec<_>>()
181 .join(" "),
182 ),
183 _ => return Err(cursor.error(format!("unknown property '{option}'"))),
184 }
185 }
186 if hosts.fallthrough_unsupported && hosts.fallthrough.domains.is_empty() {
187 return Err(cursor.error("fallthrough_unsupported requires fallthrough"));
188 }
189 hosts.domain_names = hosts
190 .domains
191 .iter()
192 .filter_map(|s| s.parse().ok())
193 .collect();
194 hosts.fallthrough_names = hosts
195 .fallthrough
196 .domains
197 .iter()
198 .filter_map(|s| s.parse().ok())
199 .collect();
200 hosts.inline_entries = data::Map::parse(
201 hosts.inline.join("\n").as_bytes(),
202 &hosts.domain_names,
203 hosts.auto_reverse,
204 "Inline hosts entries",
205 );
206 Ok(hosts)
207 }
208}
209
210impl State {
211 fn read(&self, path: &Path, domains: &[Name], auto_reverse: bool) {
212 let Ok(file) = std::fs::File::open(path) else {
213 return;
214 };
215 let Ok(metadata) = file.metadata() else {
216 return;
217 };
218 let Ok(modified) = metadata.modified() else {
219 return;
220 };
221 let stamp = (modified, metadata.len());
222 let mut previous = self.modified.lock().unwrap();
223 if *previous == Some(stamp) {
224 return;
225 }
226 let map = data::Map::parse(file, domains, auto_reverse, &format!("Hosts file {path:?}"));
227 if let Some(metrics) = self.metrics.lock().unwrap().as_ref() {
228 let timestamp = modified
229 .duration_since(std::time::UNIX_EPOCH)
230 .map_or(0.0, |duration| duration.as_secs_f64());
231 metrics.update(map.len() + *self.inline_len.lock().unwrap(), timestamp);
232 }
233 self.entries.store(Arc::new(map));
234 *previous = Some(stamp);
235 }
236}
237
238impl pharos_server::Plugin for Hosts {
239 async fn serve<N: Next + Send>(&self, cx: &mut Context<'_>, next: N) -> Outcome {
240 let Some(question) = cx.query.questions().next() else {
241 return next.run(cx).await;
242 };
243 let mut wire = [0u8; 255];
245 let mut len = 0;
246 for label in question.name.labels() {
248 wire[len] = label.len() as u8;
249 len += 1;
250 for byte in label {
251 wire[len] = byte.to_ascii_lowercase();
252 len += 1;
253 }
254 }
255 len += 1;
256 let key = &wire[..len];
257 if question.qtype != Type::PTR && !contains(key, &self.domain_names) {
258 return next.run(cx).await;
259 }
260 let fallthrough = contains(key, &self.fallthrough_names);
261 let entries = self.state.entries.load_full();
262 let ptr = if question.qtype == Type::PTR {
263 reverse_address(question.name)
264 } else {
265 None
266 };
267 let names1 = ptr.and_then(|ip| entries.reverse.get(&ip));
268 let names2 = ptr.and_then(|ip| self.inline_entries.reverse.get(&ip));
269 if question.qtype == Type::PTR && names1.is_none() && names2.is_none() {
270 return next.run(cx).await;
271 }
272 if question.qtype != Type::A
273 && question.qtype != Type::AAAA
274 && question.qtype != Type::PTR
275 && self.fallthrough_unsupported
276 && fallthrough
277 {
278 return next.run(cx).await;
279 }
280 let v4 = entries.lookup_key(key, true);
281 let v6 = entries.lookup_key(key, false);
282 let inline4 = self.inline_entries.lookup_key(key, true);
283 let inline6 = self.inline_entries.lookup_key(key, false);
284 let ips = if question.qtype == Type::A {
285 (v4, inline4)
286 } else if question.qtype == Type::AAAA {
287 (v6, inline6)
288 } else {
289 (&[][..], &[][..])
290 };
291 if ips.0.is_empty()
292 && ips.1.is_empty()
293 && names1.is_none()
294 && names2.is_none()
295 && v4.is_empty()
296 && v6.is_empty()
297 && inline4.is_empty()
298 && inline6.is_empty()
299 {
300 return if fallthrough {
301 next.run(cx).await
302 } else {
303 Outcome::Unanswered(Rcode::SERVFAIL)
304 };
305 }
306 let max = cx.max_response_size.max(512);
307 if cx.response.capacity() < max {
308 if cx.protocol == Protocol::Tcp {
309 cx.response.reserve(max.saturating_sub(cx.response.len()));
310 } else {
311 return Outcome::Error(pharos_server::Error::ResponseBufferTooSmall);
312 }
313 }
314 let mut flags = 0x8400 | (cx.query.flags() & 0x7800);
315 if cx.query.opcode() == Opcode::QUERY {
316 flags |= cx.query.flags() & 0x0110;
317 }
318 let result = (|| -> Result<(), pharos_server::Error> {
319 let mut builder = MessageBuilder::new(cx.response, max, cx.query.id(), flags)
320 .map_err(pharos_server::Error::Build)?;
321 builder
322 .question(question.name, question.qtype, question.qclass)
323 .map_err(pharos_server::Error::Build)?;
324 for ip in ips.0.iter().chain(ips.1) {
325 match ip {
326 std::net::IpAddr::V4(ip) => builder.record(
328 Section::Answer,
329 question.name,
330 question.qtype,
331 Class::IN,
332 self.ttl,
333 &ip.octets(),
334 ),
335 std::net::IpAddr::V6(ip) => builder.record(
337 Section::Answer,
338 question.name,
339 question.qtype,
340 Class::IN,
341 self.ttl,
342 &ip.octets(),
343 ),
344 }
345 .map_err(pharos_server::Error::Build)?;
346 }
347 for name in names1
349 .into_iter()
350 .flatten()
351 .chain(names2.into_iter().flatten())
352 {
353 builder
354 .record(
355 Section::Answer,
356 question.name,
357 Type::PTR,
358 Class::IN,
359 self.ttl,
360 name.as_wire(),
361 )
362 .map_err(pharos_server::Error::Build)?;
363 }
364 let _ = builder.finish();
365 Ok(())
366 })();
367 result.map_or_else(Outcome::Error, |()| Outcome::Response)
368 }
369}
370
371fn contains(mut key: &[u8], domains: &[Name]) -> bool {
373 loop {
374 if domains
375 .iter()
376 .any(|domain| key.eq_ignore_ascii_case(domain.as_wire()))
377 {
378 return true;
379 }
380 if key[0] == 0 {
381 return false;
382 }
383 key = &key[1 + usize::from(key[0])..];
384 }
385}
386
387fn reverse_address(name: NameRef<'_>) -> Option<std::net::IpAddr> {
388 let mut labels = [&[][..]; 34];
389 let mut count = 0;
390 for label in name.labels() {
391 if count == labels.len() {
392 return None;
393 }
394 labels[count] = label;
395 count += 1;
396 }
397 if count == 6
399 && labels[4].eq_ignore_ascii_case(b"in-addr")
400 && labels[5].eq_ignore_ascii_case(b"arpa")
401 {
402 let mut bytes = [0; 4];
403 for (i, label) in labels[..4].iter().enumerate() {
404 if label.is_empty()
405 || (label.len() > 1 && label[0] == b'0')
406 || !label.iter().all(u8::is_ascii_digit)
407 {
408 return None;
409 }
410 bytes[3 - i] = std::str::from_utf8(label).ok()?.parse().ok()?;
411 }
412 return Some(std::net::Ipv4Addr::from(bytes).into());
413 }
414 if count == 34
416 && labels[32].eq_ignore_ascii_case(b"ip6")
417 && labels[33].eq_ignore_ascii_case(b"arpa")
418 {
419 let mut bytes = [0; 16];
420 for (i, label) in labels[..32].iter().enumerate() {
421 if label.len() != 1 {
422 return None;
423 }
424 let digit = (label[0] as char).to_digit(16)? as u8;
425 bytes[15 - i / 2] |= digit << ((i % 2) * 4);
426 }
427 let ip = std::net::Ipv6Addr::from(bytes);
428 return Some(
429 ip.to_ipv4_mapped()
430 .map_or(std::net::IpAddr::V6(ip), std::net::IpAddr::V4),
431 );
432 }
433 None
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn reverse_ipv4_requires_canonical_octets() {
442 for (input, expected) in [
443 ("1.0.0.127.in-addr.arpa.", true),
444 ("01.0.0.127.in-addr.arpa.", false),
445 ("+1.0.0.127.in-addr.arpa.", false),
446 ] {
447 let name: Name = input.parse().unwrap();
448 assert_eq!(
449 reverse_address(NameRef::parse(name.as_wire(), 0).unwrap()).is_some(),
450 expected,
451 "{input}"
452 );
453 }
454 }
455}