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