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