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::net::{Ipv4Addr, Ipv6Addr};
14use std::str::FromStr;
15use tracing::{error, info};
16
17use crate::bind9::rndc::create_tsig_signer;
18use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
19
20#[allow(clippy::too_many_arguments)]
34pub async fn add_a_record(
35 zone_name: &str,
36 name: &str,
37 ipv4: &str,
38 ttl: Option<i32>,
39 server: &str,
40 key_data: &RndcKeyData,
41) -> Result<()> {
42 use hickory_client::rr::RecordType;
43
44 let should_update = should_update_record(
46 zone_name,
47 name,
48 RecordType::A,
49 "A",
50 server,
51 |existing_records| {
52 if existing_records.len() == 1 {
54 if let Some(RData::A(existing_ip)) = existing_records[0].data() {
55 return existing_ip.to_string() == ipv4;
56 }
57 }
58 false
59 },
60 )
61 .await?;
62
63 if !should_update {
64 return Ok(());
65 }
66
67 let zone_name_str = zone_name.to_string();
68 let name_str = name.to_string();
69 let ipv4_str = ipv4.to_string();
70 let server_str = server.to_string();
71 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
72 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
73
74 let key_data = key_data.clone();
76
77 tokio::task::spawn_blocking(move || {
79 let server_addr = server_str
81 .parse::<std::net::SocketAddr>()
82 .with_context(|| format!("Invalid server address: {server_str}"))?;
83
84 let conn =
86 UdpClientConnection::new(server_addr).context("Failed to create UDP connection")?;
87
88 let signer = create_tsig_signer(&key_data)?;
90
91 let client = SyncClient::with_tsigner(conn, signer);
93
94 let zone = Name::from_str(&zone_name_str)
96 .with_context(|| format!("Invalid zone name: {zone_name_str}"))?;
97
98 let fqdn = if name_str == "@" || name_str.is_empty() {
100 zone.clone()
101 } else {
102 Name::from_str(&format!("{name_str}.{zone_name_str}"))
103 .with_context(|| format!("Invalid record name: {name_str}.{zone_name_str}"))?
104 };
105
106 let ipv4_addr = Ipv4Addr::from_str(&ipv4_str)
108 .with_context(|| format!("Invalid IPv4 address: {ipv4_str}"))?;
109
110 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::A(ipv4_addr.into()));
112 record.set_dns_class(DNSClass::IN);
113
114 info!(
118 "Adding A record: {} -> {} (TTL: {})",
119 fqdn, ipv4_str, ttl_value
120 );
121 let response = client
122 .append(record, zone.clone(), false)
123 .with_context(|| format!("Failed to send DNS UPDATE for A record {fqdn}"))?;
124
125 match response.response_code() {
127 ResponseCode::NoError => {
128 info!("Successfully added A record: {} -> {}", name_str, ipv4_str);
129 Ok(())
130 }
131 code => {
132 error!(
133 "DNS UPDATE rejected by server for {} -> {} with response code: {:?}",
134 fqdn, ipv4_str, code
135 );
136 Err(anyhow::anyhow!(
137 "DNS update failed with response code: {code:?}"
138 ))
139 }
140 }
141 })
142 .await
143 .with_context(|| format!("DNS update task panicked or failed for A record {name} -> {ipv4}"))?
144}
145
146#[allow(clippy::too_many_arguments)]
152pub async fn add_aaaa_record(
153 zone_name: &str,
154 name: &str,
155 ipv6: &str,
156 ttl: Option<i32>,
157 server: &str,
158 key_data: &RndcKeyData,
159) -> Result<()> {
160 use hickory_client::rr::RecordType;
161
162 let should_update = should_update_record(
164 zone_name,
165 name,
166 RecordType::AAAA,
167 "AAAA",
168 server,
169 |existing_records| {
170 if existing_records.len() == 1 {
172 if let Some(RData::AAAA(existing_ip)) = existing_records[0].data() {
173 return existing_ip.to_string() == ipv6;
174 }
175 }
176 false
177 },
178 )
179 .await?;
180
181 if !should_update {
182 return Ok(());
183 }
184
185 let zone_name_str = zone_name.to_string();
186 let name_str = name.to_string();
187 let ipv6_str = ipv6.to_string();
188 let server_str = server.to_string();
189 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
190 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
191 let key_data = key_data.clone();
192
193 tokio::task::spawn_blocking(move || {
194 let server_addr = server_str
195 .parse::<std::net::SocketAddr>()
196 .with_context(|| format!("Invalid server address: {server_str}"))?;
197 let conn =
198 UdpClientConnection::new(server_addr).context("Failed to create UDP connection")?;
199 let signer = create_tsig_signer(&key_data)?;
200 let client = SyncClient::with_tsigner(conn, signer);
201
202 let zone = Name::from_str(&zone_name_str)
203 .with_context(|| format!("Invalid zone name: {zone_name_str}"))?;
204 let fqdn = if name_str == "@" || name_str.is_empty() {
205 zone.clone()
206 } else {
207 Name::from_str(&format!("{name_str}.{zone_name_str}"))
208 .with_context(|| format!("Invalid record name: {name_str}.{zone_name_str}"))?
209 };
210
211 let ipv6_addr = Ipv6Addr::from_str(&ipv6_str)
212 .with_context(|| format!("Invalid IPv6 address: {ipv6_str}"))?;
213 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::AAAA(ipv6_addr.into()));
214 record.set_dns_class(DNSClass::IN);
215
216 info!(
218 "Adding AAAA record: {} -> {} (TTL: {})",
219 fqdn, ipv6_str, ttl_value
220 );
221 let response = client
222 .append(record, zone.clone(), false)
223 .with_context(|| format!("Failed to add AAAA record for {fqdn}"))?;
224
225 match response.response_code() {
226 ResponseCode::NoError => {
227 info!(
228 "Successfully added AAAA record: {} -> {}",
229 name_str, ipv6_str
230 );
231 Ok(())
232 }
233 code => Err(anyhow::anyhow!(
234 "DNS update failed with response code: {code:?}"
235 )),
236 }
237 })
238 .await
239 .context("DNS update task failed")?
240}
241
242#[cfg(test)]
243#[path = "a_tests.rs"]
244mod a_tests;