bindy/bind9/records/
mod.rs1pub mod a;
10pub mod caa;
11pub mod cname;
12pub mod mx;
13pub mod ns;
14pub mod ptr;
15pub mod srv;
16pub mod txt;
17
18use anyhow::{Context, Result};
19use hickory_net::client::{Client, ClientHandle};
20use hickory_net::runtime::TokioRuntimeProvider;
21use hickory_net::udp::UdpClientStream;
22use hickory_proto::op::ResponseCode;
23use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType};
24use std::net::SocketAddr;
25use std::str::FromStr;
26use tracing::{info, warn};
27
28use crate::bind9::rndc::create_tsig_signer;
29use crate::bind9::types::RndcKeyData;
30use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
31
32const FALLBACK_DNS_RECORD_TTL_SECS: u32 = 300;
35
36pub(crate) fn effective_record_ttl(ttl: Option<i32>) -> u32 {
45 u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS)).unwrap_or_else(|_| {
46 u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(FALLBACK_DNS_RECORD_TTL_SECS)
47 })
48}
49
50pub(crate) fn rrset_ttl_matches(existing_records: &[Record], desired_ttl: u32) -> bool {
55 existing_records
56 .iter()
57 .all(|record| record.ttl == desired_ttl)
58}
59
60pub(crate) fn build_delete_rrset_record(fqdn: &Name, record_type: RecordType) -> Record {
65 Record::from_rdata(fqdn.clone(), 0, RData::Update0(record_type))
66}
67
68pub(crate) fn is_idempotent_delete_response_code(code: ResponseCode) -> bool {
75 matches!(
76 code,
77 ResponseCode::NoError | ResponseCode::NXDomain | ResponseCode::NXRRSet
78 )
79}
80
81async fn build_query_client(server_str: &str) -> Result<Client<TokioRuntimeProvider>> {
83 let server_addr: SocketAddr = server_str
84 .parse()
85 .with_context(|| format!("Invalid server address: {server_str}"))?;
86 let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
87 let (client, bg) = Client::<TokioRuntimeProvider>::from_sender(stream);
88 tokio::spawn(bg);
89 Ok(client)
90}
91
92pub(crate) async fn build_authenticated_client(
94 server_str: &str,
95 key_data: &RndcKeyData,
96) -> Result<Client<TokioRuntimeProvider>> {
97 let server_addr: SocketAddr = server_str
98 .parse()
99 .with_context(|| format!("Invalid server address: {server_str}"))?;
100 let signer = create_tsig_signer(key_data)?;
101 let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default())
102 .with_signer(Some(signer))
103 .build();
104 let (client, bg) = Client::<TokioRuntimeProvider>::from_sender(stream);
105 tokio::spawn(bg);
106 Ok(client)
107}
108
109pub(crate) fn build_record_fqdn(zone_name: &str, name: &str) -> Result<Name> {
113 if name == "@" || name.is_empty() {
114 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))
115 } else {
116 Name::from_str(&format!("{name}.{zone_name}"))
117 .with_context(|| format!("Invalid record name: {name}.{zone_name}"))
118 }
119}
120
121pub async fn query_dns_record(
141 zone_name: &str,
142 name: &str,
143 record_type: RecordType,
144 server: &str,
145) -> Result<Vec<Record>> {
146 let mut client = build_query_client(server).await?;
147 let fqdn = build_record_fqdn(zone_name, name)?;
148
149 let response = client
150 .query(fqdn.clone(), DNSClass::IN, record_type)
151 .await
152 .with_context(|| format!("Failed to query {record_type:?} record for {fqdn}"))?;
153
154 let records: Vec<Record> = response
155 .answers
156 .iter()
157 .filter(|r| r.record_type() == record_type)
158 .cloned()
159 .collect();
160
161 Ok(records)
162}
163
164pub async fn should_update_record<F>(
188 zone_name: &str,
189 name: &str,
190 record_type: RecordType,
191 record_type_name: &str,
192 server: &str,
193 compare_fn: F,
194) -> Result<bool>
195where
196 F: FnOnce(&[Record]) -> bool,
197{
198 match query_dns_record(zone_name, name, record_type, server).await {
199 Ok(existing_records) if !existing_records.is_empty() => {
200 if compare_fn(&existing_records) {
201 info!(
202 "{} record {} already exists with correct value - no changes needed",
203 record_type_name, name
204 );
205 Ok(false)
206 } else {
207 info!(
208 "{} record {} exists with different value(s), updating",
209 record_type_name, name
210 );
211 Ok(true)
212 }
213 }
214 Ok(_) => {
215 info!(
216 "{} record {} does not exist, creating",
217 record_type_name, name
218 );
219 Ok(true)
220 }
221 Err(e) => {
222 warn!(
223 "Failed to query existing {} record {} (will attempt update anyway): {}",
224 record_type_name, name, e
225 );
226 Ok(true)
227 }
228 }
229}
230
231pub async fn delete_dns_record(
255 zone_name: &str,
256 name: &str,
257 record_type: RecordType,
258 server: &str,
259 key_data: &RndcKeyData,
260) -> Result<()> {
261 let mut client = build_authenticated_client(server, key_data).await?;
262 let zone =
263 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))?;
264 let fqdn = build_record_fqdn(zone_name, name)?;
265
266 info!(
267 "Deleting {:?} record: {} from zone {}",
268 record_type, fqdn, zone_name
269 );
270
271 let dummy_record = build_delete_rrset_record(&fqdn, record_type);
273
274 let response = client
275 .delete_rrset(dummy_record, zone)
276 .await
277 .with_context(|| {
278 format!("Failed to send DNS UPDATE to delete {record_type:?} record {fqdn}")
279 })?;
280
281 let code = response.metadata.response_code;
282 if !is_idempotent_delete_response_code(code) {
283 return Err(anyhow::anyhow!(
284 "DNS DELETE for {record_type:?} record {fqdn} in zone {zone_name} \
285 rejected with response code: {code:?}"
286 ));
287 }
288
289 if code == ResponseCode::NoError {
290 info!(
291 "Successfully deleted {:?} record: {} from zone {}",
292 record_type, name, zone_name
293 );
294 return Ok(());
295 }
296
297 warn!(
299 "DNS DELETE for {:?} record {fqdn} returned code: {:?} (record did not exist)",
300 record_type, code
301 );
302 Ok(())
303}
304
305#[cfg(test)]
306mod mod_tests;