Skip to main content

mdns_sd/
service_info.rs

1//! Define `ServiceInfo` to represent a service and its operations.
2
3#[cfg(feature = "logging")]
4use crate::log::{debug, trace};
5use crate::{
6    dns_parser::{DnsIncoming, DnsOutgoing, DnsRecordBox, DnsRecordExt, DnsSrv, RRType, ScopedIp},
7    Error, IfKind, InterfaceId, Result,
8};
9use if_addrs::{IfAddr, Interface};
10use std::net::Ipv6Addr;
11use std::{
12    cmp,
13    collections::{HashMap, HashSet},
14    fmt,
15    net::{IpAddr, Ipv4Addr},
16    str::FromStr,
17};
18
19#[cfg(feature = "serde")]
20use serde::{Deserialize, Serialize};
21
22/// Default TTL values in seconds
23const DNS_HOST_TTL: u32 = 120; // 2 minutes for host records (A, SRV etc) per RFC6762
24const DNS_OTHER_TTL: u32 = 4500; // 75 minutes for non-host records (PTR, TXT etc) per RFC6762
25
26/// Represents a network interface.
27#[derive(Debug)]
28pub(crate) struct MyIntf {
29    /// The name of the interface.
30    pub(crate) name: String,
31
32    /// Unique index assigned by the OS. Used by IPv6 for its scope_id.
33    pub(crate) index: u32,
34
35    /// One interface can have multiple IPv4 addresses and/or multiple IPv6 addresses.
36    pub(crate) addrs: HashSet<IfAddr>,
37}
38
39impl MyIntf {
40    pub(crate) fn next_ifaddr_v4(&self) -> Option<&IfAddr> {
41        self.addrs.iter().find(|a| a.ip().is_ipv4())
42    }
43
44    pub(crate) fn next_ifaddr_v6(&self) -> Option<&IfAddr> {
45        self.addrs.iter().find(|a| a.ip().is_ipv6())
46    }
47}
48
49impl From<&MyIntf> for InterfaceId {
50    fn from(my_intf: &MyIntf) -> Self {
51        InterfaceId {
52            name: my_intf.name.clone(),
53            index: my_intf.index,
54        }
55    }
56}
57
58/// Escapes dots and backslashes in a DNS instance name according to RFC 6763 Section 4.3.
59/// - '.' becomes '\.'
60/// - '\' becomes '\\'
61///
62/// Note: `\` itself needs to be escaped in the source code.
63///
64/// This is required when concatenating the three portions of a Service Instance Name
65/// to ensure that literal dots in the instance name are not interpreted as label separators.
66fn escape_instance_name(name: &str) -> String {
67    let mut result = String::with_capacity(name.len() + 10); // Extra space for escapes
68
69    for ch in name.chars() {
70        match ch {
71            '.' => {
72                result.push('\\');
73                result.push('.');
74            }
75            '\\' => {
76                result.push('\\');
77                result.push('\\');
78            }
79            _ => result.push(ch),
80        }
81    }
82
83    result
84}
85
86/// Complete info about a Service Instance.
87///
88/// We can construct some PTR, one SRV and one TXT record from this info,
89/// as well as A (IPv4 Address) and AAAA (IPv6 Address) records.
90#[derive(Debug, Clone)]
91pub struct ServiceInfo {
92    /// Service type and domain: {service-type-name}.{domain}
93    /// By default the service-type-name length must be <= 15.
94    /// so "_abcdefghijklmno._udp.local." would be valid but "_abcdefghijklmnop._udp.local." is not
95    ty_domain: String,
96
97    /// See RFC6763 section 7.1 about "Subtypes":
98    /// <https://datatracker.ietf.org/doc/html/rfc6763#section-7.1>
99    sub_domain: Option<String>, // <subservice>._sub.<service>.<domain>
100
101    fullname: String, // <instance>.<service>.<domain>
102    server: String,   // fully qualified name for service host
103    addresses: HashSet<IpAddr>,
104    port: u16,
105    host_ttl: u32,  // used for SRV and Address records
106    other_ttl: u32, // used for PTR and TXT records
107    priority: u16,
108    weight: u16,
109    txt_properties: TxtProperties,
110    addr_auto: bool, // Let the system update addresses automatically.
111
112    status: HashMap<u32, ServiceStatus>, // keyed by interface index.
113
114    /// Whether we need to probe names before announcing this service.
115    requires_probe: bool,
116
117    /// If set, the service is only exposed on these interfaces
118    supported_intfs: Vec<IfKind>,
119
120    /// If true, only link-local addresses are published.
121    is_link_local_only: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub(crate) enum ServiceStatus {
126    Probing,
127    Announced,
128    Unknown,
129}
130
131impl ServiceInfo {
132    /// Creates a new service info.
133    ///
134    /// `ty_domain` is the service type and the domain label, for example "_my-service._udp.local.".
135    /// By default the service type length must be <= 15 bytes
136    ///
137    /// `my_name` is the instance name, without the service type suffix.
138    /// It allows dots (`.`) and backslashes (`\`).
139    ///
140    /// `host_name` is the "host" in the context of DNS. It is used as the "name"
141    /// in the address records (i.e. TYPE_A and TYPE_AAAA records). It means that
142    /// for the same hostname in the same local network, the service resolves in
143    /// the same addresses. Be sure to check it if you see unexpected addresses resolved.
144    ///
145    /// `properties` can be `None` or key/value string pairs, in a type that
146    /// implements [`IntoTxtProperties`] trait. It supports:
147    /// - `HashMap<String, String>`
148    /// - `Option<HashMap<String, String>>`
149    /// - slice of tuple: `&[(K, V)]` where `K` and `V` are [`std::string::ToString`].
150    ///
151    /// Note: The maximum length of a single property string is `255`, Property that exceed the length are truncated.
152    /// > `len(key + value) < u8::MAX`
153    ///
154    /// `ip` can be one or more IP addresses, in a type that implements
155    /// [`AsIpAddrs`] trait. It supports:
156    ///
157    /// - Single IPv4: `"192.168.0.1"`
158    /// - Single IPv6: `"2001:0db8::7334"`
159    /// - Multiple IPv4 separated by comma: `"192.168.0.1,192.168.0.2"`
160    /// - Multiple IPv6 separated by comma: `"2001:0db8::7334,2001:0db8::7335"`
161    /// - A slice of IPv4: `&["192.168.0.1", "192.168.0.2"]`
162    /// - A slice of IPv6: `&["2001:0db8::7334", "2001:0db8::7335"]`
163    /// - A mix of IPv4 and IPv6: `"192.168.0.1,2001:0db8::7334"`
164    /// - All the above formats with [IpAddr] or `String` instead of `&str`.
165    ///
166    /// The host TTL and other TTL are set to default values.
167    pub fn new<Ip: AsIpAddrs, P: IntoTxtProperties>(
168        ty_domain: &str,
169        my_name: &str,
170        host_name: &str,
171        ip: Ip,
172        port: u16,
173        properties: P,
174    ) -> Result<Self> {
175        let (ty_domain, sub_domain) = split_sub_domain(ty_domain);
176
177        let escaped_name = escape_instance_name(my_name);
178        let fullname = format!("{escaped_name}.{ty_domain}");
179        let ty_domain = ty_domain.to_string();
180        let sub_domain = sub_domain.map(str::to_string);
181        let server = normalize_hostname(host_name.to_string());
182        let addresses = ip.as_ip_addrs()?;
183        let txt_properties = properties.into_txt_properties();
184
185        // RFC6763 section 6.4: https://www.rfc-editor.org/rfc/rfc6763#section-6.4
186        // The characters of a key MUST be printable US-ASCII values (0x20-0x7E)
187        // [RFC20], excluding '=' (0x3D).
188        for prop in txt_properties.iter() {
189            let key = prop.key();
190            if !key.is_ascii() {
191                return Err(Error::Msg(format!(
192                    "TXT property key {} is not ASCII",
193                    prop.key()
194                )));
195            }
196            if key.contains('=') {
197                return Err(Error::Msg(format!(
198                    "TXT property key {} contains '='",
199                    prop.key()
200                )));
201            }
202
203            // RFC6763 section 6.1: each TXT record string is prefixed by a
204            // single length byte, so it cannot exceed 255 bytes.
205            let prop_len = key.len() + prop.val().map_or(0, |v| v.len() + 1);
206            if prop_len > u8::MAX as usize {
207                return Err(Error::Msg(format!(
208                    "TXT property '{}' has length {} bytes, exceeding the 255-byte limit",
209                    key, prop_len
210                )));
211            }
212        }
213
214        let this = Self {
215            ty_domain,
216            sub_domain,
217            fullname,
218            server,
219            addresses,
220            port,
221            host_ttl: DNS_HOST_TTL,
222            other_ttl: DNS_OTHER_TTL,
223            priority: 0,
224            weight: 0,
225            txt_properties,
226            addr_auto: false,
227            status: HashMap::new(),
228            requires_probe: true,
229            is_link_local_only: false,
230            supported_intfs: vec![IfKind::All],
231        };
232
233        Ok(this)
234    }
235
236    /// Indicates that the library should automatically
237    /// update the addresses of this service, when IP
238    /// address(es) are added or removed on the host.
239    pub const fn enable_addr_auto(mut self) -> Self {
240        self.addr_auto = true;
241        self
242    }
243
244    /// Returns if the service's addresses will be updated
245    /// automatically when the host IP addrs change.
246    pub const fn is_addr_auto(&self) -> bool {
247        self.addr_auto
248    }
249
250    /// Set whether this service info requires name probing for potential name conflicts.
251    ///
252    /// By default, it is true (i.e. requires probing) for every service info. You
253    /// set it to `false` only when you are sure there are no conflicts, or for testing purposes.
254    pub fn set_requires_probe(&mut self, enable: bool) {
255        self.requires_probe = enable;
256    }
257
258    /// Set whether the service is restricted to link-local addresses.
259    ///
260    /// By default, it is false.
261    pub fn set_link_local_only(&mut self, is_link_local_only: bool) {
262        self.is_link_local_only = is_link_local_only;
263    }
264
265    /// Set the supported interfaces for this service.
266    ///
267    /// The service will be advertised on the provided interfaces only. When ips are auto-detected
268    /// (via 'enable_addr_auto') only addresses on these interfaces will be considered.
269    pub fn set_interfaces(&mut self, intfs: Vec<IfKind>) {
270        self.supported_intfs = intfs;
271    }
272
273    /// Returns whether this service info requires name probing for potential name conflicts.
274    ///
275    /// By default, it returns true for every service info.
276    pub const fn requires_probe(&self) -> bool {
277        self.requires_probe
278    }
279
280    /// Returns the service type including the domain label.
281    ///
282    /// For example: "_my-service._udp.local.".
283    #[inline]
284    pub fn get_type(&self) -> &str {
285        &self.ty_domain
286    }
287
288    /// Returns the service subtype including the domain label,
289    /// if subtype has been defined.
290    ///
291    /// For example: "_printer._sub._http._tcp.local.".
292    #[inline]
293    pub const fn get_subtype(&self) -> &Option<String> {
294        &self.sub_domain
295    }
296
297    /// Returns whether the service type or subtype matches the given name.
298    pub(crate) fn matches_type_or_subtype(&self, name: &str) -> bool {
299        name == self.get_type() || self.get_subtype().as_ref().is_some_and(|v| v == name)
300    }
301
302    /// Returns a reference of the service fullname.
303    ///
304    /// This is useful, for example, in unregister.
305    #[inline]
306    pub fn get_fullname(&self) -> &str {
307        &self.fullname
308    }
309
310    /// Returns the properties from TXT records.
311    #[inline]
312    pub const fn get_properties(&self) -> &TxtProperties {
313        &self.txt_properties
314    }
315
316    /// Returns a property for a given `key`, where `key` is
317    /// case insensitive.
318    ///
319    /// Returns `None` if `key` does not exist.
320    pub fn get_property(&self, key: &str) -> Option<&TxtProperty> {
321        self.txt_properties.get(key)
322    }
323
324    /// Returns a property value for a given `key`, where `key` is
325    /// case insensitive.
326    ///
327    /// Returns `None` if `key` does not exist.
328    pub fn get_property_val(&self, key: &str) -> Option<Option<&[u8]>> {
329        self.txt_properties.get_property_val(key)
330    }
331
332    /// Returns a property value string for a given `key`, where `key` is
333    /// case insensitive.
334    ///
335    /// Returns `None` if `key` does not exist.
336    pub fn get_property_val_str(&self, key: &str) -> Option<&str> {
337        self.txt_properties.get_property_val_str(key)
338    }
339
340    /// Returns the service's hostname.
341    #[inline]
342    pub fn get_hostname(&self) -> &str {
343        &self.server
344    }
345
346    /// Returns the service's port.
347    #[inline]
348    pub const fn get_port(&self) -> u16 {
349        self.port
350    }
351
352    /// Returns the service's addresses
353    #[inline]
354    pub const fn get_addresses(&self) -> &HashSet<IpAddr> {
355        &self.addresses
356    }
357
358    /// Returns the service's IPv4 addresses only.
359    pub fn get_addresses_v4(&self) -> HashSet<&Ipv4Addr> {
360        let mut ipv4_addresses = HashSet::new();
361
362        for ip in &self.addresses {
363            if let IpAddr::V4(ipv4) = ip {
364                ipv4_addresses.insert(ipv4);
365            }
366        }
367
368        ipv4_addresses
369    }
370
371    /// Returns the service's TTL used for SRV and Address records.
372    #[inline]
373    pub const fn get_host_ttl(&self) -> u32 {
374        self.host_ttl
375    }
376
377    /// Returns the service's TTL used for PTR and TXT records.
378    #[inline]
379    pub const fn get_other_ttl(&self) -> u32 {
380        self.other_ttl
381    }
382
383    /// Returns the service's priority used in SRV records.
384    #[inline]
385    pub const fn get_priority(&self) -> u16 {
386        self.priority
387    }
388
389    /// Returns the service's weight used in SRV records.
390    #[inline]
391    pub const fn get_weight(&self) -> u16 {
392        self.weight
393    }
394
395    /// Returns all addresses published
396    pub(crate) fn get_addrs_on_my_intf_v4(&self, my_intf: &MyIntf) -> Vec<IpAddr> {
397        self.addresses
398            .iter()
399            .filter(|a| a.is_ipv4() && my_intf.addrs.iter().any(|x| valid_ip_on_intf(a, x)))
400            .copied()
401            .collect()
402    }
403
404    pub(crate) fn get_addrs_on_my_intf_v6(&self, my_intf: &MyIntf) -> Vec<IpAddr> {
405        self.addresses
406            .iter()
407            .filter(|a| a.is_ipv6() && my_intf.addrs.iter().any(|x| valid_ip_on_intf(a, x)))
408            .copied()
409            .collect()
410    }
411
412    /// Returns whether the service info is ready to be resolved.
413    pub(crate) fn _is_ready(&self) -> bool {
414        let some_missing = self.ty_domain.is_empty()
415            || self.fullname.is_empty()
416            || self.server.is_empty()
417            || self.addresses.is_empty();
418        !some_missing
419    }
420
421    /// Insert `addr` into service info addresses.
422    pub(crate) fn insert_ipaddr(&mut self, intf: &Interface) {
423        if self.is_address_supported(intf) {
424            self.addresses.insert(intf.addr.ip());
425        } else {
426            trace!(
427                "skipping unsupported address {} for service {}",
428                intf.addr.ip(),
429                self.fullname
430            );
431        }
432    }
433
434    pub(crate) fn remove_ipaddr(&mut self, addr: &IpAddr) {
435        self.addresses.remove(addr);
436    }
437
438    pub(crate) fn generate_txt(&self) -> Vec<u8> {
439        encode_txt(self.get_properties().iter())
440    }
441
442    pub(crate) fn _set_port(&mut self, port: u16) {
443        self.port = port;
444    }
445
446    pub(crate) fn _set_hostname(&mut self, hostname: String) {
447        self.server = normalize_hostname(hostname);
448    }
449
450    /// Returns true if properties are updated.
451    pub(crate) fn _set_properties_from_txt(&mut self, txt: &[u8]) -> bool {
452        let properties = decode_txt_unique(txt);
453        if self.txt_properties.properties != properties {
454            self.txt_properties = TxtProperties { properties };
455            true
456        } else {
457            false
458        }
459    }
460
461    pub(crate) fn _set_subtype(&mut self, subtype: String) {
462        self.sub_domain = Some(subtype);
463    }
464
465    /// host_ttl is for SRV and address records
466    /// currently only used for testing.
467    pub(crate) fn _set_host_ttl(&mut self, ttl: u32) {
468        self.host_ttl = ttl;
469    }
470
471    /// other_ttl is for PTR and TXT records.
472    pub(crate) fn _set_other_ttl(&mut self, ttl: u32) {
473        self.other_ttl = ttl;
474    }
475
476    pub(crate) fn set_status(&mut self, if_index: u32, status: ServiceStatus) {
477        match self.status.get_mut(&if_index) {
478            Some(service_status) => {
479                *service_status = status;
480            }
481            None => {
482                self.status.entry(if_index).or_insert(status);
483            }
484        }
485    }
486
487    pub(crate) fn get_status(&self, intf: u32) -> ServiceStatus {
488        self.status
489            .get(&intf)
490            .cloned()
491            .unwrap_or(ServiceStatus::Unknown)
492    }
493
494    /// Consumes self and returns a resolved service, i.e. a lite version of `ServiceInfo`.
495    pub fn as_resolved_service(self) -> ResolvedService {
496        let addresses: HashSet<ScopedIp> = self.addresses.into_iter().map(|a| a.into()).collect();
497        ResolvedService {
498            ty_domain: self.ty_domain,
499            sub_ty_domain: self.sub_domain,
500            fullname: self.fullname,
501            host: self.server,
502            port: self.port,
503            addresses,
504            txt_properties: self.txt_properties,
505        }
506    }
507
508    fn is_address_supported(&self, intf: &Interface) -> bool {
509        let interface_supported = self.supported_intfs.iter().any(|i| i.matches(intf));
510        let addr = intf.ip();
511        let passes_link_local = !self.is_link_local_only
512            || match &addr {
513                IpAddr::V4(ipv4) => ipv4.is_link_local(),
514                IpAddr::V6(ipv6) => is_unicast_link_local(ipv6),
515            };
516        debug!(
517            "matching inserted address {} on intf {}: passes_link_local={}, interface_supported={}",
518            addr, addr, passes_link_local, interface_supported
519        );
520        interface_supported && passes_link_local
521    }
522}
523
524/// Removes potentially duplicated ".local." at the end of "hostname".
525fn normalize_hostname(mut hostname: String) -> String {
526    if hostname.ends_with(".local.local.") {
527        let new_len = hostname.len() - "local.".len();
528        hostname.truncate(new_len);
529    }
530    hostname
531}
532
533/// This trait allows for parsing an input into a set of one or multiple [`Ipv4Addr`].
534pub trait AsIpAddrs {
535    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>>;
536}
537
538impl<T: AsIpAddrs> AsIpAddrs for &T {
539    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
540        (*self).as_ip_addrs()
541    }
542}
543
544/// Supports one address or multiple addresses separated by `,`.
545/// For example: "127.0.0.1,127.0.0.2".
546///
547/// If the string is empty, will return an empty set.
548impl AsIpAddrs for &str {
549    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
550        let mut addrs = HashSet::new();
551
552        if !self.is_empty() {
553            let iter = self.split(',').map(str::trim).map(IpAddr::from_str);
554            for addr in iter {
555                let addr = addr.map_err(|err| Error::ParseIpAddr(err.to_string()))?;
556                addrs.insert(addr);
557            }
558        }
559
560        Ok(addrs)
561    }
562}
563
564impl AsIpAddrs for String {
565    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
566        self.as_str().as_ip_addrs()
567    }
568}
569
570/// Support slice. Example: &["127.0.0.1", "127.0.0.2"]
571impl<I: AsIpAddrs> AsIpAddrs for &[I] {
572    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
573        let mut addrs = HashSet::new();
574
575        for result in self.iter().map(I::as_ip_addrs) {
576            addrs.extend(result?);
577        }
578
579        Ok(addrs)
580    }
581}
582
583/// Optimization for zero sized/empty values, as `()` will never take up any space or evaluate to
584/// anything, helpful in contexts where we just want an empty value.
585impl AsIpAddrs for () {
586    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
587        Ok(HashSet::new())
588    }
589}
590
591impl AsIpAddrs for std::net::IpAddr {
592    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
593        let mut ips = HashSet::new();
594        ips.insert(*self);
595
596        Ok(ips)
597    }
598}
599
600impl AsIpAddrs for Box<dyn AsIpAddrs> {
601    fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>> {
602        self.as_ref().as_ip_addrs()
603    }
604}
605
606/// Represents properties in a TXT record.
607///
608/// The key string of a property is case insensitive, and only
609/// one [`TxtProperty`] is stored for the same key.
610///
611/// [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763#section-6.4):
612/// "A given key SHOULD NOT appear more than once in a TXT record."
613#[derive(Debug, Clone, PartialEq, Eq)]
614#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
615#[cfg_attr(feature = "serde", serde(transparent))]
616pub struct TxtProperties {
617    // Use `Vec` instead of `HashMap` to keep the order of insertions.
618    properties: Vec<TxtProperty>,
619}
620
621impl Default for TxtProperties {
622    fn default() -> Self {
623        TxtProperties::new()
624    }
625}
626
627impl TxtProperties {
628    pub fn new() -> Self {
629        TxtProperties {
630            properties: Vec::new(),
631        }
632    }
633
634    /// Returns an iterator for all properties.
635    pub fn iter(&self) -> impl Iterator<Item = &TxtProperty> {
636        self.properties.iter()
637    }
638
639    /// Returns the number of properties.
640    pub fn len(&self) -> usize {
641        self.properties.len()
642    }
643
644    /// Returns if the properties are empty.
645    pub fn is_empty(&self) -> bool {
646        self.properties.is_empty()
647    }
648
649    /// Returns a property for a given `key`, where `key` is
650    /// case insensitive.
651    pub fn get(&self, key: &str) -> Option<&TxtProperty> {
652        let key = key.to_lowercase();
653        self.properties
654            .iter()
655            .find(|&prop| prop.key.to_lowercase() == key)
656    }
657
658    /// Returns a property value for a given `key`, where `key` is
659    /// case insensitive.
660    ///
661    /// Returns `None` if `key` does not exist.
662    /// Returns `Some(Option<&u8>)` for its value.
663    pub fn get_property_val(&self, key: &str) -> Option<Option<&[u8]>> {
664        self.get(key).map(|x| x.val())
665    }
666
667    /// Returns a property value string for a given `key`, where `key` is
668    /// case insensitive.
669    ///
670    /// Returns `None` if `key` does not exist.
671    /// Returns `Some("")` if its value is `None` or is empty.
672    pub fn get_property_val_str(&self, key: &str) -> Option<&str> {
673        self.get(key).map(|x| x.val_str())
674    }
675
676    /// Consumes properties and returns a hashmap, where the keys are the properties keys.
677    ///
678    /// If a property value is empty, return an empty string (because RFC 6763 allows empty values).
679    /// If a property value is non-empty but not valid UTF-8, skip the property and log a message.
680    pub fn into_property_map_str(self) -> HashMap<String, String> {
681        self.properties
682            .into_iter()
683            .filter_map(|property| {
684                let val_string = property.val.map_or(Some(String::new()), |val| {
685                    String::from_utf8(val)
686                        .map_err(|e| {
687                            debug!("Property value contains invalid UTF-8: {e}");
688                        })
689                        .ok()
690                })?;
691                Some((property.key, val_string))
692            })
693            .collect()
694    }
695}
696
697impl fmt::Display for TxtProperties {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        let delimiter = ", ";
700        let props: Vec<String> = self.properties.iter().map(|p| p.to_string()).collect();
701        write!(f, "({})", props.join(delimiter))
702    }
703}
704
705impl From<&[u8]> for TxtProperties {
706    fn from(txt: &[u8]) -> Self {
707        let properties = decode_txt_unique(txt);
708        TxtProperties { properties }
709    }
710}
711
712/// Represents a property in a TXT record.
713#[derive(Clone, PartialEq, Eq)]
714#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
715pub struct TxtProperty {
716    /// The name of the property. The original cases are kept.
717    key: String,
718
719    /// RFC 6763 says values are bytes, not necessarily UTF-8.
720    /// It is also possible that there is no value, in which case
721    /// the key is a boolean key.
722    #[cfg_attr(feature = "serde", serde(rename = "value"))]
723    val: Option<Vec<u8>>,
724}
725
726impl TxtProperty {
727    /// Returns the key of a property.
728    pub fn key(&self) -> &str {
729        &self.key
730    }
731
732    /// Returns the value of a property, which could be `None`.
733    ///
734    /// To obtain a `&str` of the value, use `val_str()` instead.
735    pub fn val(&self) -> Option<&[u8]> {
736        self.val.as_deref()
737    }
738
739    /// Returns the value of a property as str.
740    pub fn val_str(&self) -> &str {
741        self.val
742            .as_ref()
743            .map_or("", |v| std::str::from_utf8(&v[..]).unwrap_or_default())
744    }
745}
746
747/// Supports constructing from a tuple.
748impl<K, V> From<&(K, V)> for TxtProperty
749where
750    K: ToString,
751    V: ToString,
752{
753    fn from(prop: &(K, V)) -> Self {
754        Self {
755            key: prop.0.to_string(),
756            val: Some(prop.1.to_string().into_bytes()),
757        }
758    }
759}
760
761impl<K, V> From<(K, V)> for TxtProperty
762where
763    K: ToString,
764    V: AsRef<[u8]>,
765{
766    fn from(prop: (K, V)) -> Self {
767        Self {
768            key: prop.0.to_string(),
769            val: Some(prop.1.as_ref().into()),
770        }
771    }
772}
773
774/// Support a property that has no value.
775impl From<&str> for TxtProperty {
776    fn from(key: &str) -> Self {
777        Self {
778            key: key.to_string(),
779            val: None,
780        }
781    }
782}
783
784impl fmt::Display for TxtProperty {
785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786        write!(f, "{}={}", self.key, self.val_str())
787    }
788}
789
790/// Mimic the default debug output for a struct, with a twist:
791/// - If self.var is UTF-8, will output it as a string in double quotes.
792/// - If self.var is not UTF-8, will output its bytes as in hex.
793impl fmt::Debug for TxtProperty {
794    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795        let val_string = self.val.as_ref().map_or_else(
796            || "None".to_string(),
797            |v| {
798                std::str::from_utf8(&v[..]).map_or_else(
799                    |_| format!("Some({})", u8_slice_to_hex(&v[..])),
800                    |s| format!("Some(\"{s}\")"),
801                )
802            },
803        );
804
805        write!(
806            f,
807            "TxtProperty {{key: \"{}\", val: {}}}",
808            &self.key, &val_string,
809        )
810    }
811}
812
813const HEX_TABLE: [char; 16] = [
814    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
815];
816
817/// Create a hex string from `slice`, with a "0x" prefix.
818///
819/// For example, [1u8, 2u8] -> "0x0102"
820fn u8_slice_to_hex(slice: &[u8]) -> String {
821    let mut hex = String::with_capacity(slice.len() * 2 + 2);
822    hex.push_str("0x");
823    for b in slice {
824        hex.push(HEX_TABLE[(b >> 4) as usize]);
825        hex.push(HEX_TABLE[(b & 0x0F) as usize]);
826    }
827    hex
828}
829
830/// This trait allows for converting inputs into [`TxtProperties`].
831pub trait IntoTxtProperties {
832    fn into_txt_properties(self) -> TxtProperties;
833}
834
835impl IntoTxtProperties for HashMap<String, String> {
836    fn into_txt_properties(mut self) -> TxtProperties {
837        let properties = self
838            .drain()
839            .map(|(key, val)| TxtProperty {
840                key,
841                val: Some(val.into_bytes()),
842            })
843            .collect();
844        TxtProperties { properties }
845    }
846}
847
848/// Mainly for backward compatibility.
849impl IntoTxtProperties for Option<HashMap<String, String>> {
850    fn into_txt_properties(self) -> TxtProperties {
851        self.map_or_else(
852            || TxtProperties {
853                properties: Vec::new(),
854            },
855            |h| h.into_txt_properties(),
856        )
857    }
858}
859
860/// Support Vec like `[("k1", "v1"), ("k2", "v2")]`.
861impl<'a, T: 'a> IntoTxtProperties for &'a [T]
862where
863    TxtProperty: From<&'a T>,
864{
865    fn into_txt_properties(self) -> TxtProperties {
866        let mut properties = Vec::new();
867        let mut keys = HashSet::new();
868        for t in self.iter() {
869            let prop = TxtProperty::from(t);
870            let key = prop.key.to_lowercase();
871            if keys.insert(key) {
872                // Only push a new entry if the key did not exist.
873                //
874                // RFC 6763: https://www.rfc-editor.org/rfc/rfc6763#section-6.4
875                //
876                // "If a client receives a TXT record containing the same key more than
877                //    once, then the client MUST silently ignore all but the first
878                //    occurrence of that attribute. "
879                properties.push(prop);
880            }
881        }
882        TxtProperties { properties }
883    }
884}
885
886impl IntoTxtProperties for Vec<TxtProperty> {
887    fn into_txt_properties(self) -> TxtProperties {
888        TxtProperties { properties: self }
889    }
890}
891
892// Convert from properties key/value pairs to DNS TXT record content
893fn encode_txt<'a>(properties: impl Iterator<Item = &'a TxtProperty>) -> Vec<u8> {
894    let mut bytes = Vec::new();
895    for prop in properties {
896        let mut s = prop.key.clone().into_bytes();
897        if let Some(v) = &prop.val {
898            s.extend(b"=");
899            s.extend(v);
900        }
901
902        debug_assert!(
903            s.len() <= u8::MAX as usize,
904            "TXT property '{}' exceeds 255 bytes; should have been validated in ServiceInfo::new()",
905            prop.key
906        );
907        s.truncate(u8::MAX as usize);
908        let sz: u8 = s.len() as u8;
909
910        // TXT uses (Length,Value) format for each property,
911        // i.e. the first byte is the length.
912        bytes.push(sz);
913        bytes.extend(s);
914    }
915    if bytes.is_empty() {
916        bytes.push(0);
917    }
918    bytes
919}
920
921// Convert from DNS TXT record content to key/value pairs
922pub(crate) fn decode_txt(txt: &[u8]) -> Vec<TxtProperty> {
923    let mut properties = Vec::new();
924    let mut offset = 0;
925    while offset < txt.len() {
926        let length = txt[offset] as usize;
927        if length == 0 {
928            break; // reached the end
929        }
930        offset += 1; // move over the length byte
931
932        let offset_end = offset + length;
933        if offset_end > txt.len() {
934            debug!("DNS TXT record contains invalid data: Size given for property would be out of range. (offset={}, length={}, offset_end={}, record length={})", offset, length, offset_end, txt.len());
935            break; // Skipping the rest of the record content, as the size for this property would already be out of range.
936        }
937        let kv_bytes = &txt[offset..offset_end];
938
939        // split key and val using the first `=`
940        let (k, v) = kv_bytes.iter().position(|&x| x == b'=').map_or_else(
941            || (kv_bytes.to_vec(), None),
942            |idx| (kv_bytes[..idx].to_vec(), Some(kv_bytes[idx + 1..].to_vec())),
943        );
944
945        // Make sure the key can be stored in UTF-8.
946        match String::from_utf8(k) {
947            Ok(k_string) => {
948                properties.push(TxtProperty {
949                    key: k_string,
950                    val: v,
951                });
952            }
953            Err(e) => debug!("failed to convert to String from key: {}", e),
954        }
955
956        offset += length;
957    }
958
959    properties
960}
961
962fn decode_txt_unique(txt: &[u8]) -> Vec<TxtProperty> {
963    let mut properties = decode_txt(txt);
964
965    // Remove duplicated keys and retain only the first appearance
966    // of each key.
967    let mut keys = HashSet::new();
968    properties.retain(|p| {
969        let key = p.key().to_lowercase();
970        keys.insert(key) // returns True if key is new.
971    });
972    properties
973}
974
975/// Returns true if `addr` is in the same network of `intf`.
976pub(crate) fn valid_ip_on_intf(addr: &IpAddr, if_addr: &IfAddr) -> bool {
977    match (addr, if_addr) {
978        (IpAddr::V4(addr), IfAddr::V4(if_v4)) => {
979            let netmask = u32::from(if_v4.netmask);
980            let intf_net = u32::from(if_v4.ip) & netmask;
981            let addr_net = u32::from(*addr) & netmask;
982            addr_net == intf_net
983        }
984        (IpAddr::V6(addr), IfAddr::V6(if_v6)) => {
985            let netmask = u128::from(if_v6.netmask);
986            let intf_net = u128::from(if_v6.ip) & netmask;
987            let addr_net = u128::from(*addr) & netmask;
988            addr_net == intf_net
989        }
990        _ => false,
991    }
992}
993
994/// A probing for a particular name.
995#[derive(Debug)]
996pub(crate) struct Probe {
997    /// All records probing for the same name.
998    pub(crate) records: Vec<DnsRecordBox>,
999
1000    /// The fullnames of services that are probing these records.
1001    /// These are the original service names, will not change per conflicts.
1002    pub(crate) waiting_services: HashSet<String>,
1003
1004    /// The time (T) to send the first query .
1005    pub(crate) start_time: u64,
1006
1007    /// The time to send the next (including the first) query.
1008    pub(crate) next_send: u64,
1009}
1010
1011impl Probe {
1012    pub(crate) fn new(start_time: u64) -> Self {
1013        // RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.1:
1014        //
1015        // "250 ms after the first query, the host should send a second; then,
1016        //   250 ms after that, a third.  If, by 250 ms after the third probe, no
1017        //   conflicting Multicast DNS responses have been received, the host may
1018        //   move to the next step, announcing. "
1019        let next_send = start_time;
1020
1021        Self {
1022            records: Vec::new(),
1023            waiting_services: HashSet::new(),
1024            start_time,
1025            next_send,
1026        }
1027    }
1028
1029    /// Add a new record with the same probing name in a sorted order.
1030    pub(crate) fn insert_record(&mut self, record: DnsRecordBox) {
1031        /*
1032        RFC 6762: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2.1
1033
1034        " The records are sorted using the same lexicographical order as
1035        described above, that is, if the record classes differ, the record
1036        with the lower class number comes first.  If the classes are the same
1037        but the rrtypes differ, the record with the lower rrtype number comes
1038        first."
1039         */
1040        let insert_position = self
1041            .records
1042            .binary_search_by(
1043                |existing| match existing.get_class().cmp(&record.get_class()) {
1044                    std::cmp::Ordering::Equal => existing.get_type().cmp(&record.get_type()),
1045                    other => other,
1046                },
1047            )
1048            .unwrap_or_else(|pos| pos);
1049
1050        self.records.insert(insert_position, record);
1051    }
1052
1053    /// Compares with `incoming` records. Postpone probe and retry if we yield.
1054    pub(crate) fn tiebreaking(&mut self, msg: &DnsIncoming, probe_name: &str) {
1055        let now = crate::current_time_millis();
1056
1057        // Only do tiebreaking if probe already started.
1058        // This check also helps avoid redo tiebreaking if start time
1059        // was postponed.
1060        if self.start_time >= now {
1061            return;
1062        }
1063
1064        let incoming: Vec<_> = msg
1065            .authorities()
1066            .iter()
1067            .filter(|r| r.get_name() == probe_name)
1068            .collect();
1069        /*
1070        RFC 6762 section 8.2: https://datatracker.ietf.org/doc/html/rfc6762#section-8.2
1071        ...
1072        if the host finds that its own data is lexicographically later, it
1073        simply ignores the other host's probe.  If the host finds that its
1074        own data is lexicographically earlier, then it defers to the winning
1075        host by waiting one second, and then begins probing for this record
1076        again.
1077        */
1078        let min_len = self.records.len().min(incoming.len());
1079
1080        // Compare elements up to the length of the shorter vector
1081        let mut cmp_result = cmp::Ordering::Equal;
1082        for (i, incoming_record) in incoming.iter().enumerate().take(min_len) {
1083            match self.records[i].compare(incoming_record.as_ref()) {
1084                cmp::Ordering::Equal => continue,
1085                other => {
1086                    cmp_result = other;
1087                    break; // exit loop on first difference
1088                }
1089            }
1090        }
1091
1092        if cmp_result == cmp::Ordering::Equal {
1093            // If all compared records are equal, compare the lengths of the records.
1094            cmp_result = self.records.len().cmp(&incoming.len());
1095        }
1096
1097        match cmp_result {
1098            cmp::Ordering::Less => {
1099                debug!("tiebreaking '{probe_name}': LOST, will wait for one second",);
1100                self.start_time = now + 1000; // wait and restart.
1101                self.next_send = now + 1000;
1102            }
1103            ordering => {
1104                debug!("tiebreaking '{probe_name}': {:?}", ordering);
1105            }
1106        }
1107    }
1108
1109    pub(crate) fn update_next_send(&mut self, now: u64) {
1110        self.next_send = now + 250;
1111    }
1112
1113    /// Returns whether this probe is finished.
1114    pub(crate) fn expired(&self, now: u64) -> bool {
1115        // The 2nd query is T + 250ms, the 3rd query is T + 500ms,
1116        // The expire time is T + 750ms
1117        now >= self.start_time + 750
1118    }
1119}
1120
1121/// DNS records of all the registered services.
1122pub(crate) struct DnsRegistry {
1123    /// keyed by the name of all related DNS records.
1124    /*
1125     When a host is probing for a group of related records with the same
1126    name (e.g., the SRV and TXT record describing a DNS-SD service), only
1127    a single question need be placed in the Question Section, since query
1128    type "ANY" (255) is used, which will elicit answers for all records
1129    with that name.  However, for tiebreaking to work correctly in all
1130    cases, the Authority Section must contain *all* the records and
1131    proposed rdata being probed for uniqueness.
1132     */
1133    pub(crate) probing: HashMap<String, Probe>,
1134
1135    /// Already done probing, or no need to probe.
1136    /// Keyed by DNS record name.
1137    pub(crate) active: HashMap<String, Vec<DnsRecordBox>>,
1138
1139    /// timers of the newly added probes.
1140    pub(crate) new_timers: Vec<u64>,
1141
1142    /// Mapping from original names to new names.
1143    pub(crate) name_changes: HashMap<String, String>,
1144
1145    /// RFC 6762 section 6: the last time (in millis) each record was multicast
1146    /// on this interface's IPv4 group, keyed by the record's identity
1147    /// (name + type + rdata). Used to enforce the per-record, per-interface
1148    /// one-second rate limit.
1149    ///
1150    /// IPv4 and IPv6 are tracked separately: a single interface (`if_index`)
1151    /// carries both address families, but they are distinct multicast groups
1152    /// (`224.0.0.251` and `ff02::fb`) reaching potentially different listeners,
1153    /// so sending a record on one group must not throttle it on the other.
1154    pub(crate) last_multicast_v4: HashMap<String, u64>,
1155
1156    /// Same as [`Self::last_multicast_v4`] but for this interface's IPv6 group.
1157    pub(crate) last_multicast_v6: HashMap<String, u64>,
1158}
1159
1160impl DnsRegistry {
1161    pub(crate) fn new() -> Self {
1162        Self {
1163            probing: HashMap::new(),
1164            active: HashMap::new(),
1165            new_timers: Vec::new(),
1166            name_changes: HashMap::new(),
1167            last_multicast_v4: HashMap::new(),
1168            last_multicast_v6: HashMap::new(),
1169        }
1170    }
1171
1172    /// Enforces the RFC 6762 section 6 multicast rate limit on `out`.
1173    ///
1174    /// A responder MUST NOT multicast a record on a given interface until at
1175    /// least one second has elapsed since the last time that record was
1176    /// multicast on that particular interface.
1177    ///
1178    /// `is_ipv4` selects the per-family bucket: the IPv4 and IPv6 groups on one
1179    /// interface are throttled independently (see [`Self::last_multicast_v4`]).
1180    ///
1181    /// Drops from `out` any answer or additional record that was multicast within the
1182    /// last second, and records `now` as the last-multicast time for the records kept.
1183    ///
1184    /// This must NOT be applied to probe queries, legacy unicast responses, or
1185    /// goodbye packets, which are exempt from the rate limit.
1186    pub(crate) fn apply_multicast_rate_limit(
1187        &mut self,
1188        out: &mut DnsOutgoing,
1189        now: u64,
1190        is_ipv4: bool,
1191    ) {
1192        let last_multicast = if is_ipv4 {
1193            &mut self.last_multicast_v4
1194        } else {
1195            &mut self.last_multicast_v6
1196        };
1197
1198        // Prune stale entries so the map stays bounded across name changes;
1199        // any record older than the one-second window is irrelevant now.
1200        last_multicast.retain(|_, last| now.saturating_sub(*last) < MULTICAST_RATE_LIMIT_MILLIS);
1201
1202        out.retain_answers(|record| keep_after_rate_limit(last_multicast, record, now));
1203
1204        // Only touch additionals if an answer survived.
1205        if out.answers_count() > 0 {
1206            out.retain_additionals(|record| keep_after_rate_limit(last_multicast, record, now));
1207        }
1208    }
1209
1210    /// Returns the renamed name if a name change exists, otherwise returns the original name.
1211    pub(crate) fn resolve_name<'a>(&'a self, name: &'a str) -> &'a str {
1212        match self.name_changes.get(name) {
1213            Some(new_name) => new_name,
1214            None => name,
1215        }
1216    }
1217
1218    pub(crate) fn is_probing_done<T>(
1219        &mut self,
1220        answer: &T,
1221        service_name: &str,
1222        start_time: u64,
1223    ) -> bool
1224    where
1225        T: DnsRecordExt + Send + 'static,
1226    {
1227        if let Some(active_records) = self.active.get(answer.get_name()) {
1228            for record in active_records.iter() {
1229                if answer.matches(record.as_ref()) {
1230                    debug!(
1231                        "found active record {} {}",
1232                        answer.get_type(),
1233                        answer.get_name(),
1234                    );
1235                    return true;
1236                }
1237            }
1238        }
1239
1240        let probe = self
1241            .probing
1242            .entry(answer.get_name().to_string())
1243            .or_insert_with(|| {
1244                debug!("new probe of {}", answer.get_name());
1245                Probe::new(start_time)
1246            });
1247
1248        self.new_timers.push(probe.next_send);
1249
1250        for record in probe.records.iter() {
1251            if answer.matches(record.as_ref()) {
1252                debug!(
1253                    "found existing record {} in probe of '{}'",
1254                    answer.get_type(),
1255                    answer.get_name(),
1256                );
1257                probe.waiting_services.insert(service_name.to_string());
1258                return false; // Found existing probe for the same record.
1259            }
1260        }
1261
1262        debug!(
1263            "insert record {} into probe of {}",
1264            answer.get_type(),
1265            answer.get_name(),
1266        );
1267        probe.insert_record(answer.clone_box());
1268        probe.waiting_services.insert(service_name.to_string());
1269
1270        false
1271    }
1272
1273    /// check all records in "probing" and "active":
1274    /// if the record is SRV, and hostname is set to original, remove it.
1275    /// and create a new SRV with "host" set to "new_name" and put into "probing".
1276    pub(crate) fn update_hostname(
1277        &mut self,
1278        original: &str,
1279        new_name: &str,
1280        probe_time: u64,
1281    ) -> bool {
1282        let mut found_records = Vec::new();
1283        let mut new_timer_added = false;
1284
1285        for (_name, probe) in self.probing.iter_mut() {
1286            probe.records.retain(|record| {
1287                if record.get_type() == RRType::SRV {
1288                    if let Some(srv) = record.any().downcast_ref::<DnsSrv>() {
1289                        if srv.host() == original {
1290                            let mut new_record = srv.clone();
1291                            new_record.set_host(new_name.to_string());
1292                            found_records.push(new_record);
1293                            return false;
1294                        }
1295                    }
1296                }
1297                true
1298            });
1299        }
1300
1301        for (_name, records) in self.active.iter_mut() {
1302            records.retain(|record| {
1303                if record.get_type() == RRType::SRV {
1304                    if let Some(srv) = record.any().downcast_ref::<DnsSrv>() {
1305                        if srv.host() == original {
1306                            let mut new_record = srv.clone();
1307                            new_record.set_host(new_name.to_string());
1308                            found_records.push(new_record);
1309                            return false;
1310                        }
1311                    }
1312                }
1313                true
1314            });
1315        }
1316
1317        for record in found_records {
1318            let probe = match self.probing.get_mut(record.get_name()) {
1319                Some(p) => {
1320                    p.start_time = probe_time; // restart this probe.
1321                    p
1322                }
1323                None => {
1324                    let new_probe = self
1325                        .probing
1326                        .entry(record.get_name().to_string())
1327                        .or_insert_with(|| Probe::new(probe_time));
1328                    new_timer_added = true;
1329                    new_probe
1330                }
1331            };
1332
1333            debug!(
1334                "insert record {} with new hostname {new_name} into probe for: {}",
1335                record.get_type(),
1336                record.get_name()
1337            );
1338            probe.insert_record(record.boxed());
1339        }
1340
1341        new_timer_added
1342    }
1343}
1344
1345/// RFC 6762 section 6 per-record, per-interface multicast rate-limit window:
1346/// a record must not be re-multicast until at least this many millis have
1347/// elapsed since it was last multicast on that interface.
1348pub(crate) const MULTICAST_RATE_LIMIT_MILLIS: u64 = 1000;
1349
1350/// Returns whether `record` may still be multicast under the RFC 6762 section 6
1351/// rate limit, updating `last_multicast` to `now` when it is kept.
1352fn keep_after_rate_limit(
1353    last_multicast: &mut HashMap<String, u64>,
1354    record: &DnsRecordBox,
1355    now: u64,
1356) -> bool {
1357    let key = rate_limit_key(record);
1358    match last_multicast.get(&key) {
1359        Some(last) if now.saturating_sub(*last) < MULTICAST_RATE_LIMIT_MILLIS => false,
1360        _ => {
1361            last_multicast.insert(key, now);
1362            true
1363        }
1364    }
1365}
1366
1367/// Builds the identity key for a record used by the RFC 6762 section 6
1368/// multicast rate limit: name (case-insensitive) + type + rdata. TTL and the
1369/// cache-flush bit are intentionally excluded, so the same logical record maps
1370/// to a single key regardless of the TTL it is sent with.
1371fn rate_limit_key(record: &DnsRecordBox) -> String {
1372    format!(
1373        "{}-{}-{}",
1374        record.get_name().to_lowercase(),
1375        record.get_type(),
1376        record.rdata_print(),
1377    )
1378}
1379
1380/// Returns a tuple of (service_type_domain, optional_sub_domain)
1381pub(crate) fn split_sub_domain(domain: &str) -> (&str, Option<&str>) {
1382    if let Some((_, ty_domain)) = domain.rsplit_once("._sub.") {
1383        (ty_domain, Some(domain))
1384    } else {
1385        (domain, None)
1386    }
1387}
1388
1389/// Returns true if `addr` is a unicast link-local IPv6 address.
1390/// Replicates the logic from `std::net::Ipv6Addr::is_unicast_link_local()`, which is not
1391/// stable on the current mdns-sd Rust version (1.71.0).
1392///
1393/// https://github.com/rust-lang/rust/blob/9fc6b43126469e3858e2fe86cafb4f0fd5068869/library/core/src/net/ip_addr.rs#L1684
1394pub(crate) fn is_unicast_link_local(addr: &Ipv6Addr) -> bool {
1395    (addr.segments()[0] & 0xffc0) == 0xfe80
1396}
1397
1398/// Represents a resolved service as a plain data struct.
1399/// This is from a client (i.e. querier) point of view.
1400#[derive(Clone, Debug)]
1401#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
1402#[non_exhaustive]
1403pub struct ResolvedService {
1404    /// Service type and domain. For example, "_http._tcp.local."
1405    pub ty_domain: String,
1406
1407    /// Optional service subtype and domain.
1408    ///
1409    /// See RFC6763 section 7.1 about "Subtypes":
1410    /// <https://datatracker.ietf.org/doc/html/rfc6763#section-7.1>
1411    /// For example, "_printer._sub._http._tcp.local."
1412    pub sub_ty_domain: Option<String>,
1413
1414    /// Full name of the service. For example, "my-service._http._tcp.local."
1415    pub fullname: String,
1416
1417    /// Host name of the service. For example, "my-server1.local."
1418    pub host: String,
1419
1420    /// Port of the service. I.e. TCP or UDP port.
1421    pub port: u16,
1422
1423    /// Addresses of the service. IPv4 or IPv6 addresses.
1424    pub addresses: HashSet<ScopedIp>,
1425
1426    /// Properties of the service, decoded from TXT record.
1427    pub txt_properties: TxtProperties,
1428}
1429
1430impl ResolvedService {
1431    /// Returns true if the service data is valid, i.e. ready to be used.
1432    pub fn is_valid(&self) -> bool {
1433        let some_missing = self.ty_domain.is_empty()
1434            || self.fullname.is_empty()
1435            || self.host.is_empty()
1436            || self.addresses.is_empty();
1437        !some_missing
1438    }
1439
1440    #[inline]
1441    pub const fn get_subtype(&self) -> &Option<String> {
1442        &self.sub_ty_domain
1443    }
1444
1445    #[inline]
1446    pub fn get_fullname(&self) -> &str {
1447        &self.fullname
1448    }
1449
1450    #[inline]
1451    pub fn get_hostname(&self) -> &str {
1452        &self.host
1453    }
1454
1455    #[inline]
1456    pub fn get_port(&self) -> u16 {
1457        self.port
1458    }
1459
1460    #[inline]
1461    pub fn get_addresses(&self) -> &HashSet<ScopedIp> {
1462        &self.addresses
1463    }
1464
1465    pub fn get_addresses_v4(&self) -> HashSet<Ipv4Addr> {
1466        self.addresses
1467            .iter()
1468            .filter_map(|ip| match ip {
1469                ScopedIp::V4(ipv4) => Some(*ipv4.addr()),
1470                _ => None,
1471            })
1472            .collect()
1473    }
1474
1475    #[inline]
1476    pub fn get_properties(&self) -> &TxtProperties {
1477        &self.txt_properties
1478    }
1479
1480    #[inline]
1481    pub fn get_property(&self, key: &str) -> Option<&TxtProperty> {
1482        self.txt_properties.get(key)
1483    }
1484
1485    pub fn get_property_val(&self, key: &str) -> Option<Option<&[u8]>> {
1486        self.txt_properties.get_property_val(key)
1487    }
1488
1489    pub fn get_property_val_str(&self, key: &str) -> Option<&str> {
1490        self.txt_properties.get_property_val_str(key)
1491    }
1492}
1493
1494#[cfg(test)]
1495mod tests {
1496    use super::{decode_txt, encode_txt, u8_slice_to_hex, DnsRegistry, ServiceInfo, TxtProperty};
1497    use crate::dns_parser::{DnsOutgoing, DnsPointer, RRType, CLASS_IN, FLAGS_QR_RESPONSE};
1498    use crate::{IfKind, IfPredicate};
1499    use if_addrs::{IfAddr, IfOperStatus, Ifv4Addr, Ifv6Addr, Interface};
1500    use std::net::{Ipv4Addr, Ipv6Addr};
1501
1502    /// RFC 6762 section 6: the same record must not be multicast on an
1503    /// interface more than once per second, but is allowed again after a
1504    /// second has elapsed.
1505    #[test]
1506    fn test_multicast_rate_limit() {
1507        let mut registry = DnsRegistry::new();
1508
1509        let build_out = || {
1510            let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE);
1511            out.add_answer_at_time(
1512                DnsPointer::new(
1513                    "_test._tcp.local.",
1514                    RRType::PTR,
1515                    CLASS_IN,
1516                    4500,
1517                    "inst._test._tcp.local.".to_string(),
1518                ),
1519                0,
1520            );
1521            out
1522        };
1523
1524        let now = 1_000_000;
1525
1526        // First multicast at `now`: the record passes through.
1527        let mut out = build_out();
1528        registry.apply_multicast_rate_limit(&mut out, now, true);
1529        assert_eq!(out.answers_count(), 1);
1530
1531        // Again 500ms later: the record is throttled (dropped).
1532        let mut out = build_out();
1533        registry.apply_multicast_rate_limit(&mut out, now + 500, true);
1534        assert_eq!(out.answers_count(), 0);
1535
1536        // Exactly 1 second after the first send: allowed again.
1537        let mut out = build_out();
1538        registry.apply_multicast_rate_limit(&mut out, now + 1000, true);
1539        assert_eq!(out.answers_count(), 1);
1540    }
1541
1542    /// A single interface carries both IPv4 and IPv6, but they are distinct
1543    /// multicast groups reaching different listeners, so the one-second limit
1544    /// is tracked per family: multicasting a record on IPv4 must NOT throttle
1545    /// the same record on IPv6 (and vice versa). Otherwise the shared PTR/SRV/
1546    /// TXT records would be stripped from whichever family is sent second.
1547    #[test]
1548    fn test_multicast_rate_limit_per_family() {
1549        let mut registry = DnsRegistry::new();
1550
1551        let build_out = || {
1552            let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE);
1553            out.add_answer_at_time(
1554                DnsPointer::new(
1555                    "_test._tcp.local.",
1556                    RRType::PTR,
1557                    CLASS_IN,
1558                    4500,
1559                    "inst._test._tcp.local.".to_string(),
1560                ),
1561                0,
1562            );
1563            out
1564        };
1565
1566        let now = 1_000_000;
1567
1568        // Multicast the record on IPv4: passes through.
1569        let mut out = build_out();
1570        registry.apply_multicast_rate_limit(&mut out, now, true);
1571        assert_eq!(out.answers_count(), 1);
1572
1573        // The same record on IPv6 immediately after: must still pass, because
1574        // the IPv6 group has its own bucket.
1575        let mut out = build_out();
1576        registry.apply_multicast_rate_limit(&mut out, now, false);
1577        assert_eq!(out.answers_count(), 1);
1578
1579        // A second IPv4 send within the window is still throttled, confirming
1580        // the IPv6 send did not reset (or get charged to) the IPv4 bucket.
1581        let mut out = build_out();
1582        registry.apply_multicast_rate_limit(&mut out, now + 500, true);
1583        assert_eq!(out.answers_count(), 0);
1584
1585        // Likewise a second IPv6 send within the window is throttled.
1586        let mut out = build_out();
1587        registry.apply_multicast_rate_limit(&mut out, now + 500, false);
1588        assert_eq!(out.answers_count(), 0);
1589    }
1590
1591    /// When every answer is throttled the packet is not sent, so any surviving
1592    /// additional record must NOT be stamped as multicast — otherwise a later
1593    /// answer for that same record would be wrongly throttled even though it was
1594    /// never put on the wire.
1595    #[test]
1596    fn test_multicast_rate_limit_additionals_not_stamped_without_answer() {
1597        let mut registry = DnsRegistry::new();
1598
1599        let ptr_answer = || {
1600            DnsPointer::new(
1601                "_test._tcp.local.",
1602                RRType::PTR,
1603                CLASS_IN,
1604                4500,
1605                "inst._test._tcp.local.".to_string(),
1606            )
1607        };
1608        let extra = || {
1609            DnsPointer::new(
1610                "_other._tcp.local.",
1611                RRType::PTR,
1612                CLASS_IN,
1613                4500,
1614                "inst._other._tcp.local.".to_string(),
1615            )
1616        };
1617
1618        let now = 1_000_000;
1619
1620        // Send the PTR answer once so it is throttled going forward.
1621        let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE);
1622        out.add_answer_at_time(ptr_answer(), 0);
1623        registry.apply_multicast_rate_limit(&mut out, now, true);
1624        assert_eq!(out.answers_count(), 1);
1625
1626        // 100ms later: PTR answer is throttled, and `extra` rides along as an
1627        // additional. With no answer surviving, nothing is sent.
1628        let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE);
1629        out.add_answer_at_time(ptr_answer(), 0);
1630        out.add_additional_answer(extra());
1631        registry.apply_multicast_rate_limit(&mut out, now + 100, true);
1632        assert_eq!(out.answers_count(), 0);
1633
1634        // 200ms later: `extra` is now requested as a real answer. It must pass,
1635        // because it was never actually multicast above (only carried as an
1636        // unsent additional), so the 1-second limit does not apply to it.
1637        let mut out = DnsOutgoing::new(FLAGS_QR_RESPONSE);
1638        out.add_answer_at_time(extra(), 0);
1639        registry.apply_multicast_rate_limit(&mut out, now + 200, true);
1640        assert_eq!(out.answers_count(), 1);
1641    }
1642
1643    #[test]
1644    fn test_txt_encode_decode() {
1645        let properties = [
1646            TxtProperty::from(&("key1", "value1")),
1647            TxtProperty::from(&("key2", "value2")),
1648        ];
1649
1650        // test encode
1651        let property_count = properties.len();
1652        let encoded = encode_txt(properties.iter());
1653        assert_eq!(
1654            encoded.len(),
1655            "key1=value1".len() + "key2=value2".len() + property_count
1656        );
1657        assert_eq!(encoded[0] as usize, "key1=value1".len());
1658
1659        // test decode
1660        let decoded = decode_txt(&encoded);
1661        assert!(properties[..] == decoded[..]);
1662
1663        // test empty value
1664        let properties = vec![TxtProperty::from(&("key3", ""))];
1665        let property_count = properties.len();
1666        let encoded = encode_txt(properties.iter());
1667        assert_eq!(encoded.len(), "key3=".len() + property_count);
1668
1669        let decoded = decode_txt(&encoded);
1670        assert_eq!(properties, decoded);
1671
1672        // test non-string value
1673        let binary_val: Vec<u8> = vec![123, 234, 0];
1674        let binary_len = binary_val.len();
1675        let properties = vec![TxtProperty::from(("key4", binary_val))];
1676        let property_count = properties.len();
1677        let encoded = encode_txt(properties.iter());
1678        assert_eq!(encoded.len(), "key4=".len() + binary_len + property_count);
1679
1680        let decoded = decode_txt(&encoded);
1681        assert_eq!(properties, decoded);
1682
1683        // test value that contains '='
1684        let properties = vec![TxtProperty::from(("key5", "val=5"))];
1685        let property_count = properties.len();
1686        let encoded = encode_txt(properties.iter());
1687        assert_eq!(
1688            encoded.len(),
1689            "key5=".len() + "val=5".len() + property_count
1690        );
1691
1692        let decoded = decode_txt(&encoded);
1693        assert_eq!(properties, decoded);
1694
1695        // test a property that has no value.
1696        let properties = vec![TxtProperty::from("key6")];
1697        let property_count = properties.len();
1698        let encoded = encode_txt(properties.iter());
1699        assert_eq!(encoded.len(), "key6".len() + property_count);
1700        let decoded = decode_txt(&encoded);
1701        assert_eq!(properties, decoded);
1702
1703        // test property at the 255-byte limit.
1704        let properties = [TxtProperty::from(
1705            String::from_utf8(vec![0x30; 255]).unwrap().as_str(),
1706        )];
1707        let property_count = properties.len();
1708        let encoded = encode_txt(properties.iter());
1709        // `property_count` is added because each property has a length byte.
1710        assert_eq!(encoded.len(), 255 + property_count);
1711        let decoded = decode_txt(&encoded);
1712        assert_eq!(properties.to_vec(), decoded);
1713    }
1714
1715    #[test]
1716    fn test_txt_property_exceeds_255_bytes() {
1717        let long_key = String::from_utf8(vec![0x30; 256]).unwrap();
1718        let result = ServiceInfo::new(
1719            "_test._tcp.local.",
1720            "test",
1721            "host",
1722            "",
1723            1234,
1724            &[(long_key.as_str(), "")][..],
1725        );
1726        assert!(result.is_err());
1727        assert!(result
1728            .unwrap_err()
1729            .to_string()
1730            .contains("exceeding the 255-byte limit"));
1731
1732        // A property exactly at 255 bytes should succeed.
1733        // key (250 bytes) + "=" (1 byte) + value (4 bytes) = 255 bytes.
1734        let key_at_limit = String::from_utf8(vec![0x30; 250]).unwrap();
1735        let result = ServiceInfo::new(
1736            "_test._tcp.local.",
1737            "test",
1738            "host",
1739            "",
1740            1234,
1741            &[(key_at_limit.as_str(), "abcd")][..],
1742        );
1743        assert!(result.is_ok());
1744    }
1745
1746    #[test]
1747    fn test_set_properties_from_txt() {
1748        // Three duplicated keys.
1749        let properties = [
1750            TxtProperty::from(&("one", "1")),
1751            TxtProperty::from(&("ONE", "2")),
1752            TxtProperty::from(&("One", "3")),
1753        ];
1754        let encoded = encode_txt(properties.iter());
1755
1756        // Simple decode does not remove duplicated keys.
1757        let decoded = decode_txt(&encoded);
1758        assert_eq!(decoded.len(), 3);
1759
1760        // ServiceInfo removes duplicated keys and keeps only the first one.
1761        let mut service_info =
1762            ServiceInfo::new("_test._tcp", "prop_test", "localhost", "", 1234, None).unwrap();
1763        service_info._set_properties_from_txt(&encoded);
1764        assert_eq!(service_info.get_properties().len(), 1);
1765
1766        // Verify the only one property.
1767        let prop = service_info.get_properties().iter().next().unwrap();
1768        assert_eq!(prop.key, "one");
1769        assert_eq!(prop.val_str(), "1");
1770    }
1771
1772    #[test]
1773    fn test_u8_slice_to_hex() {
1774        let bytes = [0x01u8, 0x02u8, 0x03u8];
1775        let hex = u8_slice_to_hex(&bytes);
1776        assert_eq!(hex.as_str(), "0x010203");
1777
1778        let slice = "abcdefghijklmnopqrstuvwxyz";
1779        let hex = u8_slice_to_hex(slice.as_bytes());
1780        assert_eq!(hex.len(), slice.len() * 2 + 2);
1781        assert_eq!(
1782            hex.as_str(),
1783            "0x6162636465666768696a6b6c6d6e6f707172737475767778797a"
1784        );
1785    }
1786
1787    #[test]
1788    fn test_txt_property_debug() {
1789        // Test UTF-8 property value.
1790        let prop_1 = TxtProperty {
1791            key: "key1".to_string(),
1792            val: Some("val1".to_string().into()),
1793        };
1794        let prop_1_debug = format!("{:?}", &prop_1);
1795        assert_eq!(
1796            prop_1_debug,
1797            "TxtProperty {key: \"key1\", val: Some(\"val1\")}"
1798        );
1799
1800        // Test non-UTF-8 property value.
1801        let prop_2 = TxtProperty {
1802            key: "key2".to_string(),
1803            val: Some(vec![150u8, 151u8, 152u8]),
1804        };
1805        let prop_2_debug = format!("{:?}", &prop_2);
1806        assert_eq!(
1807            prop_2_debug,
1808            "TxtProperty {key: \"key2\", val: Some(0x969798)}"
1809        );
1810    }
1811
1812    #[test]
1813    fn test_txt_decode_property_size_out_of_bounds() {
1814        // Construct a TXT record with an invalid property length that would be out of bounds.
1815        let encoded: Vec<u8> = vec![
1816            0x0b, // Length 11
1817            b'k', b'e', b'y', b'1', b'=', b'v', b'a', b'l', b'u', b'e',
1818            b'1', // key1=value1 (Length 11)
1819            0x10, // Length 16 (Would be out of bounds)
1820            b'k', b'e', b'y', b'2', b'=', b'v', b'a', b'l', b'u', b'e',
1821            b'2', // key2=value2 (Length 11)
1822        ];
1823        // Decode the record content
1824        let decoded = decode_txt(&encoded);
1825        // We expect the out of bounds length for the second property to have caused the rest of the record content to be skipped.
1826        // Test that we only parsed the first property.
1827        assert_eq!(decoded.len(), 1);
1828        // Test that the key of the property we parsed is "key1"
1829        assert_eq!(decoded[0].key, "key1");
1830    }
1831
1832    #[test]
1833    fn test_is_address_supported() {
1834        let mut service_info =
1835            ServiceInfo::new("_test._tcp", "prop_test", "testhost", "", 1234, None).unwrap();
1836
1837        let intf_v6 = Interface {
1838            name: "foo".to_string(),
1839            index: Some(1),
1840            addr: IfAddr::V6(Ifv6Addr {
1841                ip: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0x1234, 0, 0, 1),
1842                netmask: Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0),
1843                broadcast: None,
1844                prefixlen: 16,
1845            }),
1846            oper_status: IfOperStatus::Up,
1847            is_p2p: false,
1848            #[cfg(windows)]
1849            adapter_name: String::new(),
1850        };
1851
1852        let intf_v4 = Interface {
1853            name: "bar".to_string(),
1854            index: Some(1),
1855            addr: IfAddr::V4(Ifv4Addr {
1856                ip: Ipv4Addr::new(192, 1, 2, 3),
1857                netmask: Ipv4Addr::new(255, 255, 0, 0),
1858                broadcast: None,
1859                prefixlen: 16,
1860            }),
1861            oper_status: IfOperStatus::Up,
1862            is_p2p: false,
1863            #[cfg(windows)]
1864            adapter_name: String::new(),
1865        };
1866
1867        let intf_baz = Interface {
1868            name: "baz".to_string(),
1869            index: Some(1),
1870            addr: IfAddr::V6(Ifv6Addr {
1871                ip: Ipv6Addr::new(0x2003, 0xdb8, 0, 0, 0x1234, 0, 0, 1),
1872                netmask: Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0),
1873                broadcast: None,
1874                prefixlen: 16,
1875            }),
1876            oper_status: IfOperStatus::Up,
1877            is_p2p: false,
1878            #[cfg(windows)]
1879            adapter_name: String::new(),
1880        };
1881
1882        let intf_loopback_v4 = Interface {
1883            name: "foo".to_string(),
1884            index: Some(1),
1885            addr: IfAddr::V4(Ifv4Addr {
1886                ip: Ipv4Addr::new(127, 0, 0, 1),
1887                netmask: Ipv4Addr::new(255, 255, 255, 255),
1888                broadcast: None,
1889                prefixlen: 16,
1890            }),
1891            oper_status: IfOperStatus::Up,
1892            is_p2p: false,
1893            #[cfg(windows)]
1894            adapter_name: String::new(),
1895        };
1896
1897        let intf_loopback_v6 = Interface {
1898            name: "foo".to_string(),
1899            index: Some(1),
1900            addr: IfAddr::V6(Ifv6Addr {
1901                ip: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1),
1902                netmask: Ipv6Addr::new(
1903                    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
1904                ),
1905                broadcast: None,
1906                prefixlen: 16,
1907            }),
1908            oper_status: IfOperStatus::Up,
1909            is_p2p: false,
1910            #[cfg(windows)]
1911            adapter_name: String::new(),
1912        };
1913
1914        let intf_link_local_v4 = Interface {
1915            name: "foo".to_string(),
1916            index: Some(1),
1917            addr: IfAddr::V4(Ifv4Addr {
1918                ip: Ipv4Addr::new(169, 254, 0, 1),
1919                netmask: Ipv4Addr::new(255, 255, 0, 0),
1920                broadcast: None,
1921                prefixlen: 16,
1922            }),
1923            oper_status: IfOperStatus::Up,
1924            is_p2p: false,
1925            #[cfg(windows)]
1926            adapter_name: String::new(),
1927        };
1928
1929        let intf_link_local_v6 = Interface {
1930            name: "foo".to_string(),
1931            index: Some(1),
1932            addr: IfAddr::V6(Ifv6Addr {
1933                ip: Ipv6Addr::new(0xfe80, 0, 0, 0, 0x1234, 0, 0, 1),
1934                netmask: Ipv6Addr::new(0xffff, 0xffff, 0xffff, 0xffff, 0, 0, 0, 0),
1935                broadcast: None,
1936                prefixlen: 16,
1937            }),
1938            oper_status: IfOperStatus::Up,
1939            is_p2p: false,
1940            #[cfg(windows)]
1941            adapter_name: String::new(),
1942        };
1943
1944        // supported addresses not specified
1945        assert!(service_info.is_address_supported(&intf_v6));
1946
1947        // Interface not supported
1948        service_info.set_interfaces(vec![
1949            IfKind::Name("foo".to_string()),
1950            IfKind::Name("bar".to_string()),
1951        ]);
1952        assert!(!service_info.is_address_supported(&intf_baz));
1953
1954        // link-local only
1955        service_info.set_link_local_only(true);
1956        assert!(!service_info.is_address_supported(&intf_v4));
1957        assert!(!service_info.is_address_supported(&intf_v6));
1958        assert!(service_info.is_address_supported(&intf_link_local_v4));
1959        assert!(service_info.is_address_supported(&intf_link_local_v6));
1960        service_info.set_link_local_only(false);
1961
1962        // supported interfaces: IfKing::All
1963        service_info.set_interfaces(vec![IfKind::All]);
1964        assert!(service_info.is_address_supported(&intf_v6));
1965        assert!(service_info.is_address_supported(&intf_v4));
1966
1967        // supported interfaces: IfKind::IPv6
1968        service_info.set_interfaces(vec![IfKind::IPv6]);
1969        assert!(service_info.is_address_supported(&intf_v6));
1970        assert!(!service_info.is_address_supported(&intf_v4));
1971
1972        // supported interfaces: IfKind::IPv4
1973        service_info.set_interfaces(vec![IfKind::IPv4]);
1974        assert!(service_info.is_address_supported(&intf_v4));
1975        assert!(!service_info.is_address_supported(&intf_v6));
1976
1977        // supported interfaces: IfKind::Addr
1978        service_info.set_interfaces(vec![IfKind::Addr(intf_v6.ip())]);
1979        assert!(service_info.is_address_supported(&intf_v6));
1980        assert!(!service_info.is_address_supported(&intf_v4));
1981
1982        // supported interfaces: IfKind::LoopbackV4
1983        service_info.set_interfaces(vec![IfKind::LoopbackV4]);
1984        assert!(service_info.is_address_supported(&intf_loopback_v4));
1985        assert!(!service_info.is_address_supported(&intf_loopback_v6));
1986
1987        // supported interfaces: IfKind::LoopbackV6
1988        service_info.set_interfaces(vec![IfKind::LoopbackV6]);
1989        assert!(!service_info.is_address_supported(&intf_loopback_v4));
1990        assert!(service_info.is_address_supported(&intf_loopback_v6));
1991
1992        // supported interfaces: IPv4 and name = "foo"
1993        service_info.set_interfaces(vec![IfKind::Predicate(IfPredicate::new(|intf| {
1994            intf.ip().is_ipv4() && intf.name == "foo"
1995        }))]);
1996        assert!(service_info.is_address_supported(&intf_loopback_v4));
1997        assert!(!service_info.is_address_supported(&intf_v4));
1998        assert!(!service_info.is_address_supported(&intf_loopback_v6));
1999    }
2000
2001    #[test]
2002    fn test_scoped_ip_set_detects_interface_id_change() {
2003        use crate::{InterfaceId, ScopedIp, ScopedIpV4};
2004        use std::collections::HashSet;
2005
2006        let intf1 = InterfaceId {
2007            name: "en0".to_string(),
2008            index: 1,
2009        };
2010        let intf2 = InterfaceId {
2011            name: "en1".to_string(),
2012            index: 2,
2013        };
2014        let addr = Ipv4Addr::new(192, 168, 1, 100);
2015
2016        let scoped_v4_one_intf = ScopedIpV4::new(addr, intf1);
2017        let mut scoped_v4_two_intfs = scoped_v4_one_intf.clone();
2018        scoped_v4_two_intfs.add_interface_id(intf2);
2019
2020        assert_ne!(scoped_v4_one_intf, scoped_v4_two_intfs);
2021
2022        let set_old: HashSet<ScopedIp> = HashSet::from([ScopedIp::V4(scoped_v4_one_intf)]);
2023        let set_new: HashSet<ScopedIp> = HashSet::from([ScopedIp::V4(scoped_v4_two_intfs)]);
2024
2025        assert_ne!(set_old, set_new);
2026    }
2027
2028    #[cfg(test)]
2029    #[cfg(feature = "serde")]
2030    mod serde {
2031        use super::{Ipv4Addr, Ipv6Addr};
2032        use crate::{ResolvedService, ScopedIp, TxtProperties};
2033
2034        use std::collections::HashSet;
2035        use std::net::IpAddr;
2036
2037        #[test]
2038        fn test_deserialize_serialize() -> Result<(), Box<dyn std::error::Error>> {
2039            let addresses = HashSet::from([
2040                ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
2041                ScopedIp::from(IpAddr::V6(Ipv6Addr::new(
2042                    0xfe80, 0x2001, 0x0db8, 0x85a3, 0x0000, 0x8a2e, 0x0370, 0x7334,
2043                ))),
2044            ]);
2045
2046            let service = ResolvedService {
2047                ty_domain: "_http._tcp.local.".to_owned(),
2048                sub_ty_domain: None,
2049                fullname: "example._http._tcp.local.".to_owned(),
2050                host: "example.local.".to_owned(),
2051                port: 1234,
2052                addresses,
2053                txt_properties: TxtProperties::new(),
2054            };
2055
2056            let json = serde_json::to_value(&service)?;
2057
2058            let parsed: ResolvedService = serde_json::from_value(json)?;
2059
2060            assert!(compare(&service, &parsed));
2061
2062            Ok(())
2063        }
2064
2065        fn compare(service: &ResolvedService, other: &ResolvedService) -> bool {
2066            service.ty_domain == other.ty_domain
2067                && service.sub_ty_domain == other.sub_ty_domain
2068                && service.fullname == other.fullname
2069                && service.host == other.host
2070                && service.port == other.port
2071                && service.addresses == other.addresses
2072                && service.txt_properties == other.txt_properties
2073        }
2074    }
2075}