Skip to main content

edge_mdns/
host.rs

1use core::net::{Ipv4Addr, Ipv6Addr};
2
3use crate::domain::base::{iana::Class, Record, Ttl};
4use crate::domain::rdata::{Aaaa, AllRecordData, Ptr, Srv, A};
5
6use crate::{HostAnswer, HostAnswers, MdnsError, NameSlice, RecordDataChain, Txt, DNS_SD_OWNER};
7
8/// A simple representation of a host that can be used to generate mDNS answers.
9///
10/// This structure implements the `HostAnswers` trait, which allows it to be used
11/// as a responder for mDNS queries coming from other network peers.
12#[derive(Debug, Clone)]
13#[cfg_attr(feature = "defmt", derive(defmt::Format))]
14pub struct Host<'a> {
15    /// The name of the host. I.e. a name "foo" will be pingable as "foo.local"
16    pub hostname: &'a str,
17    /// The IPv4 address of the host.
18    /// Leaving it as `Ipv4Addr::UNSPECIFIED` means that the host will not aswer it to A queries.
19    pub ipv4: Ipv4Addr,
20    /// The IPv6 address of the host.
21    /// Leaving it as `Ipv6Addr::UNSPECIFIED` means that the host will not aswer it to AAAA queries.
22    pub ipv6: Ipv6Addr,
23    /// The time-to-live of the mDNS answers.
24    #[cfg_attr(feature = "defmt", defmt(Debug2Format))]
25    pub ttl: Ttl,
26}
27
28impl Host<'_> {
29    fn visit_answers<F, E>(&self, mut f: F) -> Result<(), E>
30    where
31        F: FnMut(HostAnswer) -> Result<(), E>,
32        E: From<MdnsError>,
33    {
34        let owner = &[self.hostname, "local"];
35
36        if !self.ipv4.is_unspecified() {
37            f(Record::new(
38                NameSlice::new(owner),
39                Class::IN,
40                self.ttl,
41                RecordDataChain::Next(AllRecordData::A(A::new(domain::base::net::Ipv4Addr::from(
42                    self.ipv4.octets(),
43                )))),
44            ))?;
45        }
46
47        if !self.ipv6.is_unspecified() {
48            f(Record::new(
49                NameSlice::new(owner),
50                Class::IN,
51                self.ttl,
52                RecordDataChain::Next(AllRecordData::Aaaa(Aaaa::new(
53                    domain::base::net::Ipv6Addr::from(self.ipv6.octets()),
54                ))),
55            ))?;
56        }
57
58        Ok(())
59    }
60}
61
62impl HostAnswers for Host<'_> {
63    fn visit<F, E>(&self, mut f: F) -> Result<(), E>
64    where
65        F: FnMut(HostAnswer) -> Result<(), E>,
66        E: From<MdnsError>,
67    {
68        self.visit_answers(&mut f)
69    }
70}
71
72/// A simple representation of a DNS-SD service that can be used to generate mDNS answers.
73///
74/// This structure (indirectly - via the `ServiceAnswers` wraper which also provides the hostname)
75/// implements the `HostAnswers` trait, which allows it to be used as a responder for mDNS queries
76/// coming from other network peers.
77#[derive(Debug, Clone)]
78#[cfg_attr(feature = "defmt", derive(defmt::Format))]
79pub struct Service<'a> {
80    /// The name of the service. I.e. "printer"
81    pub name: &'a str,
82    /// The priority of the service.
83    pub priority: u16,
84    /// The weight of the service.
85    pub weight: u16,
86    /// The service type. I.e. "_http"
87    pub service: &'a str,
88    /// The protocol of the service. I.e. "_tcp" or "_udp"
89    pub protocol: &'a str,
90    /// The TCP/UDP port where the service listens for incoming requests.
91    pub port: u16,
92    /// The subtypes of the service, if any.
93    pub service_subtypes: &'a [&'a str],
94    /// The key-value pairs that will be included in the TXT record, as per the DNS-SD spec.
95    pub txt_kvs: &'a [(&'a str, &'a str)],
96}
97
98impl Service<'_> {
99    fn visit_answers<F, E>(&self, host: &Host, mut f: F) -> Result<(), E>
100    where
101        F: FnMut(HostAnswer) -> Result<(), E>,
102        E: From<MdnsError>,
103    {
104        host.visit_answers(&mut f)?;
105
106        let owner = &[self.name, self.service, self.protocol, "local"];
107        let stype = &[self.service, self.protocol, "local"];
108        let target = &[host.hostname, "local"];
109
110        f(Record::new(
111            NameSlice::new(owner),
112            Class::IN,
113            host.ttl,
114            RecordDataChain::Next(AllRecordData::Srv(Srv::new(
115                self.priority,
116                self.weight,
117                self.port,
118                NameSlice::new(target),
119            ))),
120        ))?;
121
122        f(Record::new(
123            NameSlice::new(owner),
124            Class::IN,
125            host.ttl,
126            RecordDataChain::This(Txt::new(self.txt_kvs)),
127        ))?;
128
129        f(Record::new(
130            DNS_SD_OWNER,
131            Class::IN,
132            host.ttl,
133            RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(stype)))),
134        ))?;
135
136        f(Record::new(
137            NameSlice::new(stype),
138            Class::IN,
139            host.ttl,
140            RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(owner)))),
141        ))?;
142
143        for subtype in self.service_subtypes {
144            let subtype_owner = &[subtype, self.name, self.service, self.protocol, "local"];
145            let subtype = &[subtype, "_sub", self.service, self.protocol, "local"];
146
147            f(Record::new(
148                NameSlice::new(subtype_owner),
149                Class::IN,
150                host.ttl,
151                RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(owner)))),
152            ))?;
153
154            f(Record::new(
155                NameSlice::new(subtype),
156                Class::IN,
157                host.ttl,
158                RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(subtype_owner)))),
159            ))?;
160
161            f(Record::new(
162                DNS_SD_OWNER,
163                Class::IN,
164                host.ttl,
165                RecordDataChain::Next(AllRecordData::Ptr(Ptr::new(NameSlice::new(subtype)))),
166            ))?;
167        }
168
169        Ok(())
170    }
171}
172
173/// A wrapper around a `Service` that also provides the Host of the service
174/// and thus allows the `HostAnswers` trait contract to be fullfilled for a `Service` instance.
175pub struct ServiceAnswers<'a> {
176    host: &'a Host<'a>,
177    service: &'a Service<'a>,
178}
179
180impl<'a> ServiceAnswers<'a> {
181    /// Create a new `ServiceAnswers` instance.
182    pub const fn new(host: &'a Host<'a>, service: &'a Service<'a>) -> Self {
183        Self { host, service }
184    }
185}
186
187impl HostAnswers for ServiceAnswers<'_> {
188    fn visit<F, E>(&self, mut f: F) -> Result<(), E>
189    where
190        F: FnMut(HostAnswer) -> Result<(), E>,
191        E: From<MdnsError>,
192    {
193        self.service.visit_answers(self.host, &mut f)
194    }
195}