use anyhow::{bail, Context, Result};
use std::net::Ipv4Addr;
pub const HEADER_LEN: usize = 12;
pub const NAME_LEN: usize = 16;
pub const ENCODED_NAME_LEN: usize = 32;
pub const MAX_DATAGRAM: usize = 576;
pub const QTYPE_NB: u16 = 0x0020;
pub const QTYPE_NBSTAT: u16 = 0x0021;
pub const RRTYPE_NULL: u16 = 0x000A;
pub const CLASS_IN: u16 = 0x0001;
pub const OPCODE_QUERY: u16 = 0;
pub const OPCODE_REGISTRATION: u16 = 5;
pub const OPCODE_RELEASE: u16 = 6;
pub const OPCODE_WACK: u16 = 7;
pub const OPCODE_REFRESH: u16 = 8;
pub const FLAG_RESPONSE: u16 = 0x8000;
pub const NM_FLAG_AA: u16 = 0x0400;
pub const NM_FLAG_TC: u16 = 0x0200;
pub const NM_FLAG_RD: u16 = 0x0100;
pub const NM_FLAG_RA: u16 = 0x0080;
pub const NM_FLAG_B: u16 = 0x0010;
const OPCODE_SHIFT: u16 = 11;
const OPCODE_MASK: u16 = 0x7800;
const RCODE_MASK: u16 = 0x000F;
pub const RCODE_OK: u16 = 0;
pub const RCODE_FMT_ERR: u16 = 0x1;
pub const RCODE_SRV_ERR: u16 = 0x2;
pub const RCODE_NAM_ERR: u16 = 0x3;
pub const RCODE_IMP_ERR: u16 = 0x4;
pub const RCODE_RFS_ERR: u16 = 0x5;
pub const RCODE_ACT_ERR: u16 = 0x6;
pub const RCODE_CFT_ERR: u16 = 0x7;
pub fn rcode_by_name(name: &str) -> Option<u16> {
match name.trim().to_ascii_lowercase().replace('-', "_").as_str() {
"ok" | "no_error" | "success" => Some(RCODE_OK),
"format_error" | "fmt_err" => Some(RCODE_FMT_ERR),
"server_failure" | "srv_err" => Some(RCODE_SRV_ERR),
"name_not_found" | "nam_err" | "name_error" => Some(RCODE_NAM_ERR),
"unsupported_request" | "imp_err" => Some(RCODE_IMP_ERR),
"refused" | "rfs_err" => Some(RCODE_RFS_ERR),
"name_active" | "act_err" => Some(RCODE_ACT_ERR),
"name_conflict" | "cft_err" => Some(RCODE_CFT_ERR),
_ => None,
}
}
pub const RCODE_NAMES: &[&str] = &[
"format_error",
"server_failure",
"name_not_found",
"unsupported_request",
"refused",
"name_active",
"name_conflict",
];
pub const NB_FLAG_GROUP: u16 = 0x8000;
const ONT_SHIFT: u16 = 13;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeType {
B,
P,
M,
H,
}
impl NodeType {
pub fn ont_bits(self) -> u16 {
let value = match self {
NodeType::B => 0,
NodeType::P => 1,
NodeType::M => 2,
NodeType::H => 3,
};
value << ONT_SHIFT
}
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().trim_end_matches("-node") {
"b" | "broadcast" => Some(NodeType::B),
"p" | "point-to-point" | "peer" => Some(NodeType::P),
"m" | "mixed" => Some(NodeType::M),
"h" | "hybrid" => Some(NodeType::H),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
NodeType::B => "b",
NodeType::P => "p",
NodeType::M => "m",
NodeType::H => "h",
}
}
}
pub const NAME_FLAG_GROUP: u16 = 0x8000;
pub const NAME_FLAG_ACTIVE: u16 = 0x0400;
pub const STATISTICS_LEN: usize = 46;
pub fn encode_first_level(name: &[u8; NAME_LEN]) -> [u8; ENCODED_NAME_LEN] {
let mut out = [0u8; ENCODED_NAME_LEN];
for (i, byte) in name.iter().enumerate() {
out[i * 2] = b'A' + (byte >> 4);
out[i * 2 + 1] = b'A' + (byte & 0x0F);
}
out
}
pub fn decode_first_level(encoded: &[u8]) -> Result<[u8; NAME_LEN]> {
if encoded.len() != ENCODED_NAME_LEN {
bail!(
"first-level encoded name must be {} octets, got {}",
ENCODED_NAME_LEN,
encoded.len()
);
}
let mut out = [0u8; NAME_LEN];
for i in 0..NAME_LEN {
let hi = encoded[i * 2];
let lo = encoded[i * 2 + 1];
if !(b'A'..=b'P').contains(&hi) || !(b'A'..=b'P').contains(&lo) {
bail!(
"first-level encoded name contains a character outside A-P at offset {}: {:?}",
i * 2,
String::from_utf8_lossy(encoded)
);
}
out[i] = ((hi - b'A') << 4) | (lo - b'A');
}
Ok(out)
}
pub const WILDCARD_NAME: &str = "*";
pub fn pad_netbios_name(name: &str, suffix: u8) -> Result<[u8; NAME_LEN]> {
let bytes = name.as_bytes();
if bytes.len() > NAME_LEN - 1 {
bail!(
"NetBIOS name '{}' is {} characters; the wire format allows at most {} \
(the 16th octet is the service suffix)",
name,
bytes.len(),
NAME_LEN - 1
);
}
if !bytes.is_ascii() {
bail!("NetBIOS name '{}' must be ASCII", name);
}
let pad = if name == WILDCARD_NAME { 0x00 } else { b' ' };
let mut out = [pad; NAME_LEN];
out[..bytes.len()].copy_from_slice(bytes);
out[NAME_LEN - 1] = suffix;
Ok(out)
}
pub fn split_netbios_name(raw: &[u8; NAME_LEN]) -> (String, u8) {
let name = String::from_utf8_lossy(&raw[..NAME_LEN - 1])
.trim_end_matches([' ', '\0'])
.to_string();
(name, raw[NAME_LEN - 1])
}
pub fn encode_name_field(name: &str, suffix: u8, scope: Option<&str>) -> Result<Vec<u8>> {
let raw = pad_netbios_name(name, suffix)?;
let encoded = encode_first_level(&raw);
let mut out = Vec::with_capacity(1 + ENCODED_NAME_LEN + 2);
out.push(ENCODED_NAME_LEN as u8);
out.extend_from_slice(&encoded);
if let Some(scope) = scope {
for label in scope.split('.').filter(|l| !l.is_empty()) {
if label.len() > 63 {
bail!("scope label '{}' exceeds 63 octets", label);
}
out.push(label.len() as u8);
out.extend_from_slice(label.as_bytes());
}
}
out.push(0);
Ok(out)
}
#[derive(Debug, Clone)]
pub struct NameField {
pub raw: Vec<u8>,
pub name: String,
pub suffix: u8,
pub scope: Option<String>,
pub end: usize,
}
pub fn read_name_field(buf: &[u8], pos: usize) -> Result<NameField> {
let mut cursor = pos;
let mut first: Option<[u8; NAME_LEN]> = None;
let mut scope_labels: Vec<String> = Vec::new();
loop {
let len = *buf
.get(cursor)
.context("datagram ended inside a NetBIOS NAME field")?;
if len & 0xC0 == 0xC0 {
bail!("compressed (pointer) NetBIOS NAME field is not supported");
}
if len & 0xC0 != 0 {
bail!("reserved label length bits set in a NetBIOS NAME field");
}
cursor += 1;
if len == 0 {
break;
}
let end = cursor + len as usize;
let label = buf
.get(cursor..end)
.context("datagram ended inside a NetBIOS NAME label")?;
if first.is_none() {
if len as usize != ENCODED_NAME_LEN {
bail!(
"first NetBIOS label is {} octets; first-level encoding always produces {}",
len,
ENCODED_NAME_LEN
);
}
first = Some(decode_first_level(label)?);
} else {
scope_labels.push(String::from_utf8_lossy(label).into_owned());
}
cursor = end;
}
let raw16 = first.context("NetBIOS NAME field carried no name label")?;
let (name, suffix) = split_netbios_name(&raw16);
Ok(NameField {
raw: buf[pos..cursor].to_vec(),
name,
suffix,
scope: if scope_labels.is_empty() {
None
} else {
Some(scope_labels.join("."))
},
end: cursor,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
pub trn_id: u16,
pub flags: u16,
pub qdcount: u16,
pub ancount: u16,
pub nscount: u16,
pub arcount: u16,
}
impl Header {
pub fn parse(buf: &[u8]) -> Result<Self> {
if buf.len() < HEADER_LEN {
bail!(
"NBNS datagram is {} octets; the header alone is {}",
buf.len(),
HEADER_LEN
);
}
let at = |i: usize| u16::from_be_bytes([buf[i], buf[i + 1]]);
Ok(Self {
trn_id: at(0),
flags: at(2),
qdcount: at(4),
ancount: at(6),
nscount: at(8),
arcount: at(10),
})
}
pub fn encode(&self) -> [u8; HEADER_LEN] {
let mut out = [0u8; HEADER_LEN];
out[0..2].copy_from_slice(&self.trn_id.to_be_bytes());
out[2..4].copy_from_slice(&self.flags.to_be_bytes());
out[4..6].copy_from_slice(&self.qdcount.to_be_bytes());
out[6..8].copy_from_slice(&self.ancount.to_be_bytes());
out[8..10].copy_from_slice(&self.nscount.to_be_bytes());
out[10..12].copy_from_slice(&self.arcount.to_be_bytes());
out
}
pub fn opcode(&self) -> u16 {
(self.flags & OPCODE_MASK) >> OPCODE_SHIFT
}
pub fn is_response(&self) -> bool {
self.flags & FLAG_RESPONSE != 0
}
pub fn rcode(&self) -> u16 {
self.flags & RCODE_MASK
}
pub fn recursion_desired(&self) -> bool {
self.flags & NM_FLAG_RD != 0
}
pub fn broadcast(&self) -> bool {
self.flags & NM_FLAG_B != 0
}
}
pub fn opcode_name(opcode: u16) -> &'static str {
match opcode {
OPCODE_QUERY => "QUERY",
OPCODE_REGISTRATION => "REGISTRATION",
OPCODE_RELEASE => "RELEASE",
OPCODE_WACK => "WACK",
OPCODE_REFRESH => "REFRESH",
_ => "UNKNOWN",
}
}
pub fn qtype_name(qtype: u16) -> &'static str {
match qtype {
QTYPE_NB => "NB",
QTYPE_NBSTAT => "NBSTAT",
RRTYPE_NULL => "NULL",
_ => "UNKNOWN",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AddressEntry {
pub flags: u16,
pub address: Ipv4Addr,
}
impl AddressEntry {
pub fn is_group(&self) -> bool {
self.flags & NB_FLAG_GROUP != 0
}
}
#[derive(Debug, Clone)]
pub struct NbnsRequest {
pub header: Header,
pub question_name: NameField,
pub qtype: u16,
pub qclass: u16,
pub addresses: Vec<AddressEntry>,
}
pub fn parse_request(buf: &[u8]) -> Result<NbnsRequest> {
if buf.len() > MAX_DATAGRAM {
bail!(
"NBNS datagram is {} octets; RFC 1002 §4.1 caps it at {}",
buf.len(),
MAX_DATAGRAM
);
}
let header = Header::parse(buf)?;
if header.is_response() {
bail!("datagram is a response (R=1), not a request");
}
if header.qdcount != 1 {
bail!(
"NBNS request must carry exactly one question, QDCOUNT={}",
header.qdcount
);
}
let question_name = read_name_field(buf, HEADER_LEN)?;
let after_name = question_name.end;
let qtype = u16::from_be_bytes([
*buf.get(after_name)
.context("question truncated before TYPE")?,
*buf.get(after_name + 1)
.context("question truncated inside TYPE")?,
]);
let qclass = u16::from_be_bytes([
*buf.get(after_name + 2)
.context("question truncated before CLASS")?,
*buf.get(after_name + 3)
.context("question truncated inside CLASS")?,
]);
let mut addresses = Vec::new();
if header.arcount > 0 {
let mut cursor = after_name + 4;
for _ in 0..header.arcount {
let Ok(rr_name) = read_name_field(buf, cursor) else {
break;
};
let mut p = rr_name.end;
if p + 10 > buf.len() {
break;
}
let rr_type = u16::from_be_bytes([buf[p], buf[p + 1]]);
let rdlength = u16::from_be_bytes([buf[p + 8], buf[p + 9]]) as usize;
p += 10;
if p + rdlength > buf.len() {
break;
}
if rr_type == QTYPE_NB {
let mut q = p;
while q + 6 <= p + rdlength {
let flags = u16::from_be_bytes([buf[q], buf[q + 1]]);
let address = Ipv4Addr::new(buf[q + 2], buf[q + 3], buf[q + 4], buf[q + 5]);
addresses.push(AddressEntry { flags, address });
q += 6;
}
}
cursor = p + rdlength;
}
}
Ok(NbnsRequest {
header,
question_name,
qtype,
qclass,
addresses,
})
}
fn response_flags(opcode: u16, recursion_desired: bool, rcode: u16) -> u16 {
let mut flags = FLAG_RESPONSE | ((opcode << OPCODE_SHIFT) & OPCODE_MASK) | NM_FLAG_AA;
if recursion_desired {
flags |= NM_FLAG_RD | NM_FLAG_RA;
}
flags | (rcode & RCODE_MASK)
}
pub fn encode_name_query_response(
trn_id: u16,
opcode: u16,
recursion_desired: bool,
name_field: &[u8],
addresses: &[AddressEntry],
ttl: u32,
) -> Result<Vec<u8>> {
if addresses.is_empty() {
bail!("a positive NetBIOS name response must carry at least one address");
}
let header = Header {
trn_id,
flags: response_flags(opcode, recursion_desired, RCODE_OK),
qdcount: 0,
ancount: 1,
nscount: 0,
arcount: 0,
};
let rdlength = addresses.len() * 6;
let rdlength = u16::try_from(rdlength).context("too many addresses for one response")?;
let mut out = Vec::with_capacity(HEADER_LEN + name_field.len() + 10 + rdlength as usize);
out.extend_from_slice(&header.encode());
out.extend_from_slice(name_field);
out.extend_from_slice(&QTYPE_NB.to_be_bytes());
out.extend_from_slice(&CLASS_IN.to_be_bytes());
out.extend_from_slice(&ttl.to_be_bytes());
out.extend_from_slice(&rdlength.to_be_bytes());
for entry in addresses {
out.extend_from_slice(&entry.flags.to_be_bytes());
out.extend_from_slice(&entry.address.octets());
}
Ok(out)
}
pub fn encode_negative_response(
trn_id: u16,
opcode: u16,
recursion_desired: bool,
name_field: &[u8],
rcode: u16,
) -> Result<Vec<u8>> {
if rcode == RCODE_OK {
bail!("a negative NetBIOS response must carry a non-zero RCODE");
}
let header = Header {
trn_id,
flags: response_flags(opcode, recursion_desired, rcode),
qdcount: 0,
ancount: 1,
nscount: 0,
arcount: 0,
};
let mut out = Vec::with_capacity(HEADER_LEN + name_field.len() + 10);
out.extend_from_slice(&header.encode());
out.extend_from_slice(name_field);
out.extend_from_slice(&RRTYPE_NULL.to_be_bytes());
out.extend_from_slice(&CLASS_IN.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NodeName {
pub raw: [u8; NAME_LEN],
pub flags: u16,
}
pub fn encode_node_status_response(
trn_id: u16,
name_field: &[u8],
names: &[NodeName],
mac: [u8; 6],
) -> Result<Vec<u8>> {
if names.is_empty() {
bail!("a node status response must list at least one name");
}
let num_names = u8::try_from(names.len())
.context("a node status response can list at most 255 names (NUM_NAMES is one octet)")?;
let rdlength = 1 + names.len() * 18 + STATISTICS_LEN;
let rdlength = u16::try_from(rdlength).context("node status RDATA exceeds 65535 octets")?;
let header = Header {
trn_id,
flags: FLAG_RESPONSE | NM_FLAG_AA,
qdcount: 0,
ancount: 1,
nscount: 0,
arcount: 0,
};
let mut out = Vec::with_capacity(HEADER_LEN + name_field.len() + 10 + rdlength as usize);
out.extend_from_slice(&header.encode());
out.extend_from_slice(name_field);
out.extend_from_slice(&QTYPE_NBSTAT.to_be_bytes());
out.extend_from_slice(&CLASS_IN.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&rdlength.to_be_bytes());
out.push(num_names);
for entry in names {
out.extend_from_slice(&entry.raw);
out.extend_from_slice(&entry.flags.to_be_bytes());
}
let mut statistics = [0u8; STATISTICS_LEN];
statistics[..6].copy_from_slice(&mac);
out.extend_from_slice(&statistics);
if out.len() > MAX_DATAGRAM {
bail!(
"node status response is {} octets, over the {} octet limit; list fewer names",
out.len(),
MAX_DATAGRAM
);
}
Ok(out)
}
pub fn parse_suffix_value(value: Option<&serde_json::Value>) -> Result<u8> {
let Some(value) = value else { return Ok(0) };
match value {
serde_json::Value::Null => Ok(0),
serde_json::Value::Number(n) => {
let n = n
.as_u64()
.context("'suffix' must be a whole number between 0 and 255")?;
u8::try_from(n).context("'suffix' must be between 0 and 255")
}
serde_json::Value::String(s) => {
let trimmed = s.trim();
match trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
Some(hex) => u8::from_str_radix(hex, 16).with_context(|| {
format!("'suffix' string '{s}' is not a hex octet (0x00-0xff)")
}),
None => bail!(
"'suffix' string '{s}' is ambiguous: NetBIOS suffixes are conventionally \
written in hex, but a bare string of digits reads as decimal, so '{s}' \
could mean two different names. Give a number (32) or an explicit hex \
string (\"0x20\")."
),
}
}
other => bail!("'suffix' must be a number (32) or a hex string (\"0x20\"), got {other}"),
}
}
pub fn parse_mac(text: &str) -> Result<[u8; 6]> {
let parts: Vec<&str> = text.split(['-', ':']).collect();
if parts.len() != 6 {
bail!(
"MAC address '{}' must have six octets separated by ':' or '-', e.g. \
\"00:11:22:33:44:55\"",
text
);
}
let mut out = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
out[i] = u8::from_str_radix(part, 16)
.with_context(|| format!("'{}' in MAC address '{}' is not a hex octet", part, text))?;
}
Ok(out)
}
pub fn format_mac(mac: &[u8; 6]) -> String {
mac.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(":")
}