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