Skip to main content

pharos_plugins/
local.rs

1//! Localhost and locally served reverse Domains.
2
3use std::sync::{
4    Arc,
5    atomic::{AtomicU64, Ordering},
6};
7
8use pharos_corefile::{Cursor, Error as CorefileError};
9use pharos_proto::{Class, MessageBuilder, NameRef, Opcode, Section, Type};
10use pharos_server::{Context, Error, Next, Outcome, Plugin, Protocol, metrics::Registry};
11
12const TTL: u32 = 604_800;
13const LOCALHOST: &[u8] = b"\x09localhost\0";
14// RFC 1035 §3.3.13: SOA RDATA contains MNAME, RNAME and five 32-bit values.
15const SOA: &[u8] =
16    b"\x09localhost\0\x04root\x09localhost\0\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\0\0\x09\x3a\x80";
17
18#[derive(Debug)]
19pub struct Local {
20    prefix: bool,
21    counter: Option<Arc<AtomicU64>>,
22}
23
24impl Local {
25    pub(crate) fn parse(cursor: &mut Cursor<'_>) -> Result<Self, CorefileError> {
26        cursor.next();
27        if !cursor.remaining_args().is_empty() {
28            return Err(cursor.arg_err());
29        }
30        let mut prefix = true;
31        while cursor.next_block() {
32            if cursor.val() != "localhost_prefix" {
33                return Err(cursor.error("unknown local property"));
34            }
35            let args = cursor.remaining_args();
36            prefix = match args.as_slice() {
37                [value] if value == "on" => true,
38                [value] if value == "off" => false,
39                _ => return Err(cursor.arg_err()),
40            };
41        }
42        Ok(Self {
43            prefix,
44            counter: None,
45        })
46    }
47
48    pub(crate) fn set_metrics(&mut self, registry: &Registry) {
49        self.counter = Some(registry.local());
50    }
51}
52
53impl Plugin for Local {
54    async fn serve<N: Next + Send>(&self, cx: &mut Context<'_>, next: N) -> Outcome {
55        let Some(question) = cx.query.questions().next() else {
56            return next.run(cx).await;
57        };
58        let mut labels = [&[][..]; 127];
59        let mut count = 0;
60        for label in question.name.labels() {
61            labels[count] = label;
62            count += 1;
63        }
64        // RFC 4343 §2: compare DNS labels without ASCII case sensitivity.
65        let eq = |i: usize, value: &[u8]| labels[i].eq_ignore_ascii_case(value);
66        // RFC 6761 §6.3: localhost and its subdomains resolve to loopback.
67        let special =
68            count > 1 && (eq(count - 1, b"localhost") || (self.prefix && eq(0, b"localhost")));
69        // RFC 6303 §4.2: serve the 0 and 127 IPv4 reverse Domains locally.
70        let zone_start = if count > 0 && eq(count - 1, b"localhost") {
71            Some(count - 1)
72        } else if count >= 3
73            && eq(count - 2, b"in-addr")
74            && eq(count - 1, b"arpa")
75            && [b"0".as_slice(), b"127", b"255"]
76                .iter()
77                .any(|z| eq(count - 3, z))
78        {
79            Some(count - 3)
80        } else {
81            None
82        };
83        if !special && zone_start.is_none() {
84            return next.run(cx).await;
85        }
86        if special {
87            if pharos_server::logging::debug_enabled() {
88                pharos_server::logging::log_debug(
89                    "local",
90                    format_args!(
91                        "Intercepting localhost query for \"{}\" {}, from {}",
92                        question.name,
93                        question.qtype,
94                        cx.client.ip()
95                    ),
96                );
97            }
98            if let Some(counter) = &self.counter {
99                counter.fetch_add(1, Ordering::Relaxed);
100            }
101        }
102        let apex = zone_start == Some(0);
103        let localhost_apex = apex && eq(0, b"localhost");
104        let reverse_ptr = count == 6
105            && eq(0, b"1")
106            && eq(1, b"0")
107            && eq(2, b"0")
108            && eq(3, b"127")
109            && eq(4, b"in-addr")
110            && eq(5, b"arpa");
111        // RFC 1034 §4.3.2: select records matching the Question type.
112        let answer_type = if special || localhost_apex {
113            match question.qtype {
114                Type::A | Type::AAAA => Some(question.qtype),
115                Type::NS | Type::SOA if !special => Some(question.qtype),
116                _ => None,
117            }
118        } else if apex && matches!(question.qtype, Type::NS | Type::SOA) {
119            Some(question.qtype)
120        } else if reverse_ptr && question.qtype == Type::PTR {
121            Some(Type::PTR)
122        } else {
123            None
124        };
125        // RFC 2308 §2: distinguish name errors from existing names without matching records.
126        let nxdomain = !special && !apex && !reverse_ptr;
127        // RFC 1035 §3.1: encode Domain names as length-prefixed labels ending in zero.
128        let mut origin = [0u8; 255];
129        let mut len = 0;
130        if !special {
131            for label in &labels[zone_start.unwrap()..count] {
132                origin[len] = label.len() as u8;
133                len += 1;
134                origin[len..len + label.len()].copy_from_slice(label);
135                len += label.len();
136            }
137        }
138        let owner = if special || apex {
139            question.name
140        } else {
141            NameRef::parse(&origin[..=len], 0).expect("valid Domain")
142        };
143        let result = (|| {
144            let max = cx.max_response_size.max(512);
145            if cx.response.capacity() < max {
146                if cx.protocol == Protocol::Tcp {
147                    cx.response.reserve(max.saturating_sub(cx.response.len()));
148                } else {
149                    return Err(Error::ResponseBufferTooSmall);
150                }
151            }
152            // RFC 1035 §4.1.1 and RFC 4035 §3.2.1: mark a Response and copy opcode, RD and CD.
153            let mut flags = 0x8000 | (cx.query.flags() & 0x7800) | if nxdomain { 3 } else { 0 };
154            if cx.query.opcode() == Opcode::QUERY {
155                flags |= cx.query.flags() & 0x0110;
156            }
157            let mut builder = MessageBuilder::new(cx.response, max, cx.query.id(), flags)
158                .map_err(Error::Build)?;
159            builder
160                .question(question.name, question.qtype, question.qclass)
161                .map_err(Error::Build)?;
162            let rtype = answer_type.unwrap_or(Type::SOA);
163            let rdata: &[u8] = match rtype {
164                Type::A => &[127, 0, 0, 1],
165                Type::AAAA => &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
166                Type::NS | Type::PTR => LOCALHOST,
167                _ => SOA,
168            };
169            builder
170                .record(
171                    if answer_type.is_some() {
172                        Section::Answer
173                    } else {
174                        Section::Authority
175                    },
176                    if answer_type.is_some() {
177                        question.name
178                    } else {
179                        owner
180                    },
181                    rtype,
182                    Class::IN,
183                    TTL,
184                    rdata,
185                )
186                .map_err(Error::Build)?;
187            let _ = builder.finish();
188            Ok(())
189        })();
190        match result {
191            Ok(()) => Outcome::Response,
192            Err(error) => Outcome::Error(error),
193        }
194    }
195}