use core::fmt::Write;
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
use domain::base::name::{Label, ToLabelIter};
use crate::dm::clusters::basic_info::BasicInfoConfig;
use crate::error::{Error, ErrorCode};
use crate::tlv::EitherIter;
use crate::utils::storage::{write_split, Vec, WriteBuf};
use super::{MatterLocalService, MatterRemoteService};
#[cfg(feature = "astro-dnssd")]
pub mod astro;
#[cfg(feature = "zbus")]
pub mod avahi;
pub mod builtin;
#[cfg(feature = "zbus")]
pub mod resolve;
#[cfg(feature = "zeroconf")]
pub mod zeroconf;
pub const MDNS_IPV6_BROADCAST_ADDR: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0x00fb);
pub const MDNS_IPV4_BROADCAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
pub const MDNS_PORT: u16 = 5353;
pub const MDNS_SOCKET_DEFAULT_BIND_ADDR: SocketAddr =
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, MDNS_PORT, 0, 0));
impl MatterLocalService {
#[allow(clippy::type_complexity)]
pub fn service<'a>(
&self,
dev_det: &BasicInfoConfig<'_>,
matter_port: u16,
buf: &'a mut [u8],
) -> Result<
(
MdnsLocalService<
'a,
impl Iterator<Item = &'a str> + Clone,
impl Iterator<Item = (&'a str, &'a str)> + Clone,
>,
&'a mut [u8],
),
Error,
> {
match self {
Self::Commissioned {
compressed_fabric_id,
node_id,
} => {
let mut wb = WriteBuf::new(buf);
let (name, mut wb) =
write_split!(wb, "{:016X}-{:016X}", compressed_fabric_id, node_id)?;
let (subtype_i, mut wb) = write_split!(wb, "_I{:016X}", compressed_fabric_id)?;
let (txt_sai, mut wb) = if let Some(sai) = dev_det.sai {
write_split!(wb, "{}", sai)?
} else {
("", wb)
};
let (txt_sii, wb) = if let Some(sii) = dev_det.sii {
write_split!(wb, "{}", sii)?
} else {
("", wb)
};
let txt_kvs = [
("SAI", txt_sai),
("SII", txt_sii),
("T", if dev_det.tcp_supported { "6" } else { "" }),
("DUMMY", "DUMMY"),
]
.into_iter()
.filter(|(_, v)| !v.is_empty());
Ok((
MdnsLocalService {
name,
service: "_matter",
protocol: "_tcp",
service_protocol: "_matter._tcp",
port: matter_port,
service_subtypes: EitherIter::First(core::iter::once(subtype_i)),
txt_kvs: EitherIter::First(txt_kvs),
},
wb.into_buf(),
))
}
Self::Commissionable {
id,
discriminator,
enhanced,
} => {
let mut wb = WriteBuf::new(buf);
let (name, mut wb) = write_split!(wb, "{:016X}", id)?;
let (subtype_discr, mut wb) = write_split!(wb, "_L{}", *discriminator)?;
let (subtype_short_discr, mut wb) = write_split!(
wb,
"_S{}",
Self::compute_short_discriminator(*discriminator)
)?;
let (subtype_v, mut wb) = write_split!(wb, "_V{}", dev_det.vid)?;
let (subtype_t, mut wb) = if let Some(dt) = dev_det.device_type {
write_split!(wb, "_T{}", dt)?
} else {
("", wb)
};
let service_subtypes = [
subtype_discr,
subtype_short_discr,
subtype_v,
subtype_t,
"_CM",
]
.into_iter()
.filter(|s| !s.is_empty());
let (txt_discr, mut wb) = write_split!(wb, "{}", *discriminator)?;
let (txt_vid_pid, mut wb) = write_split!(wb, "{}+{}", dev_det.vid, dev_det.pid)?;
let (txt_sai, mut wb) = if let Some(sai) = dev_det.sai {
write_split!(wb, "{}", sai)?
} else {
("", wb)
};
let (txt_sii, mut wb) = if let Some(sii) = dev_det.sii {
write_split!(wb, "{}", sii)?
} else {
("", wb)
};
let (txt_dn, mut wb) = write_split!(wb, "{}", dev_det.device_name)?;
let (txt_pi, mut wb) = write_split!(wb, "{}", dev_det.pairing_instruction)?;
let (txt_ph, mut wb) = write_split!(wb, "{}", dev_det.pairing_hint.bits())?;
let (txt_dt, mut wb) = if let Some(dt) = dev_det.device_type {
write_split!(wb, "{}", dt)?
} else {
("", wb)
};
let (txt_tcp, wb) = if dev_det.tcp_supported {
write_split!(wb, "6")?
} else {
("", wb)
};
let txt_kvs = [
("D", txt_discr),
("CM", if *enhanced { "2" } else { "1" }),
("VP", txt_vid_pid),
("SAI", txt_sai),
("SII", txt_sii),
("DN", txt_dn),
("PI", txt_pi),
("PH", txt_ph),
("DT", txt_dt),
("T", txt_tcp),
]
.into_iter()
.filter(|(_, v)| !v.is_empty());
Ok((
MdnsLocalService {
name,
service: "_matterc",
protocol: "_udp",
service_protocol: "_matterc._udp",
port: matter_port,
service_subtypes: EitherIter::Second(service_subtypes),
txt_kvs: EitherIter::Second(txt_kvs),
},
wb.into_buf(),
))
}
}
}
fn compute_short_discriminator(discriminator: u16) -> u16 {
const SHORT_DISCRIMINATOR_MASK: u16 = 0xF00;
const SHORT_DISCRIMINATOR_SHIFT: u16 = 8;
(discriminator & SHORT_DISCRIMINATOR_MASK) >> SHORT_DISCRIMINATOR_SHIFT
}
}
impl MatterRemoteService {
pub fn service_type(&self) -> &'static str {
match self {
Self::Operational { .. } => "_matter._tcp",
Self::Commissionable { .. } => "_matterc._udp",
}
}
pub fn instance_name(&self, buf: &mut heapless::String<128>) {
buf.clear();
match self {
Self::Operational {
compressed_fabric_id,
node_id,
} => {
write_unwrap!(
buf,
"{:016X}-{:016X}._matter._tcp.local",
compressed_fabric_id,
node_id
);
}
Self::Commissionable { id } => {
write_unwrap!(buf, "{:016X}._matterc._udp.local", id);
}
}
}
fn suffix_labels(&self) -> &'static [&'static str] {
match self {
Self::Operational { .. } => &["_matter", "_tcp", "local"],
Self::Commissionable { .. } => &["_matterc", "_udp", "local"],
}
}
pub(crate) fn query_name_labels<'b>(&self, buf: &'b mut heapless::String<33>) -> [&'b str; 4] {
buf.clear();
match self {
Self::Operational {
compressed_fabric_id,
node_id,
} => {
write_unwrap!(buf, "{:016X}-{:016X}", compressed_fabric_id, node_id);
[buf.as_str(), "_matter", "_tcp", "local"]
}
Self::Commissionable { id } => {
write_unwrap!(buf, "{:016X}", id);
[buf.as_str(), "_matterc", "_udp", "local"]
}
}
}
pub fn matches_instance<I: ToLabelIter>(&self, instance: &I) -> bool {
let mut labels = instance.iter_labels().filter(|l| !l.is_empty());
let Some(first) = labels.next() else {
return false;
};
let mut suffix = self.suffix_labels().iter();
for label in labels {
match suffix.next() {
Some(expected) if label.as_slice().eq_ignore_ascii_case(expected.as_bytes()) => {}
_ => return false,
}
}
if suffix.next().is_some() {
return false;
}
let Ok(first) = core::str::from_utf8(first.as_slice()) else {
return false;
};
match self {
Self::Operational {
compressed_fabric_id,
node_id,
} => {
let Some((fabric, node)) = first.split_once('-') else {
return false;
};
parse_hex_u64(fabric) == Some(*compressed_fabric_id)
&& parse_hex_u64(node) == Some(*node_id)
}
Self::Commissionable { id } => parse_hex_u64(first) == Some(*id),
}
}
}
pub struct MdnsLocalService<'a, S, T>
where
S: Iterator<Item = &'a str> + Clone,
T: Iterator<Item = (&'a str, &'a str)> + Clone,
{
pub name: &'a str,
pub service: &'a str,
pub protocol: &'a str,
pub service_protocol: &'a str,
pub port: u16,
pub service_subtypes: S,
pub txt_kvs: T,
}
#[derive(Debug, Clone, Copy)]
pub struct MdnsRemoteService<I, A, T> {
pub instance_name: I,
pub port: Option<u16>,
pub addrs: A,
pub txt: T,
pub scope_id: u32,
}
impl<'a, I, A, T> MdnsRemoteService<I, A, T>
where
T: Iterator<Item = (&'a str, &'a str)> + Clone,
{
pub fn session_params(&self) -> (Option<u32>, Option<u32>, Option<u16>) {
let (mut sii, mut sai, mut sat) = (None, None, None);
for (key, value) in self.txt.clone() {
if key.eq_ignore_ascii_case("SII") {
sii = value.parse().ok();
} else if key.eq_ignore_ascii_case("SAI") {
sai = value.parse().ok();
} else if key.eq_ignore_ascii_case("SAT") {
sat = value.parse().ok();
}
}
(sii, sai, sat)
}
}
#[derive(Debug, Clone, Default, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct CommissionableFilter {
pub discriminator: Option<u16>,
pub short_discriminator: Option<u8>,
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub device_type: Option<u32>,
pub commissioning_mode_only: bool,
}
impl CommissionableFilter {
pub fn service_type(&self, buf: &mut heapless::String<64>, include_local: bool) {
buf.clear();
let suffix = if include_local { ".local" } else { "" };
let mut sbuf = heapless::String::<24>::new();
if let Some(sub) = self.subtype(&mut sbuf) {
write_unwrap!(buf, "{}._sub._matterc._udp{}", sub, suffix);
} else {
write_unwrap!(buf, "_matterc._udp{}", suffix);
}
}
pub(crate) fn subtype<'b>(&self, buf: &'b mut heapless::String<24>) -> Option<&'b str> {
buf.clear();
if let Some(disc) = self.discriminator {
write_unwrap!(buf, "_L{}", disc);
} else if let Some(short_disc) = self.short_discriminator {
write_unwrap!(buf, "_S{}", short_disc);
} else if let Some(vid) = self.vendor_id {
write_unwrap!(buf, "_V{}", vid);
} else if let Some(dt) = self.device_type {
write_unwrap!(buf, "_T{}", dt);
} else if self.commissioning_mode_only {
write_unwrap!(buf, "_CM");
} else {
return None;
}
Some(buf.as_str())
}
pub fn matches<'a, I, A, T>(&self, service: &MdnsRemoteService<I, A, T>) -> bool
where
T: Iterator<Item = (&'a str, &'a str)> + Clone,
{
self.matches_txt(service.txt.clone())
}
fn matches_txt<'a, I>(&self, txt: I) -> bool
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
let mut discriminator: Option<u16> = None;
let mut vendor_id: Option<u16> = None;
let mut product_id: Option<u16> = None;
let mut device_type: Option<u32> = None;
let mut commissioning = CommissioningMode::Disabled;
for (key, value) in txt {
if key.eq_ignore_ascii_case("D") {
discriminator = value.parse::<u16>().ok().filter(|d| *d <= 0xFFF);
} else if key.eq_ignore_ascii_case("VP") {
if let Some(plus) = value.find('+') {
vendor_id = value[..plus].parse::<u16>().ok();
product_id = value[plus + 1..].parse::<u16>().ok();
} else {
vendor_id = value.parse::<u16>().ok();
}
} else if key.eq_ignore_ascii_case("CM") {
commissioning = CommissioningMode::from_txt_value(value);
} else if key.eq_ignore_ascii_case("DT") {
device_type = value.parse::<u32>().ok();
}
}
if let Some(want) = self.discriminator {
if discriminator != Some(want) {
return false;
}
}
if let Some(want) = self.short_discriminator {
if discriminator.map(|d| (d >> 8) as u8) != Some(want) {
return false;
}
}
if let Some(want) = self.vendor_id {
if vendor_id != Some(want) {
return false;
}
}
if let Some(want) = self.product_id {
if product_id != Some(want) {
return false;
}
}
if let Some(want) = self.device_type {
if device_type != Some(want) {
return false;
}
}
if self.commissioning_mode_only && !commissioning.is_commissionable() {
return false;
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
enum CommissioningMode {
#[default]
Disabled = 0,
Basic = 1,
Enhanced = 2,
}
impl CommissioningMode {
fn from_txt_value(value: &str) -> Self {
match value {
"1" => Self::Basic,
"2" => Self::Enhanced,
_ => Self::Disabled,
}
}
fn is_commissionable(&self) -> bool {
!matches!(self, Self::Disabled)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DottedName<'a>(pub &'a str);
impl ToLabelIter for DottedName<'_> {
type LabelIter<'t>
= core::iter::Map<core::str::Split<'t, char>, fn(&str) -> &Label>
where
Self: 't;
fn iter_labels(&self) -> Self::LabelIter<'_> {
fn str_to_label(s: &str) -> &Label {
Label::from_slice(s.as_bytes()).unwrap_or_else(|_| Label::root())
}
self.0
.trim_end_matches('.')
.split('.')
.map(str_to_label as fn(&str) -> &Label)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResolvedNode {
pub addr: SocketAddr,
pub sii: Option<u32>,
pub sai: Option<u32>,
pub sat: Option<u16>,
}
#[derive(Debug, Clone)]
pub(crate) enum MdnsResolveState {
Idle,
Requested { service: MatterRemoteService },
InFlight { service: MatterRemoteService },
Resolved {
ip: IpAddr,
port: u16,
scope_id: u32,
sii: Option<u32>,
sai: Option<u32>,
sat: Option<u16>,
},
}
pub(crate) const MAX_BROWSE_EXCLUDE: usize = 6;
pub(crate) type BrowseExclude = Vec<u64, MAX_BROWSE_EXCLUDE>;
#[derive(Debug, Clone)]
pub(crate) enum MdnsBrowseState {
Idle,
Requested {
filter: CommissionableFilter,
exclude: BrowseExclude,
},
InFlight {
filter: CommissionableFilter,
exclude: BrowseExclude,
},
Found {
ip: IpAddr,
port: u16,
scope_id: u32,
id: u64,
},
}
pub fn score_ip_address(addr: &IpAddr) -> u8 {
match addr {
IpAddr::V6(ipv6) => {
if ipv6.is_unicast_link_local() {
100
} else if ipv6.is_unique_local() {
80
} else if is_ipv6_global_unicast(ipv6) {
60
} else {
40
}
}
IpAddr::V4(_) => {
20
}
}
}
fn is_ipv6_global_unicast(addr: &Ipv6Addr) -> bool {
let segments = addr.segments();
(segments[0] & 0xe000) == 0x2000
}
pub(crate) fn commissionable_instance_id<I: ToLabelIter>(instance: &I) -> Option<u64> {
let first = instance.iter_labels().find(|l| !l.is_empty())?;
parse_hex_u64(core::str::from_utf8(first.as_slice()).ok()?)
}
fn parse_hex_u64(s: &str) -> Option<u64> {
if s.bytes().all(|b| b.is_ascii_hexdigit()) {
u64::from_str_radix(s, 16).ok()
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_compute_short_discriminator() {
let discriminator: u16 = 0b0000_1111_0000_0000;
let short = MatterLocalService::compute_short_discriminator(discriminator);
assert_eq!(short, 0b1111);
let discriminator: u16 = 840;
let short = MatterLocalService::compute_short_discriminator(discriminator);
assert_eq!(short, 3);
}
#[test]
fn matches_txt_empty_filter_matches_all() {
let filter = CommissionableFilter::default();
assert!(filter.matches_txt([("D", "1234"), ("VP", "65521+32768"), ("CM", "1")]));
assert!(filter.matches_txt(core::iter::empty::<(&str, &str)>())); }
#[test]
fn matches_txt_discriminator_and_short() {
let filter = CommissionableFilter {
discriminator: Some(1234),
..Default::default()
};
assert!(filter.matches_txt([("D", "1234")]));
assert!(!filter.matches_txt([("D", "5678")]));
let filter = CommissionableFilter {
short_discriminator: Some(3),
..Default::default()
};
assert!(filter.matches_txt([("D", "840")]));
assert!(!filter.matches_txt([("D", "1024")])); }
#[test]
fn matches_txt_vendor_product_and_combined() {
let filter = CommissionableFilter {
vendor_id: Some(0xFFF1),
product_id: Some(0x8000),
..Default::default()
};
assert!(filter.matches_txt([("VP", "65521+32768")]));
assert!(!filter.matches_txt([("VP", "65521+1")])); assert!(!filter.matches_txt([("VP", "1+32768")]));
let filter = CommissionableFilter {
discriminator: Some(1234),
vendor_id: Some(0xFFF1),
..Default::default()
};
assert!(filter.matches_txt([("D", "1234"), ("VP", "65521+1")]));
assert!(!filter.matches_txt([("D", "1234"), ("VP", "1+1")]));
assert!(!filter.matches_txt([("D", "9999"), ("VP", "65521+1")]));
}
#[test]
fn matches_txt_device_type_and_commissioning_mode() {
let filter = CommissionableFilter {
device_type: Some(257),
..Default::default()
};
assert!(filter.matches_txt([("DT", "257")]));
assert!(!filter.matches_txt([("DT", "256")]));
let filter = CommissionableFilter {
commissioning_mode_only: true,
..Default::default()
};
assert!(filter.matches_txt([("CM", "1")]));
assert!(filter.matches_txt([("CM", "2")]));
assert!(!filter.matches_txt([("CM", "0")]));
assert!(!filter.matches_txt(core::iter::empty::<(&str, &str)>())); }
#[test]
fn service_type_no_filter() {
let filter = CommissionableFilter::default();
let mut buf = heapless::String::<64>::new();
filter.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_matterc._udp");
filter.service_type(&mut buf, true);
assert_eq!(buf.as_str(), "_matterc._udp.local");
}
#[test]
fn service_type_with_discriminator() {
let filter = CommissionableFilter {
discriminator: Some(1234),
..Default::default()
};
let mut buf = heapless::String::<64>::new();
filter.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp");
filter.service_type(&mut buf, true);
assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp.local");
}
#[test]
fn service_type_with_short_discriminator() {
let filter = CommissionableFilter {
short_discriminator: Some(3),
..Default::default()
};
let mut buf = heapless::String::<64>::new();
filter.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_S3._sub._matterc._udp");
}
#[test]
fn service_type_with_vendor_id() {
let filter = CommissionableFilter {
vendor_id: Some(0xFFF1),
..Default::default()
};
let mut buf = heapless::String::<64>::new();
filter.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_V65521._sub._matterc._udp");
}
#[test]
fn service_type_with_device_type() {
let filter = CommissionableFilter {
device_type: Some(257),
..Default::default()
};
let mut buf = heapless::String::<64>::new();
filter.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_T257._sub._matterc._udp");
}
#[test]
fn service_type_with_commissioning_mode_only() {
let filter = CommissionableFilter {
commissioning_mode_only: true,
..Default::default()
};
let mut buf = heapless::String::<64>::new();
filter.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_CM._sub._matterc._udp");
}
#[test]
fn service_type_priority_order() {
let mut buf = heapless::String::<64>::new();
CommissionableFilter {
discriminator: Some(1234),
short_discriminator: Some(3),
vendor_id: Some(0xFFF1),
device_type: Some(257),
commissioning_mode_only: true,
product_id: Some(0x8000),
}
.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_L1234._sub._matterc._udp");
CommissionableFilter {
short_discriminator: Some(3),
vendor_id: Some(0xFFF1),
..Default::default()
}
.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_S3._sub._matterc._udp");
CommissionableFilter {
vendor_id: Some(0xFFF1),
device_type: Some(257),
..Default::default()
}
.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_V65521._sub._matterc._udp");
CommissionableFilter {
device_type: Some(257),
commissioning_mode_only: true,
..Default::default()
}
.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_T257._sub._matterc._udp");
CommissionableFilter {
product_id: Some(0x8000),
..Default::default()
}
.service_type(&mut buf, false);
assert_eq!(buf.as_str(), "_matterc._udp");
}
#[test]
fn commissioning_mode_from_txt_value() {
assert_eq!(
CommissioningMode::from_txt_value("0"),
CommissioningMode::Disabled
);
assert_eq!(
CommissioningMode::from_txt_value("1"),
CommissioningMode::Basic
);
assert_eq!(
CommissioningMode::from_txt_value("2"),
CommissioningMode::Enhanced
);
assert_eq!(
CommissioningMode::from_txt_value("x"),
CommissioningMode::Disabled
);
assert!(!CommissioningMode::Disabled.is_commissionable());
assert!(CommissioningMode::Basic.is_commissionable());
assert!(CommissioningMode::Enhanced.is_commissionable());
}
#[test]
fn score_ip_address_priority_order() {
let link_local = IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
let ula = IpAddr::V6(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1));
let global = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
let ipv4 = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
assert!(score_ip_address(&link_local) > score_ip_address(&ula));
assert!(score_ip_address(&ula) > score_ip_address(&global));
assert!(score_ip_address(&global) > score_ip_address(&ipv4));
}
#[test]
fn is_ipv6_global_unicast_correct() {
assert!(is_ipv6_global_unicast(&Ipv6Addr::new(
0x2001, 0xdb8, 0, 0, 0, 0, 0, 1
)));
assert!(is_ipv6_global_unicast(&Ipv6Addr::new(
0x3fff, 0xffff, 0, 0, 0, 0, 0, 1
)));
assert!(!is_ipv6_global_unicast(&Ipv6Addr::new(
0xfe80, 0, 0, 0, 0, 0, 0, 1
)));
assert!(!is_ipv6_global_unicast(&Ipv6Addr::new(
0xfc00, 0, 0, 0, 0, 0, 0, 1
)));
}
}