1use anyhow::{bail, Context, Result};
23use std::net::Ipv4Addr;
24
25pub const HEADER_LEN: usize = 12;
27
28pub const NAME_LEN: usize = 16;
30
31pub const ENCODED_NAME_LEN: usize = 32;
33
34pub const MAX_DATAGRAM: usize = 576;
36
37pub const QTYPE_NB: u16 = 0x0020;
41pub const QTYPE_NBSTAT: u16 = 0x0021;
43pub const RRTYPE_NULL: u16 = 0x000A;
45pub const CLASS_IN: u16 = 0x0001;
47
48pub const OPCODE_QUERY: u16 = 0;
51pub const OPCODE_REGISTRATION: u16 = 5;
52pub const OPCODE_RELEASE: u16 = 6;
53pub const OPCODE_WACK: u16 = 7;
54pub const OPCODE_REFRESH: u16 = 8;
55
56pub const FLAG_RESPONSE: u16 = 0x8000;
60pub const NM_FLAG_AA: u16 = 0x0400;
62pub const NM_FLAG_TC: u16 = 0x0200;
64pub const NM_FLAG_RD: u16 = 0x0100;
66pub const NM_FLAG_RA: u16 = 0x0080;
68pub const NM_FLAG_B: u16 = 0x0010;
70
71const OPCODE_SHIFT: u16 = 11;
72const OPCODE_MASK: u16 = 0x7800;
73const RCODE_MASK: u16 = 0x000F;
74
75pub const RCODE_OK: u16 = 0;
78pub const RCODE_FMT_ERR: u16 = 0x1;
80pub const RCODE_SRV_ERR: u16 = 0x2;
82pub const RCODE_NAM_ERR: u16 = 0x3;
84pub const RCODE_IMP_ERR: u16 = 0x4;
86pub const RCODE_RFS_ERR: u16 = 0x5;
88pub const RCODE_ACT_ERR: u16 = 0x6;
90pub const RCODE_CFT_ERR: u16 = 0x7;
92
93pub fn rcode_by_name(name: &str) -> Option<u16> {
96 match name.trim().to_ascii_lowercase().replace('-', "_").as_str() {
97 "ok" | "no_error" | "success" => Some(RCODE_OK),
98 "format_error" | "fmt_err" => Some(RCODE_FMT_ERR),
99 "server_failure" | "srv_err" => Some(RCODE_SRV_ERR),
100 "name_not_found" | "nam_err" | "name_error" => Some(RCODE_NAM_ERR),
101 "unsupported_request" | "imp_err" => Some(RCODE_IMP_ERR),
102 "refused" | "rfs_err" => Some(RCODE_RFS_ERR),
103 "name_active" | "act_err" => Some(RCODE_ACT_ERR),
104 "name_conflict" | "cft_err" => Some(RCODE_CFT_ERR),
105 _ => None,
106 }
107}
108
109pub const RCODE_NAMES: &[&str] = &[
111 "format_error",
112 "server_failure",
113 "name_not_found",
114 "unsupported_request",
115 "refused",
116 "name_active",
117 "name_conflict",
118];
119
120pub const NB_FLAG_GROUP: u16 = 0x8000;
124const ONT_SHIFT: u16 = 13;
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum NodeType {
129 B,
131 P,
133 M,
135 H,
137}
138
139impl NodeType {
140 pub fn ont_bits(self) -> u16 {
142 let value = match self {
143 NodeType::B => 0,
144 NodeType::P => 1,
145 NodeType::M => 2,
146 NodeType::H => 3,
147 };
148 value << ONT_SHIFT
149 }
150
151 pub fn parse(s: &str) -> Option<Self> {
153 match s.trim().to_ascii_lowercase().trim_end_matches("-node") {
154 "b" | "broadcast" => Some(NodeType::B),
155 "p" | "point-to-point" | "peer" => Some(NodeType::P),
156 "m" | "mixed" => Some(NodeType::M),
157 "h" | "hybrid" => Some(NodeType::H),
158 _ => None,
159 }
160 }
161
162 pub fn as_str(self) -> &'static str {
163 match self {
164 NodeType::B => "b",
165 NodeType::P => "p",
166 NodeType::M => "m",
167 NodeType::H => "h",
168 }
169 }
170}
171
172pub const NAME_FLAG_GROUP: u16 = 0x8000;
176pub const NAME_FLAG_ACTIVE: u16 = 0x0400;
179
180pub const STATISTICS_LEN: usize = 46;
182
183pub fn encode_first_level(name: &[u8; NAME_LEN]) -> [u8; ENCODED_NAME_LEN] {
196 let mut out = [0u8; ENCODED_NAME_LEN];
197 for (i, byte) in name.iter().enumerate() {
198 out[i * 2] = b'A' + (byte >> 4);
199 out[i * 2 + 1] = b'A' + (byte & 0x0F);
200 }
201 out
202}
203
204pub fn decode_first_level(encoded: &[u8]) -> Result<[u8; NAME_LEN]> {
209 if encoded.len() != ENCODED_NAME_LEN {
210 bail!(
211 "first-level encoded name must be {} octets, got {}",
212 ENCODED_NAME_LEN,
213 encoded.len()
214 );
215 }
216 let mut out = [0u8; NAME_LEN];
217 for i in 0..NAME_LEN {
218 let hi = encoded[i * 2];
219 let lo = encoded[i * 2 + 1];
220 if !(b'A'..=b'P').contains(&hi) || !(b'A'..=b'P').contains(&lo) {
221 bail!(
222 "first-level encoded name contains a character outside A-P at offset {}: {:?}",
223 i * 2,
224 String::from_utf8_lossy(encoded)
225 );
226 }
227 out[i] = ((hi - b'A') << 4) | (lo - b'A');
228 }
229 Ok(out)
230}
231
232pub const WILDCARD_NAME: &str = "*";
234
235pub fn pad_netbios_name(name: &str, suffix: u8) -> Result<[u8; NAME_LEN]> {
247 let bytes = name.as_bytes();
248 if bytes.len() > NAME_LEN - 1 {
249 bail!(
250 "NetBIOS name '{}' is {} characters; the wire format allows at most {} \
251 (the 16th octet is the service suffix)",
252 name,
253 bytes.len(),
254 NAME_LEN - 1
255 );
256 }
257 if !bytes.is_ascii() {
258 bail!("NetBIOS name '{}' must be ASCII", name);
259 }
260 let pad = if name == WILDCARD_NAME { 0x00 } else { b' ' };
261 let mut out = [pad; NAME_LEN];
262 out[..bytes.len()].copy_from_slice(bytes);
263 out[NAME_LEN - 1] = suffix;
264 Ok(out)
265}
266
267pub fn split_netbios_name(raw: &[u8; NAME_LEN]) -> (String, u8) {
273 let name = String::from_utf8_lossy(&raw[..NAME_LEN - 1])
274 .trim_end_matches([' ', '\0'])
275 .to_string();
276 (name, raw[NAME_LEN - 1])
277}
278
279pub fn encode_name_field(name: &str, suffix: u8, scope: Option<&str>) -> Result<Vec<u8>> {
282 let raw = pad_netbios_name(name, suffix)?;
283 let encoded = encode_first_level(&raw);
284
285 let mut out = Vec::with_capacity(1 + ENCODED_NAME_LEN + 2);
286 out.push(ENCODED_NAME_LEN as u8);
287 out.extend_from_slice(&encoded);
288
289 if let Some(scope) = scope {
290 for label in scope.split('.').filter(|l| !l.is_empty()) {
291 if label.len() > 63 {
292 bail!("scope label '{}' exceeds 63 octets", label);
293 }
294 out.push(label.len() as u8);
295 out.extend_from_slice(label.as_bytes());
296 }
297 }
298 out.push(0);
299 Ok(out)
300}
301
302#[derive(Debug, Clone)]
304pub struct NameField {
305 pub raw: Vec<u8>,
308 pub name: String,
310 pub suffix: u8,
312 pub scope: Option<String>,
314 pub end: usize,
316}
317
318pub fn read_name_field(buf: &[u8], pos: usize) -> Result<NameField> {
324 let mut cursor = pos;
325 let mut first: Option<[u8; NAME_LEN]> = None;
326 let mut scope_labels: Vec<String> = Vec::new();
327
328 loop {
329 let len = *buf
330 .get(cursor)
331 .context("datagram ended inside a NetBIOS NAME field")?;
332 if len & 0xC0 == 0xC0 {
333 bail!("compressed (pointer) NetBIOS NAME field is not supported");
334 }
335 if len & 0xC0 != 0 {
336 bail!("reserved label length bits set in a NetBIOS NAME field");
337 }
338 cursor += 1;
339 if len == 0 {
340 break;
341 }
342 let end = cursor + len as usize;
343 let label = buf
344 .get(cursor..end)
345 .context("datagram ended inside a NetBIOS NAME label")?;
346 if first.is_none() {
347 if len as usize != ENCODED_NAME_LEN {
348 bail!(
349 "first NetBIOS label is {} octets; first-level encoding always produces {}",
350 len,
351 ENCODED_NAME_LEN
352 );
353 }
354 first = Some(decode_first_level(label)?);
355 } else {
356 scope_labels.push(String::from_utf8_lossy(label).into_owned());
357 }
358 cursor = end;
359 }
360
361 let raw16 = first.context("NetBIOS NAME field carried no name label")?;
362 let (name, suffix) = split_netbios_name(&raw16);
363
364 Ok(NameField {
365 raw: buf[pos..cursor].to_vec(),
366 name,
367 suffix,
368 scope: if scope_labels.is_empty() {
369 None
370 } else {
371 Some(scope_labels.join("."))
372 },
373 end: cursor,
374 })
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382pub struct Header {
383 pub trn_id: u16,
384 pub flags: u16,
385 pub qdcount: u16,
386 pub ancount: u16,
387 pub nscount: u16,
388 pub arcount: u16,
389}
390
391impl Header {
392 pub fn parse(buf: &[u8]) -> Result<Self> {
393 if buf.len() < HEADER_LEN {
394 bail!(
395 "NBNS datagram is {} octets; the header alone is {}",
396 buf.len(),
397 HEADER_LEN
398 );
399 }
400 let at = |i: usize| u16::from_be_bytes([buf[i], buf[i + 1]]);
401 Ok(Self {
402 trn_id: at(0),
403 flags: at(2),
404 qdcount: at(4),
405 ancount: at(6),
406 nscount: at(8),
407 arcount: at(10),
408 })
409 }
410
411 pub fn encode(&self) -> [u8; HEADER_LEN] {
412 let mut out = [0u8; HEADER_LEN];
413 out[0..2].copy_from_slice(&self.trn_id.to_be_bytes());
414 out[2..4].copy_from_slice(&self.flags.to_be_bytes());
415 out[4..6].copy_from_slice(&self.qdcount.to_be_bytes());
416 out[6..8].copy_from_slice(&self.ancount.to_be_bytes());
417 out[8..10].copy_from_slice(&self.nscount.to_be_bytes());
418 out[10..12].copy_from_slice(&self.arcount.to_be_bytes());
419 out
420 }
421
422 pub fn opcode(&self) -> u16 {
423 (self.flags & OPCODE_MASK) >> OPCODE_SHIFT
424 }
425
426 pub fn is_response(&self) -> bool {
427 self.flags & FLAG_RESPONSE != 0
428 }
429
430 pub fn rcode(&self) -> u16 {
431 self.flags & RCODE_MASK
432 }
433
434 pub fn recursion_desired(&self) -> bool {
435 self.flags & NM_FLAG_RD != 0
436 }
437
438 pub fn broadcast(&self) -> bool {
439 self.flags & NM_FLAG_B != 0
440 }
441}
442
443pub fn opcode_name(opcode: u16) -> &'static str {
445 match opcode {
446 OPCODE_QUERY => "QUERY",
447 OPCODE_REGISTRATION => "REGISTRATION",
448 OPCODE_RELEASE => "RELEASE",
449 OPCODE_WACK => "WACK",
450 OPCODE_REFRESH => "REFRESH",
451 _ => "UNKNOWN",
452 }
453}
454
455pub fn qtype_name(qtype: u16) -> &'static str {
457 match qtype {
458 QTYPE_NB => "NB",
459 QTYPE_NBSTAT => "NBSTAT",
460 RRTYPE_NULL => "NULL",
461 _ => "UNKNOWN",
462 }
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471pub struct AddressEntry {
472 pub flags: u16,
473 pub address: Ipv4Addr,
474}
475
476impl AddressEntry {
477 pub fn is_group(&self) -> bool {
478 self.flags & NB_FLAG_GROUP != 0
479 }
480}
481
482#[derive(Debug, Clone)]
484pub struct NbnsRequest {
485 pub header: Header,
486 pub question_name: NameField,
487 pub qtype: u16,
488 pub qclass: u16,
489 pub addresses: Vec<AddressEntry>,
491}
492
493pub fn parse_request(buf: &[u8]) -> Result<NbnsRequest> {
498 if buf.len() > MAX_DATAGRAM {
499 bail!(
500 "NBNS datagram is {} octets; RFC 1002 §4.1 caps it at {}",
501 buf.len(),
502 MAX_DATAGRAM
503 );
504 }
505 let header = Header::parse(buf)?;
506 if header.is_response() {
507 bail!("datagram is a response (R=1), not a request");
508 }
509 if header.qdcount != 1 {
510 bail!(
511 "NBNS request must carry exactly one question, QDCOUNT={}",
512 header.qdcount
513 );
514 }
515
516 let question_name = read_name_field(buf, HEADER_LEN)?;
517 let after_name = question_name.end;
518 let qtype = u16::from_be_bytes([
519 *buf.get(after_name)
520 .context("question truncated before TYPE")?,
521 *buf.get(after_name + 1)
522 .context("question truncated inside TYPE")?,
523 ]);
524 let qclass = u16::from_be_bytes([
525 *buf.get(after_name + 2)
526 .context("question truncated before CLASS")?,
527 *buf.get(after_name + 3)
528 .context("question truncated inside CLASS")?,
529 ]);
530
531 let mut addresses = Vec::new();
536 if header.arcount > 0 {
537 let mut cursor = after_name + 4;
538 for _ in 0..header.arcount {
539 let Ok(rr_name) = read_name_field(buf, cursor) else {
540 break;
541 };
542 let mut p = rr_name.end;
543 if p + 10 > buf.len() {
545 break;
546 }
547 let rr_type = u16::from_be_bytes([buf[p], buf[p + 1]]);
548 let rdlength = u16::from_be_bytes([buf[p + 8], buf[p + 9]]) as usize;
549 p += 10;
550 if p + rdlength > buf.len() {
551 break;
552 }
553 if rr_type == QTYPE_NB {
554 let mut q = p;
555 while q + 6 <= p + rdlength {
556 let flags = u16::from_be_bytes([buf[q], buf[q + 1]]);
557 let address = Ipv4Addr::new(buf[q + 2], buf[q + 3], buf[q + 4], buf[q + 5]);
558 addresses.push(AddressEntry { flags, address });
559 q += 6;
560 }
561 }
562 cursor = p + rdlength;
563 }
564 }
565
566 Ok(NbnsRequest {
567 header,
568 question_name,
569 qtype,
570 qclass,
571 addresses,
572 })
573}
574
575fn response_flags(opcode: u16, recursion_desired: bool, rcode: u16) -> u16 {
581 let mut flags = FLAG_RESPONSE | ((opcode << OPCODE_SHIFT) & OPCODE_MASK) | NM_FLAG_AA;
582 if recursion_desired {
583 flags |= NM_FLAG_RD | NM_FLAG_RA;
585 }
586 flags | (rcode & RCODE_MASK)
587}
588
589pub fn encode_name_query_response(
594 trn_id: u16,
595 opcode: u16,
596 recursion_desired: bool,
597 name_field: &[u8],
598 addresses: &[AddressEntry],
599 ttl: u32,
600) -> Result<Vec<u8>> {
601 if addresses.is_empty() {
602 bail!("a positive NetBIOS name response must carry at least one address");
603 }
604 let header = Header {
605 trn_id,
606 flags: response_flags(opcode, recursion_desired, RCODE_OK),
607 qdcount: 0,
608 ancount: 1,
609 nscount: 0,
610 arcount: 0,
611 };
612
613 let rdlength = addresses.len() * 6;
614 let rdlength = u16::try_from(rdlength).context("too many addresses for one response")?;
615
616 let mut out = Vec::with_capacity(HEADER_LEN + name_field.len() + 10 + rdlength as usize);
617 out.extend_from_slice(&header.encode());
618 out.extend_from_slice(name_field);
619 out.extend_from_slice(&QTYPE_NB.to_be_bytes());
620 out.extend_from_slice(&CLASS_IN.to_be_bytes());
621 out.extend_from_slice(&ttl.to_be_bytes());
622 out.extend_from_slice(&rdlength.to_be_bytes());
623 for entry in addresses {
624 out.extend_from_slice(&entry.flags.to_be_bytes());
625 out.extend_from_slice(&entry.address.octets());
626 }
627 Ok(out)
628}
629
630pub fn encode_negative_response(
634 trn_id: u16,
635 opcode: u16,
636 recursion_desired: bool,
637 name_field: &[u8],
638 rcode: u16,
639) -> Result<Vec<u8>> {
640 if rcode == RCODE_OK {
641 bail!("a negative NetBIOS response must carry a non-zero RCODE");
642 }
643 let header = Header {
644 trn_id,
645 flags: response_flags(opcode, recursion_desired, rcode),
646 qdcount: 0,
647 ancount: 1,
648 nscount: 0,
649 arcount: 0,
650 };
651
652 let mut out = Vec::with_capacity(HEADER_LEN + name_field.len() + 10);
653 out.extend_from_slice(&header.encode());
654 out.extend_from_slice(name_field);
655 out.extend_from_slice(&RRTYPE_NULL.to_be_bytes());
656 out.extend_from_slice(&CLASS_IN.to_be_bytes());
657 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); Ok(out)
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq)]
664pub struct NodeName {
665 pub raw: [u8; NAME_LEN],
669 pub flags: u16,
670}
671
672pub fn encode_node_status_response(
679 trn_id: u16,
680 name_field: &[u8],
681 names: &[NodeName],
682 mac: [u8; 6],
683) -> Result<Vec<u8>> {
684 if names.is_empty() {
685 bail!("a node status response must list at least one name");
686 }
687 let num_names = u8::try_from(names.len())
688 .context("a node status response can list at most 255 names (NUM_NAMES is one octet)")?;
689
690 let rdlength = 1 + names.len() * 18 + STATISTICS_LEN;
691 let rdlength = u16::try_from(rdlength).context("node status RDATA exceeds 65535 octets")?;
692
693 let header = Header {
696 trn_id,
697 flags: FLAG_RESPONSE | NM_FLAG_AA,
698 qdcount: 0,
699 ancount: 1,
700 nscount: 0,
701 arcount: 0,
702 };
703
704 let mut out = Vec::with_capacity(HEADER_LEN + name_field.len() + 10 + rdlength as usize);
705 out.extend_from_slice(&header.encode());
706 out.extend_from_slice(name_field);
707 out.extend_from_slice(&QTYPE_NBSTAT.to_be_bytes());
708 out.extend_from_slice(&CLASS_IN.to_be_bytes());
709 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&rdlength.to_be_bytes());
711 out.push(num_names);
712 for entry in names {
713 out.extend_from_slice(&entry.raw);
714 out.extend_from_slice(&entry.flags.to_be_bytes());
715 }
716 let mut statistics = [0u8; STATISTICS_LEN];
717 statistics[..6].copy_from_slice(&mac);
718 out.extend_from_slice(&statistics);
719
720 if out.len() > MAX_DATAGRAM {
721 bail!(
722 "node status response is {} octets, over the {} octet limit; list fewer names",
723 out.len(),
724 MAX_DATAGRAM
725 );
726 }
727 Ok(out)
728}
729
730pub fn parse_suffix_value(value: Option<&serde_json::Value>) -> Result<u8> {
744 let Some(value) = value else { return Ok(0) };
745 match value {
746 serde_json::Value::Null => Ok(0),
747 serde_json::Value::Number(n) => {
748 let n = n
749 .as_u64()
750 .context("'suffix' must be a whole number between 0 and 255")?;
751 u8::try_from(n).context("'suffix' must be between 0 and 255")
752 }
753 serde_json::Value::String(s) => {
754 let trimmed = s.trim();
755 match trimmed
756 .strip_prefix("0x")
757 .or_else(|| trimmed.strip_prefix("0X"))
758 {
759 Some(hex) => u8::from_str_radix(hex, 16).with_context(|| {
760 format!("'suffix' string '{s}' is not a hex octet (0x00-0xff)")
761 }),
762 None => bail!(
763 "'suffix' string '{s}' is ambiguous: NetBIOS suffixes are conventionally \
764 written in hex, but a bare string of digits reads as decimal, so '{s}' \
765 could mean two different names. Give a number (32) or an explicit hex \
766 string (\"0x20\")."
767 ),
768 }
769 }
770 other => bail!("'suffix' must be a number (32) or a hex string (\"0x20\"), got {other}"),
771 }
772}
773
774pub fn parse_mac(text: &str) -> Result<[u8; 6]> {
779 let parts: Vec<&str> = text.split(['-', ':']).collect();
780 if parts.len() != 6 {
781 bail!(
782 "MAC address '{}' must have six octets separated by ':' or '-', e.g. \
783 \"00:11:22:33:44:55\"",
784 text
785 );
786 }
787 let mut out = [0u8; 6];
788 for (i, part) in parts.iter().enumerate() {
789 out[i] = u8::from_str_radix(part, 16)
790 .with_context(|| format!("'{}' in MAC address '{}' is not a hex octet", part, text))?;
791 }
792 Ok(out)
793}
794
795pub fn format_mac(mac: &[u8; 6]) -> String {
797 mac.iter()
798 .map(|b| format!("{:02x}", b))
799 .collect::<Vec<_>>()
800 .join(":")
801}