1use super::super::types::RndcKeyData;
7use super::{
8 build_authenticated_client, build_delete_rrset_record, build_record_fqdn, effective_record_ttl,
9 rrset_ttl_matches, should_update_record,
10};
11use anyhow::{Context, Result};
12use hickory_net::client::ClientHandle;
13use hickory_proto::op::ResponseCode;
14use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType};
15use std::collections::HashSet;
16use std::net::{Ipv4Addr, Ipv6Addr};
17use std::str::FromStr;
18use tracing::{error, info, warn};
19
20fn compare_ip_rrset(existing_records: &[Record], desired_ips: &[String], desired_ttl: u32) -> bool {
36 if !rrset_ttl_matches(existing_records, desired_ttl) {
37 return false;
38 }
39
40 let existing_ips: HashSet<String> = existing_records
41 .iter()
42 .filter_map(|record| {
43 if let RData::A(ipv4) = &record.data {
44 Some(ipv4.to_string())
45 } else {
46 None
47 }
48 })
49 .collect();
50
51 let desired_set: HashSet<String> = desired_ips.iter().cloned().collect();
52 existing_ips == desired_set
53}
54
55fn compare_ipv6_rrset(
77 existing_records: &[Record],
78 desired_ips: &[String],
79 desired_ttl: u32,
80) -> bool {
81 if !rrset_ttl_matches(existing_records, desired_ttl) {
82 return false;
83 }
84
85 let existing_ips: HashSet<Ipv6Addr> = existing_records
86 .iter()
87 .filter_map(|record| {
88 if let RData::AAAA(ipv6) = &record.data {
89 Some(ipv6.0)
90 } else {
91 None
92 }
93 })
94 .collect();
95
96 let mut desired_set: HashSet<Ipv6Addr> = HashSet::with_capacity(desired_ips.len());
97 for ip_str in desired_ips {
98 match Ipv6Addr::from_str(ip_str) {
99 Ok(addr) => {
100 desired_set.insert(addr);
101 }
102 Err(e) => {
103 warn!(
104 "Invalid IPv6 address '{}' in desired spec (treating as mismatch): {}",
105 ip_str, e
106 );
107 return false;
108 }
109 }
110 }
111
112 existing_ips == desired_set
113}
114
115#[allow(clippy::too_many_arguments)]
134pub async fn add_a_record(
135 zone_name: &str,
136 name: &str,
137 ipv4_addresses: &[String],
138 ttl: Option<i32>,
139 server: &str,
140 key_data: &RndcKeyData,
141) -> Result<()> {
142 let ttl_value = effective_record_ttl(ttl);
143 let should_update = should_update_record(
144 zone_name,
145 name,
146 RecordType::A,
147 "A",
148 server,
149 |existing_records| compare_ip_rrset(existing_records, ipv4_addresses, ttl_value),
150 )
151 .await?;
152
153 if !should_update {
154 return Ok(());
155 }
156
157 let zone =
158 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))?;
159 let fqdn = build_record_fqdn(zone_name, name)?;
160
161 let mut client = build_authenticated_client(server, key_data).await?;
162
163 let delete_record = build_delete_rrset_record(&fqdn, RecordType::A);
165 let _ = client.delete_rrset(delete_record, zone.clone()).await;
166
167 info!(
168 "Adding A record RRset: {} -> {:?} (TTL: {}, {} addresses)",
169 fqdn,
170 ipv4_addresses,
171 ttl_value,
172 ipv4_addresses.len()
173 );
174
175 for ip_str in ipv4_addresses {
177 let ipv4_addr = Ipv4Addr::from_str(ip_str)
178 .with_context(|| format!("Invalid IPv4 address: {ip_str}"))?;
179
180 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::A(ipv4_addr.into()));
181 record.dns_class = DNSClass::IN;
182
183 let response = client
184 .append(record, zone.clone(), false)
185 .await
186 .with_context(|| format!("Failed to add A record for {fqdn} -> {ip_str}"))?;
187
188 match response.metadata.response_code {
189 ResponseCode::NoError => {
190 info!("Successfully added A record: {} -> {}", name, ip_str);
191 }
192 code => {
193 error!(
194 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
195 fqdn, ip_str, code
196 );
197 return Err(anyhow::anyhow!(
198 "DNS update failed with response code: {code:?}"
199 ));
200 }
201 }
202 }
203
204 Ok(())
205}
206
207#[allow(clippy::too_many_arguments)]
226pub async fn add_aaaa_record(
227 zone_name: &str,
228 name: &str,
229 ipv6_addresses: &[String],
230 ttl: Option<i32>,
231 server: &str,
232 key_data: &RndcKeyData,
233) -> Result<()> {
234 let ttl_value = effective_record_ttl(ttl);
235 let should_update = should_update_record(
236 zone_name,
237 name,
238 RecordType::AAAA,
239 "AAAA",
240 server,
241 |existing_records| compare_ipv6_rrset(existing_records, ipv6_addresses, ttl_value),
242 )
243 .await?;
244
245 if !should_update {
246 return Ok(());
247 }
248
249 let zone =
250 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))?;
251 let fqdn = build_record_fqdn(zone_name, name)?;
252
253 let mut client = build_authenticated_client(server, key_data).await?;
254
255 let delete_record = build_delete_rrset_record(&fqdn, RecordType::AAAA);
256 let _ = client.delete_rrset(delete_record, zone.clone()).await;
257
258 info!(
259 "Adding AAAA record RRset: {} -> {:?} (TTL: {}, {} addresses)",
260 fqdn,
261 ipv6_addresses,
262 ttl_value,
263 ipv6_addresses.len()
264 );
265
266 for ip_str in ipv6_addresses {
267 let ipv6_addr = Ipv6Addr::from_str(ip_str)
268 .with_context(|| format!("Invalid IPv6 address: {ip_str}"))?;
269
270 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::AAAA(ipv6_addr.into()));
271 record.dns_class = DNSClass::IN;
272
273 let response = client
274 .append(record, zone.clone(), false)
275 .await
276 .with_context(|| format!("Failed to add AAAA record for {fqdn} -> {ip_str}"))?;
277
278 match response.metadata.response_code {
279 ResponseCode::NoError => {
280 info!("Successfully added AAAA record: {} -> {}", name, ip_str);
281 }
282 code => {
283 error!(
284 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
285 fqdn, ip_str, code
286 );
287 return Err(anyhow::anyhow!(
288 "DNS update failed with response code: {code:?}"
289 ));
290 }
291 }
292 }
293
294 Ok(())
295}
296
297#[cfg(test)]
298#[path = "a_tests.rs"]
299mod a_tests;