bindy/bind9/records/
txt.rs1use super::super::types::RndcKeyData;
7use super::should_update_record;
8use anyhow::Result;
9use hickory_client::client::{Client, SyncClient};
10use hickory_client::op::ResponseCode;
11use hickory_client::rr::{rdata, DNSClass, Name, RData, Record};
12use hickory_client::udp::UdpClientConnection;
13use std::str::FromStr;
14use tracing::info;
15
16use crate::bind9::rndc::create_tsig_signer;
17use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
18
19#[allow(clippy::too_many_arguments)]
25pub async fn add_txt_record(
26 zone_name: &str,
27 name: &str,
28 texts: &[String],
29 ttl: Option<i32>,
30 server: &str,
31 key_data: &RndcKeyData,
32) -> Result<()> {
33 use hickory_client::rr::RecordType;
34
35 let texts_for_comparison = texts.to_vec();
37 let should_update = should_update_record(
38 zone_name,
39 name,
40 RecordType::TXT,
41 "TXT",
42 server,
43 |existing_records| {
44 if existing_records.len() == 1 {
46 if let Some(RData::TXT(existing_txt)) = existing_records[0].data() {
47 let existing_texts: Vec<String> = existing_txt
49 .txt_data()
50 .iter()
51 .map(|bytes| String::from_utf8_lossy(bytes).to_string())
52 .collect();
53 return existing_texts == texts_for_comparison;
54 }
55 }
56 false
57 },
58 )
59 .await?;
60
61 if !should_update {
62 return Ok(());
63 }
64
65 let zone_name_str = zone_name.to_string();
66 let name_str = name.to_string();
67 let texts_vec: Vec<String> = texts.to_vec();
68 let server_str = server.to_string();
69 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
70 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
71 let key_data = key_data.clone();
72
73 tokio::task::spawn_blocking(move || {
74 let server_addr = server_str.parse::<std::net::SocketAddr>()?;
75 let conn = UdpClientConnection::new(server_addr)?;
76 let signer = create_tsig_signer(&key_data)?;
77 let client = SyncClient::with_tsigner(conn, signer);
78
79 let zone = Name::from_str(&zone_name_str)?;
80 let fqdn = if name_str == "@" || name_str.is_empty() {
81 zone.clone()
82 } else {
83 Name::from_str(&format!("{name_str}.{zone_name_str}"))?
84 };
85
86 let txt_rdata = rdata::TXT::new(texts_vec.clone());
87 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::TXT(txt_rdata));
88 record.set_dns_class(DNSClass::IN);
89
90 info!(
91 "Adding TXT record: {} -> {:?} (TTL: {})",
92 record.name(),
93 texts_vec,
94 ttl_value
95 );
96 let response = client.append(record, zone, false)?;
98
99 match response.response_code() {
100 ResponseCode::NoError => {
101 info!("Successfully added TXT record: {}", name_str);
102 Ok(())
103 }
104 code => Err(anyhow::anyhow!(
105 "DNS update failed with response code: {code:?}"
106 )),
107 }
108 })
109 .await?
110}
111
112#[cfg(test)]
113#[path = "txt_tests.rs"]
114mod txt_tests;