Skip to main content

dns_update/providers/
rfc2136.rs

1/*
2 * Copyright Stalwart Labs LLC See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12use crate::{
13    CAARecord, DnsRecord, DnsRecordType, Error, IntoFqdn, TlsaCertUsage, TlsaMatching, TlsaSelector,
14};
15use hickory_client::ClientError;
16use hickory_client::client::{Client, ClientHandle};
17use hickory_client::proto::ProtoError;
18use hickory_client::proto::dnssec::rdata::KEY;
19use hickory_client::proto::dnssec::rdata::tsig::TsigAlgorithm;
20use hickory_client::proto::dnssec::tsig::TSigner;
21use hickory_client::proto::dnssec::{Algorithm, DnsSecError, SigSigner, SigningKey};
22use hickory_client::proto::op::MessageFinalizer;
23use hickory_client::proto::op::ResponseCode;
24use hickory_client::proto::rr::rdata::caa::KeyValue;
25use hickory_client::proto::rr::rdata::tlsa::{CertUsage, Matching, Selector};
26use hickory_client::proto::rr::rdata::{A, AAAA, CAA, CNAME, MX, NS, SRV, TLSA, TXT};
27use hickory_client::proto::rr::{Name, RData, Record, RecordType};
28use hickory_client::proto::runtime::TokioRuntimeProvider;
29use hickory_client::proto::tcp::TcpClientStream;
30use hickory_client::proto::udp::UdpClientStream;
31use std::net::{AddrParseError, SocketAddr};
32use std::sync::Arc;
33
34#[derive(Clone)]
35pub struct Rfc2136Provider {
36    addr: DnsAddress,
37    signer: Arc<dyn MessageFinalizer>,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum DnsAddress {
42    Tcp(SocketAddr),
43    Udp(SocketAddr),
44}
45
46impl Rfc2136Provider {
47    pub(crate) fn new_tsig(
48        addr: impl TryInto<DnsAddress>,
49        key_name: impl AsRef<str>,
50        key: impl Into<Vec<u8>>,
51        algorithm: TsigAlgorithm,
52    ) -> crate::Result<Self> {
53        Ok(Rfc2136Provider {
54            addr: addr
55                .try_into()
56                .map_err(|_| Error::Parse("Invalid address".to_string()))?,
57            signer: Arc::new(TSigner::new(
58                key.into(),
59                algorithm,
60                Name::from_ascii(key_name.as_ref())?,
61                60,
62            )?),
63        })
64    }
65
66    /// SIG(0) support is slated for removal in v0.3.0.
67    pub(crate) fn new_sig0(
68        addr: impl TryInto<DnsAddress>,
69        signer_name: impl AsRef<str>,
70        key: Box<dyn SigningKey>,
71        public_key: impl Into<Vec<u8>>,
72        algorithm: Algorithm,
73    ) -> crate::Result<Self> {
74        let sig0key = KEY::new(
75            Default::default(),
76            Default::default(),
77            Default::default(),
78            Default::default(),
79            algorithm,
80            public_key.into(),
81        );
82
83        let signer = SigSigner::sig0(sig0key, key, Name::from_str_relaxed(signer_name.as_ref())?);
84
85        Ok(Rfc2136Provider {
86            addr: addr
87                .try_into()
88                .map_err(|_| Error::Parse("Invalid address".to_string()))?,
89            signer: Arc::new(signer),
90        })
91    }
92
93    async fn connect(&self) -> crate::Result<Client> {
94        match &self.addr {
95            DnsAddress::Udp(addr) => {
96                let stream = UdpClientStream::builder(*addr, TokioRuntimeProvider::new())
97                    .with_signer(Some(self.signer.clone()))
98                    .build();
99                let (client, bg) = Client::connect(stream).await?;
100                tokio::spawn(bg);
101                Ok(client)
102            }
103            DnsAddress::Tcp(addr) => {
104                let (stream, sender) =
105                    TcpClientStream::new(*addr, None, None, TokioRuntimeProvider::new());
106                let (client, bg) = Client::new(stream, sender, Some(self.signer.clone())).await?;
107                tokio::spawn(bg);
108                Ok(client)
109            }
110        }
111    }
112
113    pub(crate) async fn create(
114        &self,
115        name: impl IntoFqdn<'_>,
116        record: DnsRecord,
117        ttl: u32,
118        origin: impl IntoFqdn<'_>,
119    ) -> crate::Result<()> {
120        let (_rr_type, rdata) = convert_record(record)?;
121        let record = Record::from_rdata(
122            Name::from_str_relaxed(name.into_name().as_ref())?,
123            ttl,
124            rdata,
125        );
126
127        let mut client = self.connect().await?;
128        let result = client
129            .create(record, Name::from_str_relaxed(origin.into_fqdn().as_ref())?)
130            .await?;
131        if result.response_code() == ResponseCode::NoError {
132            Ok(())
133        } else {
134            Err(crate::Error::Response(result.response_code().to_string()))
135        }
136    }
137
138    pub(crate) async fn update(
139        &self,
140        name: impl IntoFqdn<'_>,
141        record: DnsRecord,
142        ttl: u32,
143        origin: impl IntoFqdn<'_>,
144    ) -> crate::Result<()> {
145        let (_rr_type, rdata) = convert_record(record)?;
146        let record = Record::from_rdata(
147            Name::from_str_relaxed(name.into_name().as_ref())?,
148            ttl,
149            rdata,
150        );
151
152        let mut client = self.connect().await?;
153        let result = client
154            .append(
155                record,
156                Name::from_str_relaxed(origin.into_fqdn().as_ref())?,
157                false,
158            )
159            .await?;
160        if result.response_code() == ResponseCode::NoError {
161            Ok(())
162        } else {
163            Err(crate::Error::Response(result.response_code().to_string()))
164        }
165    }
166
167    pub(crate) async fn delete(
168        &self,
169        name: impl IntoFqdn<'_>,
170        origin: impl IntoFqdn<'_>,
171        record_type: DnsRecordType,
172    ) -> crate::Result<()> {
173        let record = Record::update0(
174            Name::from_str_relaxed(name.into_name().as_ref())?,
175            0,
176            record_type.into(),
177        );
178
179        let mut client = self.connect().await?;
180        let result = client
181            .delete_rrset(record, Name::from_str_relaxed(origin.into_fqdn().as_ref())?)
182            .await?;
183        if result.response_code() == ResponseCode::NoError {
184            Ok(())
185        } else {
186            Err(crate::Error::Response(result.response_code().to_string()))
187        }
188    }
189}
190
191impl From<DnsRecordType> for RecordType {
192    fn from(record_type: DnsRecordType) -> Self {
193        match record_type {
194            DnsRecordType::A => RecordType::A,
195            DnsRecordType::AAAA => RecordType::AAAA,
196            DnsRecordType::CNAME => RecordType::CNAME,
197            DnsRecordType::NS => RecordType::NS,
198            DnsRecordType::MX => RecordType::MX,
199            DnsRecordType::TXT => RecordType::TXT,
200            DnsRecordType::SRV => RecordType::SRV,
201            DnsRecordType::TLSA => RecordType::TLSA,
202            DnsRecordType::CAA => RecordType::CAA,
203        }
204    }
205}
206
207fn convert_record(record: DnsRecord) -> crate::Result<(RecordType, RData)> {
208    Ok(match record {
209        DnsRecord::A(content) => (RecordType::A, RData::A(A::from(content))),
210        DnsRecord::AAAA(content) => (RecordType::AAAA, RData::AAAA(AAAA::from(content))),
211        DnsRecord::CNAME(content) => (
212            RecordType::CNAME,
213            RData::CNAME(CNAME(Name::from_str_relaxed(content)?)),
214        ),
215        DnsRecord::NS(content) => (
216            RecordType::NS,
217            RData::NS(NS(Name::from_str_relaxed(content)?)),
218        ),
219        DnsRecord::MX(content) => (
220            RecordType::MX,
221            RData::MX(MX::new(
222                content.priority,
223                Name::from_str_relaxed(content.exchange)?,
224            )),
225        ),
226        DnsRecord::TXT(content) => (RecordType::TXT, RData::TXT(TXT::new(vec![content]))),
227        DnsRecord::SRV(content) => (
228            RecordType::SRV,
229            RData::SRV(SRV::new(
230                content.priority,
231                content.weight,
232                content.port,
233                Name::from_str_relaxed(content.target)?,
234            )),
235        ),
236        DnsRecord::TLSA(content) => (
237            RecordType::TLSA,
238            RData::TLSA(TLSA::new(
239                content.cert_usage.into(),
240                content.selector.into(),
241                content.matching.into(),
242                content.cert_data,
243            )),
244        ),
245        DnsRecord::CAA(caa) => (
246            RecordType::CAA,
247            RData::CAA(match caa {
248                CAARecord::Issue {
249                    issuer_critical,
250                    name,
251                    options,
252                } => CAA::new_issue(
253                    issuer_critical,
254                    name.map(Name::from_str_relaxed).transpose()?,
255                    options
256                        .into_iter()
257                        .map(|kv| KeyValue::new(kv.key, kv.value))
258                        .collect(),
259                ),
260                CAARecord::IssueWild {
261                    issuer_critical,
262                    name,
263                    options,
264                } => CAA::new_issuewild(
265                    issuer_critical,
266                    name.map(Name::from_str_relaxed).transpose()?,
267                    options
268                        .into_iter()
269                        .map(|kv| KeyValue::new(kv.key, kv.value))
270                        .collect(),
271                ),
272                CAARecord::Iodef {
273                    issuer_critical,
274                    url,
275                } => CAA::new_iodef(
276                    issuer_critical,
277                    url.parse()
278                        .map_err(|_| Error::Parse("Invalid URL in CAA record".to_string()))?,
279                ),
280            }),
281        ),
282    })
283}
284
285impl From<TlsaCertUsage> for CertUsage {
286    fn from(usage: TlsaCertUsage) -> Self {
287        match usage {
288            TlsaCertUsage::PkixTa => CertUsage::PkixTa,
289            TlsaCertUsage::PkixEe => CertUsage::PkixEe,
290            TlsaCertUsage::DaneTa => CertUsage::DaneTa,
291            TlsaCertUsage::DaneEe => CertUsage::DaneEe,
292            TlsaCertUsage::Private => CertUsage::Private,
293        }
294    }
295}
296
297impl From<TlsaMatching> for Matching {
298    fn from(matching: TlsaMatching) -> Self {
299        match matching {
300            TlsaMatching::Raw => Matching::Raw,
301            TlsaMatching::Sha256 => Matching::Sha256,
302            TlsaMatching::Sha512 => Matching::Sha512,
303            TlsaMatching::Private => Matching::Private,
304        }
305    }
306}
307
308impl From<TlsaSelector> for Selector {
309    fn from(selector: TlsaSelector) -> Self {
310        match selector {
311            TlsaSelector::Full => Selector::Full,
312            TlsaSelector::Spki => Selector::Spki,
313            TlsaSelector::Private => Selector::Private,
314        }
315    }
316}
317
318impl TryFrom<&str> for DnsAddress {
319    type Error = ();
320
321    fn try_from(url: &str) -> Result<Self, Self::Error> {
322        let (host, is_tcp) = if let Some(host) = url.strip_prefix("udp://") {
323            (host, false)
324        } else if let Some(host) = url.strip_prefix("tcp://") {
325            (host, true)
326        } else {
327            (url, false)
328        };
329        let (host, port) = if let Some(host) = host.strip_prefix('[') {
330            let (host, maybe_port) = host.rsplit_once(']').ok_or(())?;
331
332            (
333                host,
334                maybe_port
335                    .rsplit_once(':')
336                    .map(|(_, port)| port)
337                    .unwrap_or("53"),
338            )
339        } else if let Some((host, port)) = host.rsplit_once(':') {
340            (host, port)
341        } else {
342            (host, "53")
343        };
344
345        let addr = SocketAddr::new(host.parse().map_err(|_| ())?, port.parse().map_err(|_| ())?);
346
347        if is_tcp {
348            Ok(DnsAddress::Tcp(addr))
349        } else {
350            Ok(DnsAddress::Udp(addr))
351        }
352    }
353}
354
355impl TryFrom<&String> for DnsAddress {
356    type Error = ();
357
358    fn try_from(url: &String) -> Result<Self, Self::Error> {
359        DnsAddress::try_from(url.as_str())
360    }
361}
362
363impl TryFrom<String> for DnsAddress {
364    type Error = ();
365
366    fn try_from(url: String) -> Result<Self, Self::Error> {
367        DnsAddress::try_from(url.as_str())
368    }
369}
370
371impl From<crate::TsigAlgorithm> for TsigAlgorithm {
372    fn from(alg: crate::TsigAlgorithm) -> Self {
373        match alg {
374            crate::TsigAlgorithm::HmacMd5 => TsigAlgorithm::HmacMd5,
375            crate::TsigAlgorithm::Gss => TsigAlgorithm::Gss,
376            crate::TsigAlgorithm::HmacSha1 => TsigAlgorithm::HmacSha1,
377            crate::TsigAlgorithm::HmacSha224 => TsigAlgorithm::HmacSha224,
378            crate::TsigAlgorithm::HmacSha256 => TsigAlgorithm::HmacSha256,
379            crate::TsigAlgorithm::HmacSha256_128 => TsigAlgorithm::HmacSha256_128,
380            crate::TsigAlgorithm::HmacSha384 => TsigAlgorithm::HmacSha384,
381            crate::TsigAlgorithm::HmacSha384_192 => TsigAlgorithm::HmacSha384_192,
382            crate::TsigAlgorithm::HmacSha512 => TsigAlgorithm::HmacSha512,
383            crate::TsigAlgorithm::HmacSha512_256 => TsigAlgorithm::HmacSha512_256,
384        }
385    }
386}
387
388impl From<crate::Algorithm> for Algorithm {
389    fn from(alg: crate::Algorithm) -> Self {
390        match alg {
391            crate::Algorithm::RSASHA256 => Algorithm::RSASHA256,
392            crate::Algorithm::RSASHA512 => Algorithm::RSASHA512,
393            crate::Algorithm::ECDSAP256SHA256 => Algorithm::ECDSAP256SHA256,
394            crate::Algorithm::ECDSAP384SHA384 => Algorithm::ECDSAP384SHA384,
395            crate::Algorithm::ED25519 => Algorithm::ED25519,
396        }
397    }
398}
399
400impl From<ProtoError> for Error {
401    fn from(e: ProtoError) -> Self {
402        Error::Protocol(e.to_string())
403    }
404}
405
406impl From<AddrParseError> for Error {
407    fn from(e: AddrParseError) -> Self {
408        Error::Parse(e.to_string())
409    }
410}
411
412impl From<ClientError> for Error {
413    fn from(e: ClientError) -> Self {
414        Error::Client(e.to_string())
415    }
416}
417
418impl From<DnsSecError> for Error {
419    fn from(e: DnsSecError) -> Self {
420        Error::Protocol(e.to_string())
421    }
422}