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