1use 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}