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