matc/mdns2/
dnssd.rs

1//! DNS-SD (DNS Service Discovery): service registration, record building, query matching.
2
3use std::net::{Ipv4Addr, Ipv6Addr};
4use std::time::{Duration, Instant};
5
6use byteorder::{BigEndian, WriteBytesExt};
7
8use crate::mdns;
9
10/// Description of a local service to advertise via mDNS.
11#[derive(Debug, Clone)]
12pub struct ServiceRegistration {
13    pub service_type: String,
14    pub instance_name: String,
15    pub port: u16,
16    pub hostname: String,
17    pub txt_records: Vec<(String, String)>,
18    pub ttl: u32,
19    pub subtypes: Vec<String>,
20}
21
22/// Events emitted by the mDNS service to the user.
23#[derive(Debug, Clone)]
24pub enum MdnsEvent {
25    ServiceDiscovered {
26        name: String,
27        target: String,
28        records: Vec<mdns::RR>,
29    },
30    ServiceExpired {
31        name: String,
32        rtype: u16,
33    },
34}
35
36pub(super) struct PeriodicQuery {
37    pub label: String,
38    pub qtype: u16,
39    pub interval: Duration,
40    pub last_sent: Instant,
41}
42
43/// Build the set of DNS records for a service registration.
44pub(super) fn build_service_records(
45    reg: &ServiceRegistration,
46    ips_v4: &[Ipv4Addr],
47    ips_v6: &[Ipv6Addr],
48) -> Vec<mdns::RR> {
49    let mut records = Vec::new();
50    let instance_full = format!("{}.{}", reg.instance_name, reg.service_type);
51
52    // PTR
53    records.push(mdns::RR {
54        name: format!("{}.", reg.service_type),
55        typ: mdns::TYPE_PTR,
56        class: 1,
57        ttl: reg.ttl,
58        rdata: {
59            let mut buf = Vec::new();
60            let _ = mdns::encode_label(&instance_full, &mut buf);
61            buf
62        },
63        target: None,
64        data: mdns::RRData::PTR(instance_full.clone()),
65    });
66
67    // Subtype PTR records: _<subtype>._sub.<service_type> -> instance_full
68    for sub in &reg.subtypes {
69        let subtype_name = format!("{}._sub.{}", sub, reg.service_type);
70        records.push(mdns::RR {
71            name: format!("{}.", subtype_name),
72            typ: mdns::TYPE_PTR,
73            class: 1,
74            ttl: reg.ttl,
75            rdata: {
76                let mut buf = Vec::new();
77                let _ = mdns::encode_label(&instance_full, &mut buf);
78                buf
79            },
80            target: None,
81            data: mdns::RRData::PTR(instance_full.clone()),
82        });
83    }
84
85    // SRV
86    let mut srv_rdata = Vec::new();
87    let _ = srv_rdata.write_u16::<BigEndian>(0); // priority
88    let _ = srv_rdata.write_u16::<BigEndian>(0); // weight
89    let _ = srv_rdata.write_u16::<BigEndian>(reg.port);
90    let _ = mdns::encode_label(reg.hostname.trim_end_matches('.'), &mut srv_rdata);
91    records.push(mdns::RR {
92        name: format!("{}.", instance_full),
93        typ: mdns::TYPE_SRV,
94        class: 1,
95        ttl: reg.ttl,
96        rdata: srv_rdata,
97        target: Some(format!("{}.", reg.hostname.trim_end_matches('.'))),
98        data: mdns::RRData::SRV {
99            priority: 0,
100            weight: 0,
101            port: reg.port,
102            target: format!("{}.", reg.hostname.trim_end_matches('.')),
103        },
104    });
105
106    // TXT
107    let mut txt_rdata = Vec::new();
108    for (k, v) in &reg.txt_records {
109        let entry = format!("{}={}", k, v);
110        let _ = txt_rdata.write_u8(entry.len() as u8);
111        txt_rdata.extend_from_slice(entry.as_bytes());
112    }
113    if txt_rdata.is_empty() {
114        txt_rdata.push(0); // RFC 6763: empty TXT record has single zero-length byte
115    }
116    records.push(mdns::RR {
117        name: format!("{}.", instance_full),
118        typ: mdns::TYPE_TXT,
119        class: 1,
120        ttl: reg.ttl,
121        rdata: txt_rdata,
122        target: None,
123        data: mdns::RRData::TXT(
124            reg.txt_records
125                .iter()
126                .map(|(k, v)| format!("{}={}", k, v))
127                .collect(),
128        ),
129    });
130
131
132    for ip in ips_v4 {
133        records.push(mdns::RR {
134            name: format!("{}.", reg.hostname.trim_end_matches('.')),
135            typ: mdns::TYPE_A,
136            class: 1,
137            ttl: reg.ttl,
138            rdata: ip.octets().to_vec(),
139            target: None,
140            data: mdns::RRData::A(*ip),
141        });
142    }
143
144
145    for ip in ips_v6 {
146        records.push(mdns::RR {
147            name: format!("{}.", reg.hostname.trim_end_matches('.')),
148            typ: mdns::TYPE_AAAA,
149            class: 1,
150            ttl: reg.ttl,
151            rdata: ip.octets().to_vec(),
152            target: None,
153            data: mdns::RRData::AAAA(*ip),
154        });
155    }
156
157    records
158}
159
160/// Find registered services that match an incoming query and build response records.
161pub(super) fn find_matching_services(
162    query_name: &str,
163    query_type: u16,
164    services: &[ServiceRegistration],
165    ips_v4: &[Ipv4Addr],
166    ips_v6: &[Ipv6Addr],
167) -> (Vec<mdns::RR>, Vec<mdns::RR>) {
168    let mut answers = Vec::new();
169    let mut additional = Vec::new();
170
171    let qname = query_name.to_lowercase();
172    let qname = qname.trim_end_matches('.');
173
174    for reg in services {
175        let svc_type = reg.service_type.trim_end_matches('.').to_lowercase();
176        let instance_full = format!("{}.{}", reg.instance_name.to_lowercase(), svc_type);
177
178        let all_records = build_service_records(reg, ips_v4, ips_v6);
179        let is_any = query_type == mdns::QTYPE_ANY;
180
181        // Check if query matches a subtype
182        let is_subtype_match = reg.subtypes.iter().any(|sub| {
183            let subtype_name = format!("{}._sub.{}", sub.to_lowercase(), svc_type);
184            qname == subtype_name
185        });
186
187        // Query for service type or subtype - return PTR as answer, rest as additional
188        if qname == svc_type || is_subtype_match {
189            for r in &all_records {
190                let rname = r.name.trim_end_matches('.').to_lowercase();
191                let name_matches = rname == qname || (rname == svc_type && !is_subtype_match);
192                if name_matches && (is_any || r.typ == mdns::TYPE_PTR || r.typ == query_type) {
193                    answers.push(r.clone());
194                } else if r.typ != mdns::TYPE_PTR {
195                    // Include non-PTR records as additional (SRV, TXT, A, AAAA)
196                    additional.push(r.clone());
197                }
198            }
199        }
200        // Query for specific instance - return SRV/TXT as answer, A/AAAA as additional
201        else if qname == instance_full {
202            for r in &all_records {
203                let rname = r.name.trim_end_matches('.').to_lowercase();
204                if rname == instance_full && (is_any || r.typ == query_type) {
205                    answers.push(r.clone());
206                } else if r.typ == mdns::TYPE_A || r.typ == mdns::TYPE_AAAA {
207                    additional.push(r.clone());
208                }
209            }
210        }
211        // Query for hostname - return A/AAAA as answer
212        else if qname == reg.hostname.trim_end_matches('.').to_lowercase() {
213            for r in &all_records {
214                if (r.typ == mdns::TYPE_A || r.typ == mdns::TYPE_AAAA)
215                    && (is_any || r.typ == query_type)
216                {
217                    answers.push(r.clone());
218                }
219            }
220        }
221    }
222
223    (answers, additional)
224}
225