bindy/bind9/records/
caa.rs1use 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::{rdata, DNSClass, Name, RData, Record};
12use hickory_client::udp::UdpClientConnection;
13use std::str::FromStr;
14use tracing::info;
15use url::Url;
16
17use crate::bind9::rndc::create_tsig_signer;
18use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
19
20#[allow(clippy::too_many_arguments)]
30#[allow(clippy::too_many_lines)]
31pub async fn add_caa_record(
32 zone_name: &str,
33 name: &str,
34 flags: i32,
35 tag: &str,
36 value: &str,
37 ttl: Option<i32>,
38 server: &str,
39 key_data: &RndcKeyData,
40) -> Result<()> {
41 use hickory_client::rr::RecordType;
42
43 let flags_for_comparison = flags;
45 let tag_for_comparison = tag.to_string();
46 let value_for_comparison = value.to_string();
47 let should_update = should_update_record(
48 zone_name,
49 name,
50 RecordType::CAA,
51 "CAA",
52 server,
53 |existing_records| {
54 if existing_records.len() == 1 {
56 if let Some(RData::CAA(existing_caa)) = existing_records[0].data() {
57 let issuer_critical = flags_for_comparison != 0;
58 let flags_match = existing_caa.issuer_critical() == issuer_critical;
59 let tag_match = existing_caa.tag().to_string() == tag_for_comparison;
60
61 let value_match = match tag_for_comparison.as_str() {
63 "issue" | "issuewild" => {
64 let existing_value = existing_caa.value().to_string();
66 existing_value == value_for_comparison
67 }
68 "iodef" => {
69 let existing_value = existing_caa.value().to_string();
70 existing_value == value_for_comparison
71 }
72 _ => false,
73 };
74
75 return flags_match && tag_match && value_match;
76 }
77 }
78 false
79 },
80 )
81 .await?;
82
83 if !should_update {
84 return Ok(());
85 }
86
87 let zone_name_str = zone_name.to_string();
88 let name_str = name.to_string();
89 let tag_str = tag.to_string();
90 let value_str = value.to_string();
91 let server_str = server.to_string();
92 let key_data = key_data.clone();
93
94 tokio::task::spawn_blocking(move || {
95 let server_addr = server_str.parse::<std::net::SocketAddr>().context(format!(
96 "Invalid server address for CAA record update: {server_str}"
97 ))?;
98
99 let conn = UdpClientConnection::new(server_addr)
100 .context("Failed to create UDP connection for CAA record")?;
101
102 let signer =
103 create_tsig_signer(&key_data).context("Failed to create TSIG signer for CAA record")?;
104
105 let client = SyncClient::with_tsigner(conn, signer);
106
107 let fqdn_str = if name_str.is_empty() || name_str == "@" {
108 zone_name_str.clone()
109 } else {
110 format!("{name_str}.{zone_name_str}")
111 };
112
113 let fqdn = Name::from_str(&fqdn_str)
114 .context(format!("Invalid FQDN for CAA record: {fqdn_str}"))?;
115
116 let zone = Name::from_str(&zone_name_str)
117 .context(format!("Invalid zone name for CAA: {zone_name_str}"))?;
118
119 let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
120 .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
121
122 let issuer_critical = flags != 0;
124
125 let record_data = match tag_str.as_str() {
127 "issue" => {
128 let ca_name = if value_str.is_empty() {
130 None
131 } else {
132 Some(
133 Name::from_str(&value_str)
134 .context(format!("Invalid CA domain name: {value_str}"))?,
135 )
136 };
137 rdata::CAA::new_issue(issuer_critical, ca_name, Vec::new())
138 }
139 "issuewild" => {
140 let ca_name = if value_str.is_empty() {
141 None
142 } else {
143 Some(
144 Name::from_str(&value_str)
145 .context(format!("Invalid CA domain name: {value_str}"))?,
146 )
147 };
148 rdata::CAA::new_issuewild(issuer_critical, ca_name, Vec::new())
149 }
150 "iodef" => {
151 let url =
152 Url::parse(&value_str).context(format!("Invalid iodef URL: {value_str}"))?;
153 rdata::CAA::new_iodef(issuer_critical, url)
154 }
155 _ => anyhow::bail!(
156 "Unsupported CAA tag: {tag_str}. Supported tags: issue, issuewild, iodef"
157 ),
158 };
159
160 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::CAA(record_data));
161 record.set_dns_class(DNSClass::IN);
162
163 let response = client
165 .append(record, zone.clone(), false)
166 .context(format!("Failed to send CAA record update for {fqdn_str}"))?;
167
168 match response.response_code() {
169 ResponseCode::NoError => {
170 info!(
171 "Successfully added CAA record: {} -> {} {} \"{}\" (TTL: {})",
172 fqdn_str, flags, tag_str, value_str, ttl_value
173 );
174 }
175 code => {
176 anyhow::bail!("DNS server rejected CAA record update for {fqdn_str}: {code:?}");
177 }
178 }
179
180 Ok(())
181 })
182 .await
183 .context("CAA record update task panicked")??;
184
185 Ok(())
186}
187
188#[cfg(test)]
189#[path = "caa_tests.rs"]
190mod caa_tests;