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_client::client::{Client, SyncClient};
19use hickory_client::rr::Name;
20use hickory_client::rr::{DNSClass, Record};
21use hickory_client::udp::UdpClientConnection;
22use std::str::FromStr;
23use tracing::{info, warn};
24
25pub async fn query_dns_record(
45 zone_name: &str,
46 name: &str,
47 record_type: hickory_client::rr::RecordType,
48 server: &str,
49) -> Result<Vec<Record>> {
50 let zone_name_str = zone_name.to_string();
51 let name_str = name.to_string();
52 let server_str = server.to_string();
53
54 tokio::task::spawn_blocking(move || {
55 let server_addr = server_str
57 .parse::<std::net::SocketAddr>()
58 .with_context(|| format!("Invalid server address: {server_str}"))?;
59
60 let conn = UdpClientConnection::new(server_addr)
62 .context("Failed to create UDP connection for query")?;
63 let client = SyncClient::new(conn);
64
65 let fqdn = if name_str == "@" || name_str.is_empty() {
67 Name::from_str(&zone_name_str)
68 .with_context(|| format!("Invalid zone name: {zone_name_str}"))?
69 } else {
70 Name::from_str(&format!("{name_str}.{zone_name_str}"))
71 .with_context(|| format!("Invalid record name: {name_str}.{zone_name_str}"))?
72 };
73
74 let response = client
76 .query(&fqdn, DNSClass::IN, record_type)
77 .with_context(|| format!("Failed to query {record_type:?} record for {fqdn}"))?;
78
79 let records: Vec<Record> = response
81 .answers()
82 .iter()
83 .filter(|r| r.record_type() == record_type)
84 .cloned()
85 .collect();
86
87 Ok(records)
88 })
89 .await
90 .context("DNS query task failed")?
91}
92
93pub async fn should_update_record<F>(
117 zone_name: &str,
118 name: &str,
119 record_type: hickory_client::rr::RecordType,
120 record_type_name: &str,
121 server: &str,
122 compare_fn: F,
123) -> Result<bool>
124where
125 F: FnOnce(&[Record]) -> bool,
126{
127 match query_dns_record(zone_name, name, record_type, server).await {
128 Ok(existing_records) if !existing_records.is_empty() => {
129 if compare_fn(&existing_records) {
131 info!(
132 "{} record {} already exists with correct value - no changes needed",
133 record_type_name, name
134 );
135 Ok(false) } else {
137 info!(
138 "{} record {} exists with different value(s), updating",
139 record_type_name, name
140 );
141 Ok(true) }
143 }
144 Ok(_) => {
145 info!(
147 "{} record {} does not exist, creating",
148 record_type_name, name
149 );
150 Ok(true) }
152 Err(e) => {
153 warn!(
155 "Failed to query existing {} record {} (will attempt update anyway): {}",
156 record_type_name, name, e
157 );
158 Ok(true) }
160 }
161}
162
163pub async fn delete_dns_record(
184 zone_name: &str,
185 name: &str,
186 record_type: hickory_client::rr::RecordType,
187 server: &str,
188 key_data: &crate::bind9::types::RndcKeyData,
189) -> Result<()> {
190 use crate::bind9::rndc::create_tsig_signer;
191 use hickory_client::op::ResponseCode;
192 use hickory_client::rr::DNSClass;
193
194 let zone_name_str = zone_name.to_string();
195 let name_str = name.to_string();
196 let server_str = server.to_string();
197 let key_data = key_data.clone();
198
199 tokio::task::spawn_blocking(move || {
200 let server_addr = server_str
202 .parse::<std::net::SocketAddr>()
203 .with_context(|| format!("Invalid server address: {server_str}"))?;
204
205 let conn =
207 UdpClientConnection::new(server_addr).context("Failed to create UDP connection")?;
208
209 let signer = create_tsig_signer(&key_data)?;
211
212 let client = SyncClient::with_tsigner(conn, signer);
214
215 let zone = Name::from_str(&zone_name_str)
217 .with_context(|| format!("Invalid zone name: {zone_name_str}"))?;
218
219 let fqdn = if name_str == "@" || name_str.is_empty() {
221 zone.clone()
222 } else {
223 Name::from_str(&format!("{name_str}.{zone_name_str}"))
224 .with_context(|| format!("Invalid record name: {name_str}.{zone_name_str}"))?
225 };
226
227 info!(
228 "Deleting {:?} record: {} from zone {}",
229 record_type, fqdn, zone_name_str
230 );
231
232 let mut dummy_record = Record::new();
234 dummy_record.set_name(fqdn.clone());
235 dummy_record.set_record_type(record_type);
236 dummy_record.set_dns_class(DNSClass::IN);
237
238 let response = client
240 .delete_rrset(dummy_record, zone.clone())
241 .with_context(|| {
242 format!("Failed to send DNS UPDATE to delete {record_type:?} record {fqdn}")
243 })?;
244
245 match response.response_code() {
247 ResponseCode::NoError => {
248 info!(
249 "Successfully deleted {:?} record: {} from zone {}",
250 record_type, name_str, zone_name_str
251 );
252 Ok(())
253 }
254 code => {
255 warn!(
256 "DNS DELETE for {:?} record {fqdn} returned code: {:?} (may not have existed)",
257 record_type, code
258 );
259 Ok(())
261 }
262 }
263 })
264 .await
265 .with_context(|| {
266 format!("DNS delete task panicked or failed for {record_type:?} record {name}")
267 })?
268}
269
270#[cfg(test)]
271mod mod_tests;