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