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