1use 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 pub session_idle_interval_ms: Option<u32>,
39 pub session_active_interval_ms: Option<u32>,
41 pub session_active_threshold_ms: Option<u32>,
43 pub device_type: Option<String>,
45 pub scope_id: Option<u32>,
48}
49
50impl MatterDeviceInfo {
51 pub fn mrp_params(&self) -> crate::mrp::MrpParameters {
54 crate::mrp::MrpParameters::from_txt_ms(
55 self.session_idle_interval_ms,
56 self.session_active_interval_ms,
57 self.session_active_threshold_ms,
58 )
59 }
60
61 pub fn print_compact(&self) {
62 let mut info = format!("{} ({})", self.instance, self.device);
63 if let Some(name) = &self.name {
64 info += &format!(", name: {}", name);
65 }
66 if let Some(vendor_id) = &self.vendor_id {
67 info += &format!(", vendor_id: {}", vendor_id);
68 }
69 if let Some(product_id) = &self.product_id {
70 info += &format!(", product_id: {}", product_id);
71 }
72 if let Some(discriminator) = &self.discriminator {
73 info += &format!(", discriminator: {}", discriminator);
74 }
75 if let Some(cm) = &self.commissioning_mode {
76 info += &format!(", commissioning_mode: {:?}", cm);
77 }
78 if let Some(pairing_hint) = &self.pairing_hint {
79 info += &format!(", pairing_hint: {}", pairing_hint);
80 }
81 if let Some(port) = &self.port {
82 info += &format!(", port: {}", port);
83 }
84 if let Some(sii) = &self.session_idle_interval_ms {
85 info += &format!(", sii_ms: {}", sii);
86 }
87 if let Some(sai) = &self.session_active_interval_ms {
88 info += &format!(", sai_ms: {}", sai);
89 }
90 println!("{}", info);
91 if !self.ips.is_empty() {
92 println!(" ips:");
93 for ip in &self.ips {
94 println!(" {}", ip);
95 }
96 }
97
98 }
99}
100
101
102pub fn parse_txt_records(data: &[u8]) -> Result<HashMap<String, String>> {
103 let mut cursor = Cursor::new(data);
104 let mut out = HashMap::new();
105 while cursor.remaining() > 0 {
106 let len = cursor.read_u8()?;
107 let mut buf = vec![0; len as usize];
108 cursor.read_exact(buf.as_mut_slice())?;
109 let splitstr = std::str::from_utf8(&buf)?.splitn(2, "=");
110 let x: Vec<&str> = splitstr.collect();
111 if x.len() == 2 {
112 out.insert(x[0].to_owned(), x[1].to_owned());
113 }
114 }
115 Ok(out)
116}
117
118fn parse_mrp_txt(rec: &HashMap<String, String>) -> (Option<u32>, Option<u32>, Option<u32>) {
121 let get = |key: &str| rec.get(key).and_then(|v| v.parse::<u32>().ok());
122 (get("SII"), get("SAI"), get("SAT"))
123}
124
125fn remove_string_suffix(string: &str, suffix: &str) -> String {
126 if let Some(s) = string.strip_suffix(suffix) {
127 s.to_owned()
128 } else {
129 string.to_owned()
130 }
131}
132
133pub fn to_matter_info2(msg: &DnsMessage, svc: &str) -> Result<Vec<MatterDeviceInfo>> {
134 let mut out = Vec::new();
135 let mut matter_service = false;
136 let svcname = ".".to_owned() + svc + ".";
137 for answer in &msg.answers {
138 if answer.name == svcname[1..] {
139 matter_service = true
140 }
141 }
142 if !matter_service {
143 return Err(anyhow::anyhow!("not matter service"));
144 }
145 let mut services = HashMap::new();
146 let mut targets = HashMap::new();
147 for additional in &msg.additional {
148 if additional.typ == mdns::TYPE_A {
149 let arr: [u8; 4] = match additional.rdata.clone().try_into() {
150 Ok(v) => v,
151 Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
152 };
153 let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
154 if !targets.contains_key(&additional.name) {
155 targets.insert(additional.name.clone(), Vec::new());
156 }
157 targets.get_mut(&additional.name).unwrap().push(val);
158 }
159 if additional.typ == mdns::TYPE_AAAA {
160 let arr: [u8; 16] = match additional.rdata.clone().try_into() {
161 Ok(v) => v,
162 Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
163 };
164 let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
165 if !targets.contains_key(&additional.name) {
166 targets.insert(additional.name.clone(), Vec::new());
167 }
168 targets.get_mut(&additional.name).unwrap().push(val);
169 }
170 }
171 let mut all = msg.additional.to_vec();
172 all.append(&mut msg.answers.to_vec());
173 for additional in &all {
174 if additional.typ == mdns::TYPE_SRV {
175 let service_name = remove_string_suffix(&additional.name, &svcname);
176 if additional.rdata.len() < 6 {
177 continue;
178 }
179 let port = ((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16);
180 let target_name = {
181 if let Some(at) = additional.target.as_ref() {
182 at
183 } else {
184 continue;
185 }
186 };
187 let target_ip = targets.get(target_name).cloned().unwrap_or_default();
188 let mi = MatterDeviceInfo {
189 instance: service_name.clone(),
190 device: remove_string_suffix(target_name, ".local.").to_owned(),
191 ips: target_ip,
192 name: None,
193 discriminator: None,
194 commissioning_mode: None,
195 pairing_hint: None,
196 source_ip: msg.source.to_string(),
197 vendor_id: None,
198 product_id: None,
199 port: Some(port),
200 session_idle_interval_ms: None,
201 session_active_interval_ms: None,
202 session_active_threshold_ms: None,
203 device_type: None,
204 scope_id: None,
205 };
206 services.insert(service_name, mi);
207 }
208 }
209 for s in services.values() {
210 out.push(s.clone());
211 }
212
213 Ok(out)
214}
215
216pub fn to_matter_info(msg: &DnsMessage, svc: &str) -> Result<MatterDeviceInfo> {
217 let mut device = None;
218 let mut service = None;
219 let mut ips = BTreeMap::new();
220 let mut name = None;
221 let mut discriminator = None;
222 let mut cm = None;
223 let mut pairing_hint = None;
224 let mut vendor_id = None;
225 let mut product_id = None;
226 let mut port: Option<u16> = None;
227 let mut mrp = (None, None, None);
228 let mut device_type = None;
229
230 let mut matter_service = false;
231 let svcname = ".".to_owned() + svc + ".";
232 for answer in &msg.answers {
233 if answer.name == svcname[1..] {
234 matter_service = true
235 }
236 }
237 for additional in &msg.additional {
238 if additional.typ == mdns::TYPE_A {
239 let arr: [u8; 4] = match additional.rdata.clone().try_into() {
240 Ok(v) => v,
241 Err(_e) => return Err(anyhow::anyhow!("A record is not correct")),
242 };
243 let val = IpAddr::V4(Ipv4Addr::from_bits(u32::from_be_bytes(arr)));
244 ips.insert(val, true);
245 device = Some(remove_string_suffix(&additional.name, ".local."));
246 }
247 if additional.typ == mdns::TYPE_AAAA {
248 let arr: [u8; 16] = match additional.rdata.clone().try_into() {
249 Ok(v) => v,
250 Err(_e) => return Err(anyhow::anyhow!("AAAA record is not correct")),
251 };
252 let val = IpAddr::V6(Ipv6Addr::from_bits(u128::from_be_bytes(arr)));
253 ips.insert(val, true);
254 device = Some(remove_string_suffix(&additional.name, ".local."));
255 }
256 if additional.typ == mdns::TYPE_SRV {
257 service = Some(remove_string_suffix(&additional.name, &svcname));
258 if additional.rdata.len() >= 6 {
259 port = Some(((additional.rdata[4] as u16) << 8) | (additional.rdata[5] as u16))
260 }
261 }
262 if additional.typ == mdns::TYPE_TXT {
263 let rec = parse_txt_records(&additional.rdata)?;
264 name = rec.get("DN").cloned();
265 discriminator = rec.get("D").cloned();
266 pairing_hint = rec.get("PH").cloned();
267 device_type = rec.get("DT").cloned();
268 mrp = parse_mrp_txt(&rec);
269 if let Some(vp) = rec.get("VP") {
270 let mut split = vp.split("+");
271 vendor_id = split.next().map(str::to_owned);
272 product_id = split.next().map(str::to_owned);
273 }
274 cm = match rec.get("CM") {
275 Some(v) => match v.as_str() {
276 "0" => Some(CommissioningMode::No),
277 "1" => Some(CommissioningMode::Yes),
278 "2" => Some(CommissioningMode::WithPasscode),
279 _ => None,
280 },
281 None => None,
282 };
283 }
284 }
285
286 if !matter_service {
287 return Err(anyhow::anyhow!("not matter service"));
288 }
289
290 Ok(MatterDeviceInfo {
291 instance: service.context("service name not detected")?,
292 device: device.context("device name not detected")?,
293 ips: ips.into_keys().collect(),
294 name,
295 discriminator,
296 commissioning_mode: cm,
297 pairing_hint,
298 source_ip: msg.source.to_string(),
299 vendor_id,
300 product_id,
301 port,
302 session_idle_interval_ms: mrp.0,
303 session_active_interval_ms: mrp.1,
304 session_active_threshold_ms: mrp.2,
305 device_type,
306 scope_id: None,
307 })
308}
309
310async fn discover_common(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
311 let stop = tokio_util::sync::CancellationToken::new();
312 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
313
314 mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
315
316 tokio::spawn(async move {
317 tokio::time::sleep(timeout).await;
318 stop.cancel();
319 });
320 let mut cache = HashMap::new();
321 let mut out = Vec::new();
322 while let Some(dns) = receiver.recv().await {
323 if cache.contains_key(&dns) {
324 continue;
325 }
326 let info = match to_matter_info(&dns, svc_type) {
327 Ok(info) => info,
328 Err(_) => continue,
329 };
330 out.push(info);
331 cache.insert(dns, true);
332 }
333 Ok(out)
334}
335
336pub async fn discover_commissionable(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
338 discover_common(timeout, "_matterc._udp.local").await
339}
340
341pub async fn discover_commissioned(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
343 discover_common(timeout, "_matter._tcp.local").await
344}
345
346
347async fn discover_common2(timeout: Duration, svc_type: &str) -> Result<Vec<MatterDeviceInfo>> {
348 let stop = tokio_util::sync::CancellationToken::new();
349 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<DnsMessage>();
350
351 mdns::discover(svc_type, mdns::QTYPE_ANY, sender, stop.child_token()).await?;
352
353 tokio::spawn(async move {
354 tokio::time::sleep(timeout).await;
355 stop.cancel();
356 });
357 let mut cache = HashMap::new();
358 let mut out: Vec<MatterDeviceInfo> = Vec::new();
359 while let Some(dns) = receiver.recv().await {
360 if cache.contains_key(&dns) {
361 continue;
362 }
363 let info = match to_matter_info2(&dns, svc_type) {
364 Ok(info) => info,
365 Err(e) => {
366 log::trace!("failed to parse mdns message from {}: {:?}", dns.source, e);
367 continue;
368 },
369 };
370 for i in &info {
371 out.push(i.clone());
372 }
373 cache.insert(dns, true);
374 }
375 Ok(out)
376}
377
378pub async fn discover_commissionable2(timeout: Duration) -> Result<Vec<MatterDeviceInfo>> {
380 discover_common2(timeout, "_matterc._udp.local").await
381}
382
383pub async fn discover_commissioned2(timeout: Duration, device: &Option<String>) -> Result<Vec<MatterDeviceInfo>> {
385 let query = {
386 match device {
387 None => "_matter._tcp.local".to_owned(),
388 Some(d) => format!("{}._matter._tcp.local", d),
389 }
390 };
391 discover_common2(timeout, &query).await
392}
393
394
395
396pub async fn discover_one<F>(
404 mdns: &mdns2::MdnsService,
405 query: &str,
406 service_name: &str,
407 timeout: Duration,
408 predicate: F,
409) -> Result<(String, MatterDeviceInfo)>
410where
411 F: Fn(&str, &MatterDeviceInfo) -> bool,
412{
413 let mut rx = mdns.subscribe();
414 mdns.active_lookup(query, mdns::QTYPE_ANY).await;
415 let deadline = std::time::Instant::now() + timeout;
416 loop {
417 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
418 if remaining.is_zero() {
419 anyhow::bail!("mDNS discovery timeout for {}", query);
420 }
421 match tokio::time::timeout(remaining, rx.recv()).await {
422 Err(_) => anyhow::bail!("mDNS discovery timeout for {}", query),
423 Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(n))) => {
424 log::warn!("mDNS discovery: dropped {} events due to lag, continuing", n);
425 }
426 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
427 anyhow::bail!("mDNS service closed");
428 }
429 Ok(Ok(mdns2::MdnsEvent::ServiceExpired { .. })) => {}
430 Ok(Ok(mdns2::MdnsEvent::ServiceDiscovered { name, target, .. })) => {
431 if name != service_name {
432 continue;
433 }
434 let info = match extract_matter_info(&target, mdns).await {
435 Ok(i) => i,
436 Err(e) => {
437 log::debug!("failed to extract Matter info from {}: {}", target, e);
438 continue;
439 }
440 };
441 if predicate(&target, &info) {
442 return Ok((target, info));
443 }
444 }
445 }
446 }
447}
448
449pub async fn discover_all(
455 mdns: &mdns2::MdnsService,
456 query: &str,
457 service_name: &str,
458 timeout: Duration,
459) -> Result<Vec<(String, MatterDeviceInfo)>> {
460 let mut rx = mdns.subscribe();
461 mdns.active_lookup(query, mdns::QTYPE_ANY).await;
462 let deadline = std::time::Instant::now() + timeout;
463 let mut out = Vec::new();
464 loop {
465 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
466 if remaining.is_zero() {
467 break;
468 }
469 match tokio::time::timeout(remaining, rx.recv()).await {
470 Err(_) => break,
471 Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(n))) => {
472 log::warn!("mDNS discover_all: dropped {} events due to lag, continuing", n);
473 }
474 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => break,
475 Ok(Ok(mdns2::MdnsEvent::ServiceExpired { .. })) => {}
476 Ok(Ok(mdns2::MdnsEvent::ServiceDiscovered { name, target, .. })) => {
477 if name != service_name {
478 continue;
479 }
480 match extract_matter_info(&target, mdns).await {
481 Ok(info) => out.push((target, info)),
482 Err(e) => {
483 log::debug!("failed to extract Matter info from {}: {}", target, e);
484 }
485 }
486 }
487 }
488 }
489 Ok(out)
490}
491
492pub async fn extract_matter_info(target: &str, mdns: &mdns2::MdnsService) -> Result<MatterDeviceInfo> {
493 let txt_records = mdns.lookup(target, mdns::TYPE_TXT).await;
494 let mut txt_info = HashMap::new();
495 for txt_rr in txt_records {
496 txt_info.extend(parse_txt_records(&txt_rr.rdata)?);
497 }
498 let srv_records = mdns.lookup(target, mdns::TYPE_SRV).await;
499 let srv_rr = srv_records.first().ok_or_else(|| anyhow::anyhow!("No SRV record found for {}", target))?;
500 let (srv_target, port) = match srv_rr.data {
501 mdns::RRData::SRV { ref target, port, .. } => (target.clone(), port),
502 _ => return Err(anyhow::anyhow!("Invalid SRV record for {}", target)),
503 };
504 let mut ips = Vec::new();
505 let a_records = mdns.lookup(&srv_target, mdns::TYPE_A).await;
506 for a_rr in a_records {
507 if let mdns::RRData::A(ip) = a_rr.data {
508 ips.push(ip.into());
509 }
510 }
511 let aaaa_records = mdns.lookup(&srv_target, mdns::TYPE_AAAA).await;
512 for aaaa_rr in aaaa_records {
513 if let mdns::RRData::AAAA(ip) = aaaa_rr.data {
514 ips.push(ip.into());
515 }
516 }
517 let (vendor_id, product_id) = {
518 let vp = txt_info.get("VP");
519 if let Some(vp) = vp {
520 let mut parts = vp.split('+');
521 let vendor_id = parts.next();
522 let product_id = parts.next();
523 (vendor_id.map(|v| v.to_owned()), product_id.map(|p| p.to_owned()))
524 } else {
525 (None, None)
526 }
527 };
528 let discriminator = txt_info.get("D").cloned();
529 let name = txt_info.get("DN").cloned();
530 let commissioning_mode = match txt_info.get("CM") {
531 Some(v) => match v.as_str() {
532 "0" => Some(CommissioningMode::No),
533 "1" => Some(CommissioningMode::Yes),
534 "2" => Some(CommissioningMode::WithPasscode),
535 _ => None,
536 },
537 None => None,
538 };
539 let pairing_hint = txt_info.get("PH").cloned();
540 let device_type = txt_info.get("DT").cloned();
541 let (sii, sai, sat) = parse_mrp_txt(&txt_info);
542 let mut scope_id = None;
545 for ip in &ips {
546 if let IpAddr::V6(v6) = ip {
547 if (v6.segments()[0] & 0xffc0) == 0xfe80 {
548 scope_id = mdns.scope_for(v6).await;
549 if scope_id.is_some() {
550 break;
551 }
552 }
553 }
554 }
555 Ok(MatterDeviceInfo {
556 name,
557 instance: target.trim_end_matches('.').to_owned(),
558 device: srv_target.trim_end_matches('.').to_owned(),
559 ips,
560 vendor_id,
561 product_id,
562 discriminator,
563 commissioning_mode,
564 pairing_hint,
565 source_ip: "".to_owned(),
566 port: Some(port),
567 session_idle_interval_ms: sii,
568 session_active_interval_ms: sai,
569 session_active_threshold_ms: sat,
570 device_type,
571 scope_id,
572 })
573}
574
575pub fn addr_string(ip: &IpAddr, port: u16, scope_id: Option<u32>) -> String {
579 match ip {
580 IpAddr::V6(v6) if (v6.segments()[0] & 0xffc0) == 0xfe80 => match scope_id {
581 Some(idx) => format!("[{}%{}]:{}", v6, idx, port),
582 None => format!("[{}]:{}", v6, port),
583 },
584 IpAddr::V6(v6) => format!("[{}]:{}", v6, port),
585 IpAddr::V4(v4) => format!("{}:{}", v4, port),
586 }
587}
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn txt_rdata(entries: &[&str]) -> Vec<u8> {
593 let mut out = Vec::new();
594 for e in entries {
595 out.push(e.len() as u8);
596 out.extend_from_slice(e.as_bytes());
597 }
598 out
599 }
600
601 #[test]
602 fn test_parse_mrp_txt() {
603 let rec = parse_txt_records(&txt_rdata(&["SII=5000", "SAI=300", "SAT=4000", "D=840"]))
604 .unwrap();
605 assert_eq!(parse_mrp_txt(&rec), (Some(5000), Some(300), Some(4000)));
606
607 let rec = parse_txt_records(&txt_rdata(&["SII=abc", "D=840"])).unwrap();
608 assert_eq!(parse_mrp_txt(&rec), (None, None, None));
609
610 let rec = parse_txt_records(&txt_rdata(&["D=840"])).unwrap();
611 assert_eq!(parse_mrp_txt(&rec), (None, None, None));
612 }
613}