use domain::base::header::Flags;
use domain::base::iana::{Class, Opcode, Rcode, Rtype};
use domain::base::message_builder::MessageBuilder;
use domain::base::{Message, ParsedName, ToName, UnknownRecordData};
use domain::rdata::{Aaaa, Ptr, Srv, A};
use crate::error::Error;
use crate::transport::network::mdns::builtin::types::Buf;
use crate::transport::network::mdns::MdnsRemoteService;
use crate::transport::network::{IpAddr, Ipv4Addr, Ipv6Addr};
pub fn build_browse_query(name: impl ToName, buf: &mut [u8]) -> Result<usize, Error> {
build_query(name, Rtype::PTR, buf)
}
pub fn build_resolve_query(name: impl ToName, buf: &mut [u8]) -> Result<usize, Error> {
build_query(name, Rtype::ANY, buf)
}
fn build_query(name: impl ToName, rtype: Rtype, buf: &mut [u8]) -> Result<usize, Error> {
let buf = Buf::new(buf);
let message = MessageBuilder::from_target(buf)?;
let mut question = message.question();
let header = question.header_mut();
header.set_id(0); header.set_opcode(Opcode::QUERY);
header.set_rcode(Rcode::NOERROR);
let mut flags = Flags::new();
flags.qr = false; header.set_flags(flags);
question.push((name, rtype, Class::IN))?;
let buf = question.finish();
Ok(buf.1)
}
#[derive(Debug, Clone, Copy)]
pub struct MdnsAddrs<'a> {
msg: Message<&'a [u8]>,
target: Option<ParsedName<&'a [u8]>>,
yielded: usize,
}
impl Iterator for MdnsAddrs<'_> {
type Item = IpAddr;
fn next(&mut self) -> Option<IpAddr> {
let target = self.target?;
let mut seen = 0;
for section in [self.msg.answer(), self.msg.additional()] {
let Ok(section) = section else { continue };
for record in section {
let Ok(record) = record else { continue };
if record.owner() != target {
continue;
}
let addr = if let Ok(Some(rec)) = record.to_record::<A>() {
IpAddr::V4(Ipv4Addr::from(rec.data().addr().octets()))
} else if let Ok(Some(rec)) = record.to_record::<Aaaa>() {
IpAddr::V6(Ipv6Addr::from(rec.data().addr().octets()))
} else {
continue;
};
if seen == self.yielded {
self.yielded += 1;
return Some(addr);
}
seen += 1;
}
}
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct MdnsTxt<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> MdnsTxt<'a> {
const fn empty() -> Self {
Self { data: &[], pos: 0 }
}
const fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
}
impl<'a> Iterator for MdnsTxt<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<(&'a str, &'a str)> {
while self.pos < self.data.len() {
let len = self.data[self.pos] as usize;
let start = self.pos + 1;
let end = (start + len).min(self.data.len());
self.pos = end;
if let Ok(s) = core::str::from_utf8(&self.data[start..end]) {
if let Some(eq) = s.find('=') {
return Some((&s[..eq], &s[eq + 1..]));
}
}
}
None
}
}
#[allow(clippy::type_complexity)]
pub fn parse_into_answer(
data: &[u8],
ipv6_scope: Option<u32>,
) -> Result<Option<MdnsRemoteService<ParsedName<&[u8]>, MdnsAddrs<'_>, MdnsTxt<'_>>>, Error> {
let msg = Message::from_octets(data)?;
if !msg.header().flags().qr {
return Ok(None);
}
let mut instance: Option<ParsedName<&[u8]>> = None;
let mut hostname: Option<ParsedName<&[u8]>> = None;
let mut port: Option<u16> = None;
let mut have_srv = false;
for section in [msg.answer(), msg.additional()] {
let Ok(section) = section else { continue };
for record in section {
let Ok(record) = record else { continue };
if let Ok(Some(rec)) = record.to_record::<Srv<_>>() {
instance = Some(*rec.owner());
hostname = Some(*rec.data().target());
port = Some(rec.data().port());
have_srv = true;
} else if !have_srv && instance.is_none() {
if let Ok(Some(rec)) = record.to_record::<Ptr<_>>() {
instance = Some(*rec.data().ptrdname());
}
}
}
}
let Some(instance) = instance else {
return Ok(None);
};
let mut txt = MdnsTxt::empty();
'txt: for section in [msg.answer(), msg.additional()] {
let Ok(section) = section else { continue };
for record in section {
let Ok(record) = record else { continue };
if record.rtype() != Rtype::TXT || record.owner() != instance {
continue;
}
if let Ok(Some(rec)) = record.to_record::<UnknownRecordData<_>>() {
let &data = rec.data().data();
txt = MdnsTxt::new(data);
}
break 'txt;
}
}
Ok(Some(MdnsRemoteService {
instance_name: instance,
port,
addrs: MdnsAddrs {
msg,
target: hostname,
yielded: 0,
},
txt,
scope_id: ipv6_scope.unwrap_or(0),
}))
}
#[cfg(test)]
mod tests {
use super::*;
use core::fmt::Write as _;
use crate::transport::network::mdns::builtin::respond::Host;
use crate::transport::network::mdns::builtin::types::NameSlice;
use crate::transport::network::mdns::MdnsLocalService;
use crate::transport::network::Ipv6Addr;
use domain::base::Message;
fn name_str(name: ParsedName<&[u8]>) -> heapless::String<64> {
let mut s = heapless::String::new();
write!(s, "{}", name).unwrap();
while s.ends_with('.') {
s.pop();
}
s
}
fn host() -> Host<'static> {
Host {
hostname: "myhost",
ip: Ipv4Addr::new(192, 168, 1, 5),
ipv6: Ipv6Addr::UNSPECIFIED,
}
}
fn commissionable_response(buf: &mut [u8], subtypes: &[&str]) -> usize {
let service = MdnsLocalService {
name: "ABCD1234",
service: "_matterc",
protocol: "_udp",
service_protocol: "_matterc._udp",
port: 5540,
service_subtypes: subtypes.iter().copied(),
txt_kvs: [("D", "1234"), ("VP", "65521+32769")].into_iter(),
};
host().broadcast(&service, buf, 60, 60).unwrap()
}
#[test]
fn build_query_is_a_query() {
let mut buf = [0u8; 512];
let len =
build_browse_query(NameSlice::new(["_matterc", "_udp", "local"]), &mut buf).unwrap();
assert!(len > 0);
let message = Message::from_octets(&buf[..len]).unwrap();
assert!(!message.header().flags().qr); assert_eq!(message.header().opcode(), Opcode::QUERY);
}
#[test]
fn ignores_queries() {
let mut buf = [0u8; 512];
let len =
build_browse_query(NameSlice::new(["_matterc", "_udp", "local"]), &mut buf).unwrap();
assert!(parse_into_answer(&buf[..len], None).unwrap().is_none());
}
#[test]
fn parses_full_response() {
let mut buf = [0u8; 1024];
let len = commissionable_response(&mut buf, &["_L1234", "_S3", "_CM"]);
let answer = parse_into_answer(&buf[..len], Some(7)).unwrap().unwrap();
assert_eq!(answer.scope_id, 7);
assert_eq!(
name_str(answer.instance_name).as_str(),
"ABCD1234._matterc._udp.local"
);
assert_eq!(answer.port, Some(5540));
assert!(answer
.addrs
.clone()
.any(|a| a == IpAddr::V4(Ipv4Addr::new(192, 168, 1, 5))));
let filter = crate::transport::network::mdns::CommissionableFilter {
discriminator: Some(1234),
vendor_id: Some(65521),
product_id: Some(32769),
..Default::default()
};
assert!(filter.matches(&answer));
}
#[test]
fn parses_response_without_txt() {
let mut buf = [0u8; 1024];
let service = MdnsLocalService {
name: "ABCD1234",
service: "_matterc",
protocol: "_udp",
service_protocol: "_matterc._udp",
port: 5540,
service_subtypes: core::iter::empty(),
txt_kvs: core::iter::empty::<(&str, &str)>(),
};
let len = host().broadcast(&service, &mut buf, 60, 60).unwrap();
let answer = parse_into_answer(&buf[..len], None).unwrap().unwrap();
assert_eq!(answer.port, Some(5540));
assert_eq!(answer.scope_id, 0);
assert_eq!(answer.txt.count(), 0);
}
}