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_net::client::{Client, ClientHandle};
19use hickory_net::runtime::TokioRuntimeProvider;
20use hickory_net::udp::UdpClientStream;
21use hickory_proto::op::ResponseCode;
22use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType};
23use std::net::SocketAddr;
24use std::str::FromStr;
25use tracing::{info, warn};
26
27use crate::bind9::rndc::create_tsig_signer;
28use crate::bind9::types::RndcKeyData;
29
30async fn build_query_client(server_str: &str) -> Result<Client<TokioRuntimeProvider>> {
32 let server_addr: SocketAddr = server_str
33 .parse()
34 .with_context(|| format!("Invalid server address: {server_str}"))?;
35 let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default()).build();
36 let (client, bg) = Client::<TokioRuntimeProvider>::from_sender(stream);
37 tokio::spawn(bg);
38 Ok(client)
39}
40
41pub(crate) async fn build_authenticated_client(
43 server_str: &str,
44 key_data: &RndcKeyData,
45) -> Result<Client<TokioRuntimeProvider>> {
46 let server_addr: SocketAddr = server_str
47 .parse()
48 .with_context(|| format!("Invalid server address: {server_str}"))?;
49 let signer = create_tsig_signer(key_data)?;
50 let stream = UdpClientStream::builder(server_addr, TokioRuntimeProvider::default())
51 .with_signer(Some(signer))
52 .build();
53 let (client, bg) = Client::<TokioRuntimeProvider>::from_sender(stream);
54 tokio::spawn(bg);
55 Ok(client)
56}
57
58pub(crate) fn build_record_fqdn(zone_name: &str, name: &str) -> Result<Name> {
62 if name == "@" || name.is_empty() {
63 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))
64 } else {
65 Name::from_str(&format!("{name}.{zone_name}"))
66 .with_context(|| format!("Invalid record name: {name}.{zone_name}"))
67 }
68}
69
70pub async fn query_dns_record(
90 zone_name: &str,
91 name: &str,
92 record_type: RecordType,
93 server: &str,
94) -> Result<Vec<Record>> {
95 let mut client = build_query_client(server).await?;
96 let fqdn = build_record_fqdn(zone_name, name)?;
97
98 let response = client
99 .query(fqdn.clone(), DNSClass::IN, record_type)
100 .await
101 .with_context(|| format!("Failed to query {record_type:?} record for {fqdn}"))?;
102
103 let records: Vec<Record> = response
104 .answers
105 .iter()
106 .filter(|r| r.record_type() == record_type)
107 .cloned()
108 .collect();
109
110 Ok(records)
111}
112
113pub async fn should_update_record<F>(
137 zone_name: &str,
138 name: &str,
139 record_type: RecordType,
140 record_type_name: &str,
141 server: &str,
142 compare_fn: F,
143) -> Result<bool>
144where
145 F: FnOnce(&[Record]) -> bool,
146{
147 match query_dns_record(zone_name, name, record_type, server).await {
148 Ok(existing_records) if !existing_records.is_empty() => {
149 if compare_fn(&existing_records) {
150 info!(
151 "{} record {} already exists with correct value - no changes needed",
152 record_type_name, name
153 );
154 Ok(false)
155 } else {
156 info!(
157 "{} record {} exists with different value(s), updating",
158 record_type_name, name
159 );
160 Ok(true)
161 }
162 }
163 Ok(_) => {
164 info!(
165 "{} record {} does not exist, creating",
166 record_type_name, name
167 );
168 Ok(true)
169 }
170 Err(e) => {
171 warn!(
172 "Failed to query existing {} record {} (will attempt update anyway): {}",
173 record_type_name, name, e
174 );
175 Ok(true)
176 }
177 }
178}
179
180pub async fn delete_dns_record(
201 zone_name: &str,
202 name: &str,
203 record_type: RecordType,
204 server: &str,
205 key_data: &RndcKeyData,
206) -> Result<()> {
207 let mut client = build_authenticated_client(server, key_data).await?;
208 let zone =
209 Name::from_str(zone_name).with_context(|| format!("Invalid zone name: {zone_name}"))?;
210 let fqdn = build_record_fqdn(zone_name, name)?;
211
212 info!(
213 "Deleting {:?} record: {} from zone {}",
214 record_type, fqdn, zone_name
215 );
216
217 let dummy_record = Record::from_rdata(fqdn.clone(), 0, RData::Update0(record_type));
219
220 let response = client
221 .delete_rrset(dummy_record, zone)
222 .await
223 .with_context(|| {
224 format!("Failed to send DNS UPDATE to delete {record_type:?} record {fqdn}")
225 })?;
226
227 match response.metadata.response_code {
228 ResponseCode::NoError => {
229 info!(
230 "Successfully deleted {:?} record: {} from zone {}",
231 record_type, name, zone_name
232 );
233 Ok(())
234 }
235 code => {
236 warn!(
237 "DNS DELETE for {:?} record {fqdn} returned code: {:?} (may not have existed)",
238 record_type, code
239 );
240 Ok(())
242 }
243 }
244}
245
246#[cfg(test)]
247mod mod_tests;