matc/
discover.rs

1//! Module with very simple mdns based discovery of matter devices.
2//! Usually application shall discover devices using these methods and filter according discriminator.
3//! This module tries to send mdns using ipv4 and ipv6 multicast at same time.
4//! If more control over discovery mechanism is required, it may be better to use some external mdns library.
5
6use crate::mdns::{self, DnsMessage};
7use anyhow::{Context, Result};
8use byteorder::ReadBytesExt;
9use std::{
10    collections::{BTreeMap, HashMap},
11    io::{Cursor, Read},
12    net::{IpAddr, Ipv4Addr, Ipv6Addr},
13    time::Duration,
14};
15use tokio_util::bytes::Buf;
16
17#[derive(Debug)]
18pub enum CommissioningMode {
19    No,
20    Yes,
21    WithPasscode,
22}
23
24#[derive(Debug)]
25pub struct MatterDeviceInfo {
26    pub service: String,
27    pub device: String,
28    pub ips: Vec<IpAddr>,
29    pub name: Option<String>,
30    pub vendor_id: Option<String>,
31    pub product_id: Option<String>,
32    pub discriminator: Option<String>,
33    pub commissioning_mode: Option<CommissioningMode>,
34    pub pairing_hint: Option<String>,
35    pub source_ip: String,
36    pub port: Option<u16>,
37}
38
39fn parse_txt_records(data: &[u8]) -> Result<HashMap<String, String>> {
40    let mut cursor = Cursor::new(data);
41    let mut out = HashMap::new();
42    while cursor.remaining() > 0 {
43        let len = cursor.read_u8()?;
44        let mut buf = vec![0; len as usize];
45        cursor.read_exact(buf.as_mut_slice())?;
46        let splitstr = std::str::from_utf8(&buf)?.split("=");
47        let x: Vec<&str> = splitstr.collect();
48        if x.len() == 2 {
49            out.insert(x[0].to_owned(), x[1].to_owned());
50        }
51    }
52    Ok(out)
53}
54
55fn remove_string_suffix(string: &str, suffix: &str) -> String {
56    if let Some(s) = string.strip_suffix(suffix) {
57        s.to_owned()
58    } else {
59        string.to_owned()
60    }
61}
62
63fn to_matter_info(msg: &DnsMessage, svc: &str) -> Result<MatterDeviceInfo> {
64    let mut device = None;
65    let mut service = None;
66    let mut ips = BTreeMap::new();
67    let mut name = None;
68    let mut discriminator = None;
69    let mut cm = None;
70    let mut pairing_hint = None;
71    let mut vendor_id = None;
72    let mut product_id = None;
73    let mut port: Option<u16> = None;
74
75    let mut matter_service = false;
76    let svcname = ".".to_owned() + svc + ".";
77    for answer in &msg.answers {
78        if answer.name == svcname[1..] {
79            matter_service = true
80        }
81    }
82    for additional in &msg.additional {
83        if additional.typ == mdns::TYPE_A {
84            let arr: [u8; 4] = match additional.rdata.clone().try_into() {
85                Ok(v) => v,
86                Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
87            };
88            let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
89            ips.insert(val, true);
90            device = Some(remove_string_suffix(&additional.name, ".local."));
91        }
92        if additional.typ == mdns::TYPE_AAAA {
93            let arr: [u8; 16] = match additional.rdata.clone().try_into() {
94                Ok(v) => v,
95                Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
96            };
97            let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
98            ips.insert(val, true);
99            device = Some(remove_string_suffix(&additional.name, ".local."));
100        }
101        if additional.typ == mdns::TYPE_SRV {
102            service = Some(remove_string_suffix(&additional.name, &svcname));
103            if additional.rdata.len() >= 6 {
104                port = Some(((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16))
105            }
106        }
107        if additional.typ == mdns::TYPE_TXT {
108            let rec = parse_txt_records(&additional.rdata)?;
109            name = rec.get("DN").cloned();
110            discriminator = rec.get("D").cloned();
111            pairing_hint = rec.get("PH").cloned();
112            if let Some(vp) = rec.get("VP") {
113                let mut split = vp.split("+");
114                vendor_id = split.next().map(str::to_owned);
115                product_id = split.next().map(str::to_owned);
116            }
117            cm = match rec.get("CM") {
118                Some(v) => match v.as_str() {
119                    "0" => Some(CommissioningMode::No),
120                    "1" => Some(CommissioningMode::Yes),
121                    "2" => Some(CommissioningMode::WithPasscode),
122                    _ => None,
123                },
124                None => None,
125            };
126        }
127    }
128
129    if !matter_service {
130        return Err(anyhow::anyhow!("not matter service"));
131    }
132
133    Ok(MatterDeviceInfo {
134        service: service.context("service name not detected")?,
135        device: device.context("device name not detected")?,
136        ips: ips.into_keys().collect(),
137        name,
138        discriminator,
139        commissioning_mode: cm,
140        pairing_hint,
141        source_ip: msg.source.to_string(),
142        vendor_id,
143        product_id,
144        port,
145    })
146}
147
148async fn discover_common(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
149    let stop = tokio_util::sync::CancellationToken::new();
150    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
151
152    mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
153
154    tokio::spawn(async move {
155        tokio::time::sleep(timeout).await;
156        stop.cancel();
157    });
158    let mut cache = HashMap::new();
159    let mut out = Vec::new();
160    while let Some(dns) = receiver.recv().await {
161        if cache.contains_key(&dns) {
162            continue;
163        }
164        let info = match to_matter_info(&dns, svc_type) {
165            Ok(info) => info,
166            Err(_) => continue,
167        };
168        out.push(info);
169        cache.insert(dns, true);
170    }
171    Ok(out)
172}
173
174/// Discover commissionable devices using mdns
175pub async fn discover_commissionable(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
176    discover_common(timeout, "_matterc._udp.local").await
177}
178
179/// Discover commissioned devices using mdns
180pub async fn discover_commissioned(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
181    discover_common(timeout, "_matter._tcp.local").await
182}