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