1use anyhow::{Context, Result};
14use rndc::RndcClient;
15use std::time::Instant;
16use tracing::{debug, error, info};
17
18use crate::metrics;
19
20#[derive(Debug, Clone)]
22pub struct RndcConfig {
23 pub server: String,
24 pub algorithm: String,
25 pub secret: String,
26}
27
28pub struct RndcExecutor {
30 client: RndcClient,
31}
32
33impl RndcExecutor {
34 pub fn new(server: String, algorithm: String, secret: String) -> Result<Self> {
46 let server = server.trim();
48 let mut algorithm = algorithm.trim().to_string();
49 let secret = secret.trim();
50
51 if algorithm.starts_with("hmac-") {
54 algorithm = algorithm.trim_start_matches("hmac-").to_string();
55 }
56
57 debug!("Using algorithm: {} for server: {}", algorithm, server);
58
59 let valid_algorithms = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"];
61
62 if !valid_algorithms.contains(&algorithm.as_str()) {
63 return Err(anyhow::anyhow!(
64 "Invalid algorithm '{}'. Valid algorithms (without 'hmac-' prefix): {:?}",
65 algorithm,
66 valid_algorithms
67 ));
68 }
69
70 let client = RndcClient::new(server, &algorithm, secret)?;
71
72 Ok(Self { client })
73 }
74
75 async fn execute(&self, command: &str) -> Result<String> {
87 debug!("Executing RNDC command: {}", command);
88
89 let start = Instant::now();
90 let command_name = command.split_whitespace().next().unwrap_or("unknown");
91
92 let result = tokio::task::spawn_blocking({
94 let client = self.client.clone();
95 let command = command.to_string();
96 move || client.rndc_command(&command)
97 })
98 .await
99 .with_context(|| {
100 format!(
101 "Failed to execute RNDC command '{}': task join error",
102 command_name
103 )
104 })?;
105
106 let duration = start.elapsed().as_secs_f64();
107
108 let rndc_result = result.map_err(|e| {
109 let error_msg = format!("RNDC command '{}' failed: {}", command_name, e);
110 error!("{}", error_msg);
111 metrics::record_rndc_command(command_name, false, duration);
112 anyhow::anyhow!("{}", error_msg)
113 })?;
114
115 if let Some(err) = &rndc_result.err {
117 let error_msg = format!("RNDC command '{}' failed: {}", command_name, err);
118 error!("{}", error_msg);
119 metrics::record_rndc_command(command_name, false, duration);
120 return Err(anyhow::anyhow!("{}", error_msg));
121 }
122
123 let response = rndc_result.text.unwrap_or_default();
125 debug!("RNDC command '{}' succeeded: {}", command_name, response);
126 metrics::record_rndc_command(command_name, true, duration);
127 Ok(response)
128 }
129
130 pub async fn status(&self) -> Result<String> {
132 self.execute("status").await
133 }
134
135 pub async fn addzone(&self, zone_name: &str, zone_config: &str) -> Result<String> {
141 let command = format!("addzone {} {}", zone_name, zone_config);
142 self.execute(&command).await
143 }
144
145 pub async fn delzone(&self, zone_name: &str) -> Result<String> {
147 let command = format!("delzone {}", zone_name);
148 self.execute(&command).await
149 }
150
151 pub async fn reload(&self, zone_name: &str) -> Result<String> {
153 let command = format!("reload {}", zone_name);
154 self.execute(&command).await
155 }
156
157 pub async fn zonestatus(&self, zone_name: &str) -> Result<String> {
159 let command = format!("zonestatus {}", zone_name);
160 self.execute(&command).await
161 }
162
163 pub async fn freeze(&self, zone_name: &str) -> Result<String> {
165 let command = format!("freeze {}", zone_name);
166 self.execute(&command).await
167 }
168
169 pub async fn thaw(&self, zone_name: &str) -> Result<String> {
171 let command = format!("thaw {}", zone_name);
172 self.execute(&command).await
173 }
174
175 pub async fn notify(&self, zone_name: &str) -> Result<String> {
177 let command = format!("notify {}", zone_name);
178 self.execute(&command).await
179 }
180
181 pub async fn retransfer(&self, zone_name: &str) -> Result<String> {
186 let command = format!("retransfer {}", zone_name);
187 self.execute(&command).await
188 }
189
190 pub async fn modzone(&self, zone_name: &str, zone_config: &str) -> Result<String> {
196 let command = format!("modzone {} {}", zone_name, zone_config);
197 self.execute(&command).await
198 }
199
200 pub async fn showzone(&self, zone_name: &str) -> Result<String> {
208 let command = format!("showzone {}", zone_name);
209 self.execute(&command).await
210 }
211}
212
213impl Clone for RndcExecutor {
214 fn clone(&self) -> Self {
215 Self {
216 client: self.client.clone(),
217 }
218 }
219}
220
221pub fn parse_rndc_conf(path: &str) -> Result<RndcConfig> {
232 use crate::rndc_conf_parser::parse_rndc_conf_file;
233 use std::path::Path;
234
235 info!("Parsing rndc.conf from {}", path);
236
237 let conf_file = parse_rndc_conf_file(Path::new(path))
239 .map_err(|e| anyhow::anyhow!("Failed to parse rndc.conf: {}", e))?;
240
241 let default_key_name = conf_file
243 .options
244 .default_key
245 .clone()
246 .or_else(|| conf_file.keys.keys().next().cloned());
247
248 let key_block = if let Some(ref key_name) = default_key_name {
249 conf_file
250 .keys
251 .get(key_name)
252 .ok_or_else(|| anyhow::anyhow!("Default key '{}' not found", key_name))?
253 } else {
254 return Err(anyhow::anyhow!("No keys found in configuration"));
255 };
256
257 let server = conf_file
259 .options
260 .default_server
261 .clone()
262 .unwrap_or_else(|| "127.0.0.1".to_string());
263
264 let server = if server.contains(':') {
266 server
267 } else {
268 let port = conf_file.options.default_port.unwrap_or(953);
269 format!("{}:{}", server, port)
270 };
271
272 info!(
273 "Parsed rndc configuration: server={}, algorithm={}, key={}",
274 server,
275 key_block.algorithm,
276 default_key_name.unwrap_or_else(|| "unnamed".to_string())
277 );
278
279 Ok(RndcConfig {
280 server,
281 algorithm: key_block.algorithm.clone(),
282 secret: key_block.secret.clone(),
283 })
284}