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