1use 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 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 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 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 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 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}