Skip to main content

pharos_plugins/
nsid.rs

1//! NSID Directive.
2
3use pharos_corefile::{Cursor, Error as CorefileError};
4use pharos_proto::{EdnsOption, Message, Type};
5use pharos_server::{Context, Error, Next, Outcome, Plugin};
6
7#[derive(Debug)]
8pub struct Nsid {
9    data: Vec<u8>,
10}
11
12impl Nsid {
13    pub(crate) fn parse(cursor: &mut Cursor<'_>) -> Result<Self, CorefileError> {
14        let mut data = nix::unistd::gethostname()
15            .ok()
16            .map(|value| value.into_encoded_bytes())
17            .unwrap_or_else(|| b"localhost".to_vec());
18        if cursor.next() {
19            let args = cursor.remaining_args();
20            if !args.is_empty() {
21                data = args.join(" ").into_bytes();
22            }
23        }
24        if cursor.next() {
25            return Err(cursor.error("nsid can only be used once"));
26        }
27        Ok(Self { data })
28    }
29}
30
31impl Plugin for Nsid {
32    async fn serve<N: Next + Send>(&self, cx: &mut Context<'_>, next: N) -> Outcome {
33        // RFC 5001 §2.2: return NSID only when the Query contains the NSID option.
34        let requested = cx.query.opt().is_some_and(|opt| {
35            opt.options()
36                .iter()
37                .any(|option| matches!(option, EdnsOption::Nsid(_)))
38        });
39        let outcome = next.run(cx).await;
40        if outcome == Outcome::Response
41            && let Err(error) = update_nsid(cx, requested.then_some(self.data.as_slice()))
42        {
43            return Outcome::Error(error);
44        }
45        outcome
46    }
47}
48
49fn update_nsid(cx: &mut Context<'_>, data: Option<&[u8]>) -> Result<(), Error> {
50    let message = Message::parse(cx.response).map_err(|_| Error::InvalidQuery)?;
51    // RFC 5001 §2.2: an NSID from Next must not reach a client that did not request it.
52    if data.is_none()
53        && !message.opt().is_some_and(|opt| {
54            opt.options()
55                .iter()
56                .any(|option| matches!(option, EdnsOption::Nsid(_)))
57        })
58    {
59        return Ok(());
60    }
61    let record = message
62        .additionals()
63        .enumerate()
64        .find(|(_, record)| record.rtype == Type::OPT);
65    let mut options = [0u8; 65535];
66    let mut len = 0;
67    let mut found = false;
68    if let Some((_, record)) = record {
69        let mut remaining = record.rdata;
70        while !remaining.is_empty() {
71            let code = u16::from_be_bytes([remaining[0], remaining[1]]);
72            let size = usize::from(u16::from_be_bytes([remaining[2], remaining[3]])) + 4;
73            let option = &remaining[..size];
74            if code == 3 {
75                if let Some(data) = data {
76                    append_option(&mut options, &mut len, data)?;
77                }
78                found = true;
79            } else {
80                let end = len
81                    .checked_add(size)
82                    .filter(|end| *end <= options.len())
83                    .ok_or(Error::ResponseBufferTooSmall)?;
84                options[len..end].copy_from_slice(option);
85                len = end;
86            }
87            remaining = &remaining[size..];
88        }
89    }
90    if !found && let Some(data) = data {
91        append_option(&mut options, &mut len, data)?;
92    }
93    // RFC 6891 §6.1.2: OPT RDLENGTH is the combined length of its options.
94    let len_wire = u16::try_from(len).map_err(|_| Error::ResponseBufferTooSmall)?;
95    let (start, old_end) = record.map_or((cx.response.len(), cx.response.len()), |(_, rr)| {
96        (rr.rdata_offset, rr.rdata_offset + rr.rdata.len())
97    });
98    let new_len = start
99        .checked_add(len + if record.is_none() { 11 } else { 0 })
100        .filter(|n| *n <= 65535 && *n <= cx.response.capacity())
101        .ok_or(Error::ResponseBufferTooSmall)?;
102    if record.is_none() {
103        let count = message
104            .additional_count()
105            .checked_add(1)
106            .ok_or(Error::ResponseBufferTooSmall)?;
107        let size = cx
108            .query
109            .opt()
110            .ok_or(Error::InvalidQuery)?
111            .udp_payload_size();
112        cx.response.extend_from_slice(&[0, 0, 41]);
113        cx.response.extend_from_slice(&size.to_be_bytes());
114        cx.response.extend_from_slice(&0x8000_u32.to_be_bytes());
115        cx.response.extend_from_slice(&len_wire.to_be_bytes());
116        cx.response.extend_from_slice(&options[..len]);
117        cx.response[10..12].copy_from_slice(&count.to_be_bytes());
118    } else {
119        let (index, _) = record.ok_or(Error::InvalidQuery)?;
120        if old_end < cx.response.len() {
121            // shortcut: records after OPT are dropped; re-encode the Message to retain them.
122            // RFC 1035 §4.1.4: removing later records avoids invalidating compression pointers.
123            cx.response[2] |= 0x02;
124            cx.response[10..12].copy_from_slice(&((index + 1) as u16).to_be_bytes());
125        }
126        if new_len > cx.response.len() {
127            cx.response.resize(new_len, 0);
128        }
129        cx.response.truncate(new_len);
130        cx.response[start - 2..start].copy_from_slice(&len_wire.to_be_bytes());
131        cx.response[start..start + len].copy_from_slice(&options[..len]);
132    }
133    Ok(())
134}
135
136fn append_option(options: &mut [u8], len: &mut usize, data: &[u8]) -> Result<(), Error> {
137    let end = len
138        .checked_add(4)
139        .and_then(|n| n.checked_add(data.len()))
140        .filter(|end| *end <= options.len())
141        .ok_or(Error::ResponseBufferTooSmall)?;
142    let size = u16::try_from(data.len()).map_err(|_| Error::ResponseBufferTooSmall)?;
143    // RFC 5001 §2.3: NSID is EDNS option code 3 with an opaque byte payload.
144    options[*len..*len + 2].copy_from_slice(&3_u16.to_be_bytes());
145    options[*len + 2..*len + 4].copy_from_slice(&size.to_be_bytes());
146    options[*len + 4..end].copy_from_slice(data);
147    *len = end;
148    Ok(())
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use pharos_proto::Name;
155    use pharos_server::{Metadata, Protocol, Transport};
156
157    struct Return(Outcome);
158
159    impl Next for Return {
160        async fn run(self, _cx: &mut Context<'_>) -> Outcome {
161            self.0
162        }
163    }
164
165    #[tokio::test]
166    async fn strips_unsolicited_nsid_and_preserves_other_options() {
167        let query = [
168            0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 0,
169        ];
170        let mut response = Vec::with_capacity(512);
171        response.extend_from_slice(&[
172            0, 1, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 10, 0, 3, 0, 1,
173            1, 0, 12, 0, 1, 9,
174        ]);
175        let domain: Name = ".".parse().unwrap();
176        let mut slots = [];
177        let mut cx = Context {
178            query: Message::parse(&query).unwrap(),
179            client: "127.0.0.1:53".parse().unwrap(),
180            local: "127.0.0.1:53".parse().unwrap(),
181            transport: Transport::Dns,
182            protocol: Protocol::Udp,
183            domain: &domain,
184            metadata: Metadata::new(&mut slots),
185            response: &mut response,
186            max_response_size: 512,
187        };
188        let plugin = Nsid {
189            data: b"NSID".to_vec(),
190        };
191        assert_eq!(
192            plugin.serve(&mut cx, Return(Outcome::Response)).await,
193            Outcome::Response
194        );
195        let message = Message::parse(&response).unwrap();
196        assert_eq!(
197            message.opt().unwrap().options().iter().collect::<Vec<_>>(),
198            [EdnsOption::Padding(&[9])]
199        );
200    }
201
202    #[tokio::test]
203    async fn preserves_nonresponse_outcomes() {
204        let query = [
205            0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0,
206        ];
207        let domain: Name = ".".parse().unwrap();
208        let plugin = Nsid {
209            data: b"NSID".to_vec(),
210        };
211        for outcome in [
212            Outcome::Drop,
213            Outcome::Unanswered(pharos_proto::Rcode::REFUSED),
214            Outcome::Error(Error::NoNextPlugin),
215            Outcome::Stream(pharos_server::Stream),
216        ] {
217            let mut response = Vec::with_capacity(512);
218            let mut slots = [];
219            let mut cx = Context {
220                query: Message::parse(&query).unwrap(),
221                client: "127.0.0.1:53".parse().unwrap(),
222                local: "127.0.0.1:53".parse().unwrap(),
223                transport: Transport::Dns,
224                protocol: Protocol::Udp,
225                domain: &domain,
226                metadata: Metadata::new(&mut slots),
227                response: &mut response,
228                max_response_size: 512,
229            };
230            assert_eq!(plugin.serve(&mut cx, Return(outcome)).await, outcome);
231            assert!(response.is_empty());
232        }
233    }
234
235    #[test]
236    fn replaces_all_existing_nsid_options_and_preserves_others() {
237        let query = [
238            0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0,
239        ];
240        let mut response = Vec::with_capacity(512);
241        response.extend_from_slice(&[
242            0, 1, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 15, 0, 3, 0, 1,
243            1, 0, 12, 0, 1, 9, 0, 3, 0, 1, 2,
244        ]);
245        let domain: Name = ".".parse().unwrap();
246        let mut slots = [];
247        let mut cx = Context {
248            query: Message::parse(&query).unwrap(),
249            client: "127.0.0.1:53".parse().unwrap(),
250            local: "127.0.0.1:53".parse().unwrap(),
251            transport: Transport::Dns,
252            protocol: Protocol::Udp,
253            domain: &domain,
254            metadata: Metadata::new(&mut slots),
255            response: &mut response,
256            max_response_size: 512,
257        };
258        update_nsid(&mut cx, Some(b"NSID")).unwrap();
259        let message = Message::parse(&response).unwrap();
260        assert_eq!(
261            message.opt().unwrap().options().iter().collect::<Vec<_>>(),
262            [
263                EdnsOption::Nsid(b"NSID"),
264                EdnsOption::Padding(&[9]),
265                EdnsOption::Nsid(b"NSID")
266            ]
267        );
268    }
269
270    #[test]
271    fn rejects_oversized_identifier_without_mutating_response() {
272        let query = [
273            0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0,
274        ];
275        let mut response = Vec::with_capacity(65535);
276        response.extend_from_slice(&[0, 1, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
277        let original = response.clone();
278        let domain: Name = ".".parse().unwrap();
279        let mut slots = [];
280        let mut cx = Context {
281            query: Message::parse(&query).unwrap(),
282            client: "127.0.0.1:53".parse().unwrap(),
283            local: "127.0.0.1:53".parse().unwrap(),
284            transport: Transport::Dns,
285            protocol: Protocol::Udp,
286            domain: &domain,
287            metadata: Metadata::new(&mut slots),
288            response: &mut response,
289            max_response_size: 512,
290        };
291        assert_eq!(
292            update_nsid(&mut cx, Some(&vec![b'x'; 65535])),
293            Err(Error::ResponseBufferTooSmall)
294        );
295        assert_eq!(response, original);
296    }
297
298    #[test]
299    fn truncates_records_after_opt_before_replacing_nsid() {
300        let query = [
301            0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 4, 0, 3, 0, 0,
302        ];
303        let mut response = Vec::with_capacity(512);
304        response.extend_from_slice(&[
305            0, 1, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 0, 0xc0, 12, 0,
306            1, 0, 1, 0, 0, 0, 0, 0, 4, 127, 0, 0, 1,
307        ]);
308        let domain: Name = ".".parse().unwrap();
309        let mut slots = [];
310        let mut cx = Context {
311            query: Message::parse(&query).unwrap(),
312            client: "127.0.0.1:53".parse().unwrap(),
313            local: "127.0.0.1:53".parse().unwrap(),
314            transport: Transport::Dns,
315            protocol: Protocol::Udp,
316            domain: &domain,
317            metadata: Metadata::new(&mut slots),
318            response: &mut response,
319            max_response_size: 512,
320        };
321        update_nsid(&mut cx, Some(b"NSID")).unwrap();
322        let message = Message::parse(&response).unwrap();
323        assert!(message.tc());
324        assert_eq!(message.additional_count(), 1);
325        assert_eq!(
326            message.opt().unwrap().options().iter().next(),
327            Some(EdnsOption::Nsid(b"NSID"))
328        );
329    }
330}