1use anyhow::{Context, Result};
14use rndc::RndcClient;
15use std::time::Instant;
16use tracing::{debug, error, info};
17
18use crate::metrics;
19
20#[derive(Clone)]
22pub struct RndcConfig {
23 pub server: String,
24 pub algorithm: String,
25 pub secret: String,
26}
27
28impl std::fmt::Debug for RndcConfig {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("RndcConfig")
34 .field("server", &self.server)
35 .field("algorithm", &self.algorithm)
36 .field("secret", &"[REDACTED]")
37 .finish()
38 }
39}
40
41const ACCEPTED_RNDC_ALGORITHMS: &[&str] = &["sha224", "sha256", "sha384", "sha512"];
44
45const MAX_ZONE_NAME_LEN: usize = 253;
47
48const DEFAULT_RNDC_PORT: u16 = 953;
50
51pub(crate) fn validate_rndc_zone_name(zone_name: &str) -> Result<()> {
63 if zone_name.is_empty() || zone_name.len() > MAX_ZONE_NAME_LEN {
64 return Err(anyhow::anyhow!("invalid zone name length"));
65 }
66 if zone_name.contains("..") {
67 return Err(anyhow::anyhow!("zone name must not contain '..'"));
68 }
69 if let Some(bad) = zone_name
70 .chars()
71 .find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')))
72 {
73 return Err(anyhow::anyhow!(
74 "zone name contains invalid character: {:?}",
75 bad
76 ));
77 }
78 Ok(())
79}
80
81pub struct RndcExecutor {
83 client: RndcClient,
84}
85
86impl RndcExecutor {
87 pub fn new(server: String, algorithm: String, secret: String) -> Result<Self> {
99 let server = server.trim();
101 let mut algorithm = algorithm.trim().to_string();
102 let secret = secret.trim();
103
104 if algorithm.starts_with("hmac-") {
107 algorithm = algorithm.trim_start_matches("hmac-").to_string();
108 }
109
110 debug!("Using algorithm: {} for server: {}", algorithm, server);
111
112 if !ACCEPTED_RNDC_ALGORITHMS.contains(&algorithm.as_str()) {
116 return Err(anyhow::anyhow!(
117 "Invalid or deprecated algorithm '{}'. Accepted algorithms (without 'hmac-' prefix): {:?}",
118 algorithm,
119 ACCEPTED_RNDC_ALGORITHMS
120 ));
121 }
122
123 let client = RndcClient::new(server, &algorithm, secret)?;
124
125 Ok(Self { client })
126 }
127
128 async fn execute(&self, command: &str) -> Result<String> {
140 debug!("Executing RNDC command: {}", command);
141
142 let start = Instant::now();
143 let command_name = command.split_whitespace().next().unwrap_or("unknown");
144
145 let result = tokio::task::spawn_blocking({
147 let client = self.client.clone();
148 let command = command.to_string();
149 move || client.rndc_command(&command)
150 })
151 .await
152 .with_context(|| {
153 format!(
154 "Failed to execute RNDC command '{}': task join error",
155 command_name
156 )
157 })?;
158
159 let duration = start.elapsed().as_secs_f64();
160
161 let rndc_result = result.map_err(|e| {
162 let error_msg = format!("RNDC command '{}' failed: {}", command_name, e);
163 error!("{}", error_msg);
164 metrics::record_rndc_command(command_name, false, duration);
165 anyhow::anyhow!("{}", error_msg)
166 })?;
167
168 if let Some(err) = &rndc_result.err {
170 let error_msg = format!("RNDC command '{}' failed: {}", command_name, err);
171 error!("{}", error_msg);
172 metrics::record_rndc_command(command_name, false, duration);
173 return Err(anyhow::anyhow!("{}", error_msg));
174 }
175
176 let response = rndc_result.text.unwrap_or_default();
178 debug!("RNDC command '{}' succeeded: {}", command_name, response);
179 metrics::record_rndc_command(command_name, true, duration);
180 Ok(response)
181 }
182
183 pub async fn status(&self) -> Result<String> {
185 self.execute("status").await
186 }
187
188 pub async fn addzone(&self, zone_name: &str, zone_config: &str) -> Result<String> {
194 validate_rndc_zone_name(zone_name)?;
195 let command = format!("addzone {} {}", zone_name, zone_config);
196 self.execute(&command).await
197 }
198
199 pub async fn delzone(&self, zone_name: &str) -> Result<String> {
201 validate_rndc_zone_name(zone_name)?;
202 let command = format!("delzone {}", zone_name);
203 self.execute(&command).await
204 }
205
206 pub async fn reload(&self, zone_name: &str) -> Result<String> {
208 validate_rndc_zone_name(zone_name)?;
209 let command = format!("reload {}", zone_name);
210 self.execute(&command).await
211 }
212
213 pub async fn zonestatus(&self, zone_name: &str) -> Result<String> {
215 validate_rndc_zone_name(zone_name)?;
216 let command = format!("zonestatus {}", zone_name);
217 self.execute(&command).await
218 }
219
220 pub async fn freeze(&self, zone_name: &str) -> Result<String> {
222 validate_rndc_zone_name(zone_name)?;
223 let command = format!("freeze {}", zone_name);
224 self.execute(&command).await
225 }
226
227 pub async fn thaw(&self, zone_name: &str) -> Result<String> {
229 validate_rndc_zone_name(zone_name)?;
230 let command = format!("thaw {}", zone_name);
231 self.execute(&command).await
232 }
233
234 pub async fn notify(&self, zone_name: &str) -> Result<String> {
236 validate_rndc_zone_name(zone_name)?;
237 let command = format!("notify {}", zone_name);
238 self.execute(&command).await
239 }
240
241 pub async fn retransfer(&self, zone_name: &str) -> Result<String> {
246 validate_rndc_zone_name(zone_name)?;
247 let command = format!("retransfer {}", zone_name);
248 self.execute(&command).await
249 }
250
251 pub async fn modzone(&self, zone_name: &str, zone_config: &str) -> Result<String> {
257 validate_rndc_zone_name(zone_name)?;
258 let command = format!("modzone {} {}", zone_name, zone_config);
259 self.execute(&command).await
260 }
261
262 pub async fn showzone(&self, zone_name: &str) -> Result<String> {
270 validate_rndc_zone_name(zone_name)?;
271 let command = format!("showzone {}", zone_name);
272 self.execute(&command).await
273 }
274}
275
276impl Clone for RndcExecutor {
277 fn clone(&self) -> Self {
278 Self {
279 client: self.client.clone(),
280 }
281 }
282}
283
284pub fn parse_rndc_conf(path: &str) -> Result<RndcConfig> {
295 use crate::rndc_conf_parser::parse_rndc_conf_file;
296 use std::path::Path;
297
298 info!("Parsing rndc.conf from {}", path);
299
300 let conf_file = parse_rndc_conf_file(Path::new(path))
302 .map_err(|e| anyhow::anyhow!("Failed to parse rndc.conf: {}", e))?;
303
304 let default_key_name = match conf_file.options.default_key.clone() {
309 Some(name) => Some(name),
310 None => {
311 if conf_file.keys.len() > 1 {
312 return Err(anyhow::anyhow!(
313 "rndc.conf defines {} keys but no `default-key`; specify one explicitly",
314 conf_file.keys.len()
315 ));
316 }
317 let mut names: Vec<&String> = conf_file.keys.keys().collect();
318 names.sort();
319 names.first().map(|name| (*name).clone())
320 }
321 };
322
323 let key_block = if let Some(ref key_name) = default_key_name {
324 conf_file
325 .keys
326 .get(key_name)
327 .ok_or_else(|| anyhow::anyhow!("Default key '{}' not found", key_name))?
328 } else {
329 return Err(anyhow::anyhow!("No keys found in configuration"));
330 };
331
332 if key_block.secret.trim().is_empty() {
336 return Err(anyhow::anyhow!(
337 "key '{}' has an empty secret",
338 default_key_name.as_deref().unwrap_or("unnamed")
339 ));
340 }
341
342 let server = conf_file
344 .options
345 .default_server
346 .clone()
347 .unwrap_or_else(|| "127.0.0.1".to_string());
348
349 let server = if server.contains(':') {
351 server
352 } else {
353 let port = conf_file.options.default_port.unwrap_or(DEFAULT_RNDC_PORT);
354 format!("{}:{}", server, port)
355 };
356
357 info!(
358 "Parsed rndc configuration: server={}, algorithm={}, key={}",
359 server,
360 key_block.algorithm,
361 default_key_name.unwrap_or_else(|| "unnamed".to_string())
362 );
363
364 Ok(RndcConfig {
365 server,
366 algorithm: key_block.algorithm.clone(),
367 secret: key_block.secret.clone(),
368 })
369}