///|
pub suberror WireError {
Malformed(String)
}
///|
pub(all) enum RData {
A(Bytes)
Aaaa(Bytes)
Ptr(@model.DnsName)
Srv(Int32, Int32, Int32, @model.DnsName)
Txt(@model.TxtRecord)
Unknown(Bytes)
}
///|
pub(all) enum Section {
Answer
Authority
Additional
}
///|
pub(all) struct Question {
name : @model.DnsName
rr_type : Int32
rr_class : Int32
unicast_response : Bool
}
///|
pub(all) struct Record {
name : @model.DnsName
rr_type : Int32
rr_class : Int32
cache_flush : Bool
ttl : UInt32
data : RData
section : Section
}
///|
pub(all) struct Message {
id : Int32
flags : Int32
questions : Array[Question]
records : Array[Record]
}
///|
fn u16(data : Bytes, at : Int32) -> Int32 {
(data[at].to_int() << 8) | data[at + 1].to_int()
}
///|
fn u32(data : Bytes, at : Int32) -> UInt32 {
((u16(data, at) << 16) | u16(data, at + 2)).reinterpret_as_uint()
}
///|
/// Decode a compressed name; cap dereferences and enforce octet limits.
pub fn decode_name(
data : Bytes,
start : Int32,
) -> (@model.DnsName, Int32) raise WireError {
guard data.length() <= 9000 && start >= 0 && start < data.length() else {
raise Malformed("name offset/packet length out of bounds")
}
let labels : Array[Bytes] = []
let visited : Array[Int32] = []
let mut at = start
let mut next = -1
let mut size = 1
for _ in 0..<256 {
guard at < data.length() else { raise Malformed("truncated name") }
guard !visited.contains(at) else {
raise Malformed("compression pointer cycle")
}
visited.push(at)
let length = data[at].to_int()
if length == 0 {
let name = @model.DnsName::from_labels(labels) catch {
_ => raise Malformed("invalid decoded name")
}
return (name, if next < 0 { at + 1 } else { next })
}
if (length & 0xc0) == 0xc0 {
guard at + 1 < data.length() else {
raise Malformed("truncated compression pointer")
}
let target = ((length & 0x3f) << 8) | data[at + 1].to_int()
guard target < at else {
raise Malformed("compression pointer must refer backwards")
}
if next < 0 {
next = at + 2
}
at = target
} else {
guard length <= 63 else { raise Malformed("unsupported label encoding") }
guard length < data.length() - at else {
raise Malformed("truncated label")
}
size += length + 1
guard size <= 256 else {
raise Malformed("expanded mDNS name exceeds 255 octets plus terminator")
}
labels.push(data[at + 1:at + 1 + length].to_owned())
at += length + 1
}
}
raise Malformed("name decode step limit (256)")
}
///|
fn rdata(
data : Bytes,
rr_type : Int32,
at : Int32,
end : Int32,
) -> RData raise WireError {
match rr_type {
1 => {
guard end - at == 4 else { raise Malformed("A must contain 4 octets") }
A(data[at:end].to_owned())
}
28 => {
guard end - at == 16 else {
raise Malformed("AAAA must contain 16 octets")
}
Aaaa(data[at:end].to_owned())
}
12 => {
let (name, next) = decode_name(data, at)
guard next == end else { raise Malformed("PTR RDLENGTH mismatch") }
Ptr(name)
}
33 => {
guard end - at >= 7 else { raise Malformed("SRV too short") }
let (target, next) = decode_name(data, at + 6)
guard next == end else { raise Malformed("SRV RDLENGTH mismatch") }
Srv(u16(data, at), u16(data, at + 2), u16(data, at + 4), target)
}
16 => {
let txt = @model.TxtRecord::from_wire(data[at:end].to_owned()) catch {
_ => raise Malformed("malformed TXT RDATA")
}
Txt(txt)
}
_ => Unknown(data[at:end].to_owned())
}
}
///|
/// Limited DNS-SD decoder. Unknown RR stays opaque; no cache acceptance implied.
pub fn decode(data : Bytes) -> Message raise WireError {
guard data.length() >= 12 && data.length() <= 9000 else {
raise Malformed("packet length outside 12..9000")
}
let qd = u16(data, 4)
let an = u16(data, 6)
let ns = u16(data, 8)
let ar = u16(data, 10)
guard qd + an + ns + ar <= 256 else {
raise Malformed("question/record limit (256)")
}
let questions : Array[Question] = []
let records : Array[Record] = []
let mut at = 12
for _ in 0..= 4 else { raise Malformed("truncated question") }
let raw_class = u16(data, at + 2)
questions.push({
name,
rr_type: u16(data, at),
rr_class: raw_class & 0x7fff,
unicast_response: (raw_class & 0x8000) != 0,
})
at += 4
}
for i in 0..<(an + ns + ar) {
let (name, next) = decode_name(data, at)
at = next
guard data.length() - at >= 10 else {
raise Malformed("truncated RR header")
}
let rr_type = u16(data, at)
let raw_class = u16(data, at + 2)
let ttl = u32(data, at + 4)
let length = u16(data, at + 8)
at += 10
guard length <= data.length() - at else {
raise Malformed("truncated RDATA")
}
let payload = rdata(data, rr_type, at, at + length)
records.push({
name,
rr_type,
rr_class: raw_class & 0x7fff,
cache_flush: (raw_class & 0x8000) != 0,
ttl,
data: payload,
section: if i < an {
Answer
} else if i < an + ns {
Authority
} else {
Additional
},
})
at += length
}
guard at == data.length() else { raise Malformed("trailing packet octets") }
{ id: u16(data, 0), flags: u16(data, 2), questions, records, }
}
///|
pub fn qm_query(
name : @model.DnsName,
rr_type : Int32,
) -> Bytes raise WireError {
guard rr_type > 0 && rr_type <= 65535 else {
raise Malformed("question type outside 1..65535")
}
let bytes = [
b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x01', b'\x00', b'\x00', b'\x00',
b'\x00', b'\x00', b'\x00',
]
for b in name.to_wire() {
bytes.push(b)
}
bytes.push((rr_type >> 8).to_byte())
bytes.push((rr_type & 255).to_byte())
bytes.push(0)
bytes.push(1)
Bytes::makei(bytes.length(), i => bytes[i])
}