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