1use super::super::types::RndcKeyData;
7use super::{build_authenticated_client, build_record_fqdn, should_update_record};
8use anyhow::{Context, Result};
9use hickory_net::client::ClientHandle;
10use hickory_proto::op::ResponseCode;
11use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType};
12use std::collections::HashSet;
13use std::net::{Ipv4Addr, Ipv6Addr};
14use std::str::FromStr;
15use tracing::{error, info};
16
17use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
18
19fn compare_ip_rrset(existing_records: &[Record], desired_ips: &[String]) -> bool {
34 let existing_ips: HashSet<String> = existing_records
35 .iter()
36 .filter_map(|record| {
37 if let RData::A(ipv4) = &record.data {
38 Some(ipv4.to_string())
39 } else {
40 None
41 }
42 })
43 .collect();
44
45 let desired_set: HashSet<String> = desired_ips.iter().cloned().collect();
46 existing_ips == desired_set
47}
48
49fn compare_ipv6_rrset(existing_records: &[Record], desired_ips: &[String]) -> bool {
64 let existing_ips: HashSet<String> = existing_records
65 .iter()
66 .filter_map(|record| {
67 if let RData::AAAA(ipv6) = &record.data {
68 Some(ipv6.to_string())
69 } else {
70 None
71 }
72 })
73 .collect();
74
75 let desired_set: HashSet<String> = desired_ips.iter().cloned().collect();
76 existing_ips == desired_set
77}
78
79#[allow(clippy::too_many_arguments)]
98pub async fn add_a_record(
99 zone_name: &str,
100 name: &str,
101 ipv4_addresses: &[String],
102 ttl: Option<i32>,
103 server: &str,
104 key_data: &RndcKeyData,
105) -> Result<()> {
106 let should_update = should_update_record(
107 zone_name,
108 name,
109 RecordType::A,
110 "A",
111 server,
112 |existing_records| compare_ip_rrset(existing_records, ipv4_addresses),
113 )
114 .await?;
115
116 if !should_update {
117 return Ok(());
118 }
119
120 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
121 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
122
123 let zone =
124 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))?;
125 let fqdn = build_record_fqdn(zone_name, name)?;
126
127 let mut client = build_authenticated_client(server, key_data).await?;
128
129 let delete_record = Record::from_rdata(fqdn.clone(), 0, RData::Update0(RecordType::A));
131 let _ = client.delete_rrset(delete_record, zone.clone()).await;
132
133 info!(
134 "Adding A record RRset: {} -> {:?} (TTL: {}, {} addresses)",
135 fqdn,
136 ipv4_addresses,
137 ttl_value,
138 ipv4_addresses.len()
139 );
140
141 for ip_str in ipv4_addresses {
143 let ipv4_addr = Ipv4Addr::from_str(ip_str)
144 .with_context(|| format!("Invalid IPv4 address: {ip_str}"))?;
145
146 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::A(ipv4_addr.into()));
147 record.dns_class = DNSClass::IN;
148
149 let response = client
150 .append(record, zone.clone(), false)
151 .await
152 .with_context(|| format!("Failed to add A record for {fqdn} -> {ip_str}"))?;
153
154 match response.metadata.response_code {
155 ResponseCode::NoError => {
156 info!("Successfully added A record: {} -> {}", name, ip_str);
157 }
158 code => {
159 error!(
160 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
161 fqdn, ip_str, code
162 );
163 return Err(anyhow::anyhow!(
164 "DNS update failed with response code: {code:?}"
165 ));
166 }
167 }
168 }
169
170 Ok(())
171}
172
173#[allow(clippy::too_many_arguments)]
192pub async fn add_aaaa_record(
193 zone_name: &str,
194 name: &str,
195 ipv6_addresses: &[String],
196 ttl: Option<i32>,
197 server: &str,
198 key_data: &RndcKeyData,
199) -> Result<()> {
200 let should_update = should_update_record(
201 zone_name,
202 name,
203 RecordType::AAAA,
204 "AAAA",
205 server,
206 |existing_records| compare_ipv6_rrset(existing_records, ipv6_addresses),
207 )
208 .await?;
209
210 if !should_update {
211 return Ok(());
212 }
213
214 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
215 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
216
217 let zone =
218 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))?;
219 let fqdn = build_record_fqdn(zone_name, name)?;
220
221 let mut client = build_authenticated_client(server, key_data).await?;
222
223 let delete_record = Record::from_rdata(fqdn.clone(), 0, RData::Update0(RecordType::AAAA));
224 let _ = client.delete_rrset(delete_record, zone.clone()).await;
225
226 info!(
227 "Adding AAAA record RRset: {} -> {:?} (TTL: {}, {} addresses)",
228 fqdn,
229 ipv6_addresses,
230 ttl_value,
231 ipv6_addresses.len()
232 );
233
234 for ip_str in ipv6_addresses {
235 let ipv6_addr = Ipv6Addr::from_str(ip_str)
236 .with_context(|| format!("Invalid IPv6 address: {ip_str}"))?;
237
238 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::AAAA(ipv6_addr.into()));
239 record.dns_class = DNSClass::IN;
240
241 let response = client
242 .append(record, zone.clone(), false)
243 .await
244 .with_context(|| format!("Failed to add AAAA record for {fqdn} -> {ip_str}"))?;
245
246 match response.metadata.response_code {
247 ResponseCode::NoError => {
248 info!("Successfully added AAAA record: {} -> {}", name, ip_str);
249 }
250 code => {
251 error!(
252 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
253 fqdn, ip_str, code
254 );
255 return Err(anyhow::anyhow!(
256 "DNS update failed with response code: {code:?}"
257 ));
258 }
259 }
260 }
261
262 Ok(())
263}
264
265#[cfg(test)]
266#[path = "a_tests.rs"]
267mod a_tests;