1use super::super::types::RndcKeyData;
7use super::should_update_record;
8use anyhow::{Context, Result};
9use hickory_client::client::{Client, SyncClient};
10use hickory_client::op::ResponseCode;
11use hickory_client::rr::{DNSClass, Name, RData, Record};
12use hickory_client::udp::UdpClientConnection;
13use std::collections::HashSet;
14use std::net::{Ipv4Addr, Ipv6Addr};
15use std::str::FromStr;
16use tracing::{error, info};
17
18use crate::bind9::rndc::create_tsig_signer;
19use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
20
21fn compare_ip_rrset(existing_records: &[Record], desired_ips: &[String]) -> bool {
36 let existing_ips: HashSet<String> = existing_records
38 .iter()
39 .filter_map(|record| {
40 if let Some(RData::A(ipv4)) = record.data() {
42 Some(ipv4.to_string())
43 } else {
44 None }
46 })
47 .collect();
48
49 let desired_set: HashSet<String> = desired_ips.iter().cloned().collect();
54
55 existing_ips == desired_set
57}
58
59fn compare_ipv6_rrset(existing_records: &[Record], desired_ips: &[String]) -> bool {
74 let existing_ips: HashSet<String> = existing_records
76 .iter()
77 .filter_map(|record| {
78 if let Some(RData::AAAA(ipv6)) = record.data() {
80 Some(ipv6.to_string())
81 } else {
82 None
83 }
84 })
85 .collect();
86
87 let desired_set: HashSet<String> = desired_ips.iter().cloned().collect();
89
90 existing_ips == desired_set
92}
93
94#[allow(clippy::too_many_arguments)]
113pub async fn add_a_record(
114 zone_name: &str,
115 name: &str,
116 ipv4_addresses: &[String],
117 ttl: Option<i32>,
118 server: &str,
119 key_data: &RndcKeyData,
120) -> Result<()> {
121 use hickory_client::rr::RecordType;
122
123 let should_update = should_update_record(
125 zone_name,
126 name,
127 RecordType::A,
128 "A",
129 server,
130 |existing_records| {
131 compare_ip_rrset(existing_records, ipv4_addresses)
133 },
134 )
135 .await?;
136
137 if !should_update {
138 return Ok(());
139 }
140
141 let zone_name_str = zone_name.to_string();
142 let name_str = name.to_string();
143 let ipv4_addresses_vec: Vec<String> = ipv4_addresses.to_vec();
144 let server_str = server.to_string();
145 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
146 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
147
148 let key_data = key_data.clone();
150
151 let ipv4_addresses_for_error = ipv4_addresses_vec.clone();
153
154 tokio::task::spawn_blocking(move || {
156 let server_addr = server_str
158 .parse::<std::net::SocketAddr>()
159 .with_context(|| format!("Invalid server address: {server_str}"))?;
160
161 let conn =
163 UdpClientConnection::new(server_addr).context("Failed to create UDP connection")?;
164
165 let signer = create_tsig_signer(&key_data)?;
167
168 let client = SyncClient::with_tsigner(conn, signer);
170
171 let zone = Name::from_str(&zone_name_str)
173 .with_context(|| format!("Invalid zone name: {zone_name_str}"))?;
174
175 let fqdn = if name_str == "@" || name_str.is_empty() {
177 zone.clone()
178 } else {
179 Name::from_str(&format!("{name_str}.{zone_name_str}"))
180 .with_context(|| format!("Invalid record name: {name_str}.{zone_name_str}"))?
181 };
182
183 let mut delete_record = Record::new();
185 delete_record.set_name(fqdn.clone());
186 delete_record.set_record_type(RecordType::A);
187 delete_record.set_dns_class(DNSClass::IN);
188 let _ = client.delete_rrset(delete_record, zone.clone());
189
190 info!(
192 "Adding A record RRset: {} -> {:?} (TTL: {}, {} addresses)",
193 fqdn,
194 ipv4_addresses_vec,
195 ttl_value,
196 ipv4_addresses_vec.len()
197 );
198
199 for ip_str in &ipv4_addresses_vec {
200 let ipv4_addr = Ipv4Addr::from_str(ip_str)
202 .with_context(|| format!("Invalid IPv4 address: {ip_str}"))?;
203
204 let mut record =
206 Record::from_rdata(fqdn.clone(), ttl_value, RData::A(ipv4_addr.into()));
207 record.set_dns_class(DNSClass::IN);
208
209 let response = client
211 .append(record, zone.clone(), false)
212 .with_context(|| format!("Failed to add A record for {fqdn} -> {ip_str}"))?;
213
214 match response.response_code() {
216 ResponseCode::NoError => {
217 info!("Successfully added A record: {} -> {}", name_str, ip_str);
218 }
219 code => {
220 error!(
221 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
222 fqdn, ip_str, code
223 );
224 return Err(anyhow::anyhow!(
225 "DNS update failed with response code: {code:?}"
226 ));
227 }
228 }
229 }
230
231 Ok(())
232 })
233 .await
234 .with_context(|| format!("DNS update task panicked or failed for A record {name} -> {ipv4_addresses_for_error:?}"))?
235}
236
237#[allow(clippy::too_many_arguments)]
256pub async fn add_aaaa_record(
257 zone_name: &str,
258 name: &str,
259 ipv6_addresses: &[String],
260 ttl: Option<i32>,
261 server: &str,
262 key_data: &RndcKeyData,
263) -> Result<()> {
264 use hickory_client::rr::RecordType;
265
266 let should_update = should_update_record(
268 zone_name,
269 name,
270 RecordType::AAAA,
271 "AAAA",
272 server,
273 |existing_records| {
274 compare_ipv6_rrset(existing_records, ipv6_addresses)
276 },
277 )
278 .await?;
279
280 if !should_update {
281 return Ok(());
282 }
283
284 let zone_name_str = zone_name.to_string();
285 let name_str = name.to_string();
286 let ipv6_addresses_vec: Vec<String> = ipv6_addresses.to_vec();
287 let server_str = server.to_string();
288 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
289 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
290 let key_data = key_data.clone();
291
292 let ipv6_addresses_for_error = ipv6_addresses_vec.clone();
294
295 tokio::task::spawn_blocking(move || {
296 let server_addr = server_str
297 .parse::<std::net::SocketAddr>()
298 .with_context(|| format!("Invalid server address: {server_str}"))?;
299 let conn =
300 UdpClientConnection::new(server_addr).context("Failed to create UDP connection")?;
301 let signer = create_tsig_signer(&key_data)?;
302 let client = SyncClient::with_tsigner(conn, signer);
303
304 let zone = Name::from_str(&zone_name_str)
305 .with_context(|| format!("Invalid zone name: {zone_name_str}"))?;
306 let fqdn = if name_str == "@" || name_str.is_empty() {
307 zone.clone()
308 } else {
309 Name::from_str(&format!("{name_str}.{zone_name_str}"))
310 .with_context(|| format!("Invalid record name: {name_str}.{zone_name_str}"))?
311 };
312
313 let mut delete_record = Record::new();
315 delete_record.set_name(fqdn.clone());
316 delete_record.set_record_type(RecordType::AAAA);
317 delete_record.set_dns_class(DNSClass::IN);
318 let _ = client.delete_rrset(delete_record, zone.clone());
319
320 info!(
322 "Adding AAAA record RRset: {} -> {:?} (TTL: {}, {} addresses)",
323 fqdn,
324 ipv6_addresses_vec,
325 ttl_value,
326 ipv6_addresses_vec.len()
327 );
328
329 for ip_str in &ipv6_addresses_vec {
330 let ipv6_addr = Ipv6Addr::from_str(ip_str)
331 .with_context(|| format!("Invalid IPv6 address: {ip_str}"))?;
332 let mut record =
333 Record::from_rdata(fqdn.clone(), ttl_value, RData::AAAA(ipv6_addr.into()));
334 record.set_dns_class(DNSClass::IN);
335
336 let response = client
337 .append(record, zone.clone(), false)
338 .with_context(|| format!("Failed to add AAAA record for {fqdn} -> {ip_str}"))?;
339
340 match response.response_code() {
341 ResponseCode::NoError => {
342 info!("Successfully added AAAA record: {} -> {}", name_str, ip_str);
343 }
344 code => {
345 error!(
346 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
347 fqdn, ip_str, code
348 );
349 return Err(anyhow::anyhow!(
350 "DNS update failed with response code: {code:?}"
351 ));
352 }
353 }
354 }
355
356 Ok(())
357 })
358 .await
359 .with_context(|| format!("DNS update task panicked or failed for AAAA record {name} -> {ipv6_addresses_for_error:?}"))?
360}
361
362#[cfg(test)]
363#[path = "a_tests.rs"]
364mod a_tests;