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}, mdns2};
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, Clone)]
18pub enum CommissioningMode {
19    No,
20    Yes,
21    WithPasscode,
22}
23
24#[derive(Debug, Clone)]
25pub struct MatterDeviceInfo {
26    pub instance: 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
39impl MatterDeviceInfo {
40    pub fn print_compact(&self) {
41        let mut info = format!("{} ({})", self.instance, self.device);
42        if let Some(name) = &self.name {
43            info += &format!(", name: {}", name);
44        }
45        if let Some(vendor_id) = &self.vendor_id {
46            info += &format!(", vendor_id: {}", vendor_id);
47        }
48        if let Some(product_id) = &self.product_id {
49            info += &format!(", product_id: {}", product_id);
50        }
51        if let Some(discriminator) = &self.discriminator {
52            info += &format!(", discriminator: {}", discriminator);
53        }
54        if let Some(cm) = &self.commissioning_mode {
55            info += &format!(", commissioning_mode: {:?}", cm);
56        }
57        if let Some(pairing_hint) = &self.pairing_hint {
58            info += &format!(", pairing_hint: {}", pairing_hint);
59        }
60        if let Some(port) = &self.port {
61            info += &format!(", port: {}", port);
62        }
63        println!("{}", info);
64        if !self.ips.is_empty() {
65            println!("  ips:");
66            for ip in &self.ips {
67                println!("      {}", ip);
68            }
69        }
70
71    }
72}
73
74
75pub fn parse_txt_records(data: &[u8]) -> Result<HashMap<String, String>> {
76    let mut cursor = Cursor::new(data);
77    let mut out = HashMap::new();
78    while cursor.remaining() > 0 {
79        let len = cursor.read_u8()?;
80        let mut buf = vec![0; len as usize];
81        cursor.read_exact(buf.as_mut_slice())?;
82        let splitstr = std::str::from_utf8(&buf)?.splitn(2, "=");
83        let x: Vec<&str> = splitstr.collect();
84        if x.len() == 2 {
85            out.insert(x[0].to_owned(), x[1].to_owned());
86        }
87    }
88    Ok(out)
89}
90
91fn remove_string_suffix(string: &str, suffix: &str) -> String {
92    if let Some(s) = string.strip_suffix(suffix) {
93        s.to_owned()
94    } else {
95        string.to_owned()
96    }
97}
98
99pub fn to_matter_info2(msg: &DnsMessage, svc: &str) -> Result<Vec<MatterDeviceInfo>> {
100    let mut out = Vec::new();
101    let mut matter_service = false;
102    let svcname = ".".to_owned() + svc + ".";
103    for answer in &msg.answers {
104        if answer.name == svcname[1..] {
105            matter_service = true
106        }
107    }
108    if !matter_service {
109        return Err(anyhow::anyhow!("not matter service"));
110    }
111    let mut services = HashMap::new();
112    let mut targets = HashMap::new();
113    for additional in &msg.additional {
114        if additional.typ == mdns::TYPE_A {
115            let arr: [u8; 4] = match additional.rdata.clone().try_into() {
116                Ok(v) => v,
117                Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
118            };
119            let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
120            if !targets.contains_key(&additional.name) {
121                targets.insert(additional.name.clone(), Vec::new());
122            }
123            targets.get_mut(&additional.name).unwrap().push(val);
124        }
125        if additional.typ == mdns::TYPE_AAAA {
126            let arr: [u8; 16] = match additional.rdata.clone().try_into() {
127                Ok(v) => v,
128                Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
129            };
130            let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
131            if !targets.contains_key(&additional.name) {
132                targets.insert(additional.name.clone(), Vec::new());
133            }
134            targets.get_mut(&additional.name).unwrap().push(val);
135        }
136    }
137    let mut all = msg.additional.to_vec();
138    all.append(&mut msg.answers.to_vec());
139    for additional in &all {
140        if additional.typ == mdns::TYPE_SRV {
141            let service_name = remove_string_suffix(&additional.name, &svcname);
142            if additional.rdata.len() < 6 {
143                continue;
144            }
145            let port = ((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16);
146            let target_name = {
147                if let Some(at) = additional.target.as_ref() {
148                    at
149                } else {
150                    continue;
151                }
152            };
153            let target_ip = targets.get(target_name).cloned().unwrap_or_default();
154            let mi = MatterDeviceInfo {
155                instance: service_name.clone(),
156                device: remove_string_suffix(target_name, ".local.").to_owned(),
157                ips: target_ip,
158                name: None,
159                discriminator: None,
160                commissioning_mode: None,
161                pairing_hint: None,
162                source_ip: msg.source.to_string(),
163                vendor_id: None,
164                product_id: None,
165                port: Some(port),
166            };
167            services.insert(service_name, mi);
168        }
169    }
170    for s in services.values() {
171        out.push(s.clone());
172    }
173
174    Ok(out)
175}
176
177pub fn to_matter_info(msg: &DnsMessage, svc: &str) -> Result<MatterDeviceInfo> {
178    let mut device = None;
179    let mut service = None;
180    let mut ips = BTreeMap::new();
181    let mut name = None;
182    let mut discriminator = None;
183    let mut cm = None;
184    let mut pairing_hint = None;
185    let mut vendor_id = None;
186    let mut product_id = None;
187    let mut port: Option<u16> = None;
188
189    let mut matter_service = false;
190    let svcname = ".".to_owned() + svc + ".";
191    for answer in &msg.answers {
192        if answer.name == svcname[1..] {
193            matter_service = true
194        }
195    }
196    for additional in &msg.additional {
197        if additional.typ == mdns::TYPE_A {
198            let arr: [u8; 4] = match additional.rdata.clone().try_into() {
199                Ok(v) => v,
200                Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
201            };
202            let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
203            ips.insert(val, true);
204            device = Some(remove_string_suffix(&additional.name, ".local."));
205        }
206        if additional.typ == mdns::TYPE_AAAA {
207            let arr: [u8; 16] = match additional.rdata.clone().try_into() {
208                Ok(v) => v,
209                Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
210            };
211            let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
212            ips.insert(val, true);
213            device = Some(remove_string_suffix(&additional.name, ".local."));
214        }
215        if additional.typ == mdns::TYPE_SRV {
216            service = Some(remove_string_suffix(&additional.name, &svcname));
217            if additional.rdata.len() >= 6 {
218                port = Some(((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16))
219            }
220        }
221        if additional.typ == mdns::TYPE_TXT {
222            let rec = parse_txt_records(&additional.rdata)?;
223            name = rec.get("DN").cloned();
224            discriminator = rec.get("D").cloned();
225            pairing_hint = rec.get("PH").cloned();
226            if let Some(vp) = rec.get("VP") {
227                let mut split = vp.split("+");
228                vendor_id = split.next().map(str::to_owned);
229                product_id = split.next().map(str::to_owned);
230            }
231            cm = match rec.get("CM") {
232                Some(v) => match v.as_str() {
233                    "0" => Some(CommissioningMode::No),
234                    "1" => Some(CommissioningMode::Yes),
235                    "2" => Some(CommissioningMode::WithPasscode),
236                    _ => None,
237                },
238                None => None,
239            };
240        }
241    }
242
243    if !matter_service {
244        return Err(anyhow::anyhow!("not matter service"));
245    }
246
247    Ok(MatterDeviceInfo {
248        instance: service.context("service name not detected")?,
249        device: device.context("device name not detected")?,
250        ips: ips.into_keys().collect(),
251        name,
252        discriminator,
253        commissioning_mode: cm,
254        pairing_hint,
255        source_ip: msg.source.to_string(),
256        vendor_id,
257        product_id,
258        port,
259    })
260}
261
262async fn discover_common(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
263    let stop = tokio_util::sync::CancellationToken::new();
264    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
265
266    mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
267
268    tokio::spawn(async move {
269        tokio::time::sleep(timeout).await;
270        stop.cancel();
271    });
272    let mut cache = HashMap::new();
273    let mut out = Vec::new();
274    while let Some(dns) = receiver.recv().await {
275        if cache.contains_key(&dns) {
276            continue;
277        }
278        let info = match to_matter_info(&dns, svc_type) {
279            Ok(info) => info,
280            Err(_) => continue,
281        };
282        out.push(info);
283        cache.insert(dns, true);
284    }
285    Ok(out)
286}
287
288/// Discover commissionable devices using mdns
289pub async fn discover_commissionable(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
290    discover_common(timeout, "_matterc._udp.local").await
291}
292
293/// Discover commissioned devices using mdns
294pub async fn discover_commissioned(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
295    discover_common(timeout, "_matter._tcp.local").await
296}
297
298
299async fn discover_common2(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
300    let stop = tokio_util::sync::CancellationToken::new();
301    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
302
303    mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
304
305    tokio::spawn(async move {
306        tokio::time::sleep(timeout).await;
307        stop.cancel();
308    });
309    let mut cache = HashMap::new();
310    let mut out: Vec<MatterDeviceInfo> = Vec::new();
311    while let Some(dns) = receiver.recv().await {
312        if cache.contains_key(&dns) {
313            continue;
314        }
315        let info = match to_matter_info2(&dns, svc_type) {
316            Ok(info) => info,
317            Err(e) => {
318                log::trace!("failed to parse mdns message from {}: {:?}", dns.source, e);
319                continue;
320            },
321        };
322        for i in &info {
323            out.push(i.clone());
324        }
325        cache.insert(dns, true);
326    }
327    Ok(out)
328}
329
330/// Discover commissionable devices using mdns
331pub async fn discover_commissionable2(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
332    discover_common2(timeout, "_matterc._udp.local").await
333}
334
335/// Discover commissioned devices using mdns
336pub async fn discover_commissioned2(timeout: Duration, device: &Option<String>) -> Result<Vec<MatterDeviceInfo>> {
337    let query = {
338        match device {
339            None => "_matter._tcp.local".to_owned(),
340            Some(d) => format!("{}._matter._tcp.local", d),
341        }
342    };
343    discover_common2(timeout, &query).await
344}
345
346
347
348pub async fn extract_matter_info(target: &str, mdns: &mdns2::MdnsService) -> Result<MatterDeviceInfo> {
349    let txt_records = mdns.lookup(target, mdns::TYPE_TXT).await;
350    let mut txt_info = HashMap::new();
351    for txt_rr in txt_records {
352        txt_info.extend(parse_txt_records(&txt_rr.rdata)?);
353    }
354    let srv_records = mdns.lookup(target, mdns::TYPE_SRV).await;
355    let srv_rr = srv_records.first().ok_or_else(|| anyhow::anyhow!("No SRV record found for {}", target))?;
356    let (srv_target, port) = match srv_rr.data {
357        mdns::RRData::SRV { ref target, port, .. } => (target.clone(), port),
358        _ => return Err(anyhow::anyhow!("Invalid SRV record for {}", target)),
359    };
360    let mut ips = Vec::new();
361    let a_records = mdns.lookup(&srv_target, mdns::TYPE_A).await;
362    for a_rr in a_records {
363        if let mdns::RRData::A(ip) = a_rr.data {
364            ips.push(ip.into());
365        }
366    }
367    let aaaa_records = mdns.lookup(&srv_target, mdns::TYPE_AAAA).await;
368    for aaaa_rr in aaaa_records {
369        if let mdns::RRData::AAAA(ip) = aaaa_rr.data {
370            ips.push(ip.into());
371        }
372    }
373    let (vendor_id, product_id) = {
374        let vp = txt_info.get("VP");
375        if let Some(vp) = vp {
376            let mut parts = vp.split('+');
377            let vendor_id = parts.next();
378            let product_id = parts.next();
379            (vendor_id.map(|v| v.to_owned()), product_id.map(|p| p.to_owned()))
380        } else {
381            (None, None)
382        }
383    };
384    let discriminator = txt_info.get("D").cloned();
385    let name = txt_info.get("DN").cloned();
386    let commissioning_mode = match txt_info.get("CM") {
387                Some(v) => match v.as_str() {
388                    "0" => Some(CommissioningMode::No),
389                    "1" => Some(CommissioningMode::Yes),
390                    "2" => Some(CommissioningMode::WithPasscode),
391                    _ => None,
392                },
393                None => None,
394            };
395    let pairing_hint = txt_info.get("PH").cloned();
396    Ok(MatterDeviceInfo {
397        name,
398        instance: target.trim_end_matches('.').to_owned(),
399        device: srv_target.trim_end_matches('.').to_owned(),
400        ips,
401        vendor_id,
402        product_id,
403        discriminator,
404        commissioning_mode,
405        pairing_hint,
406        source_ip: "".to_owned(),
407        port: Some(port),
408    })
409}