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