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