bindcar/
rndc.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! RNDC command execution using native RNDC protocol
5//!
6//! This module communicates with BIND9 using the RNDC protocol directly,
7//! rather than shelling out to the rndc binary. This provides:
8//! - Better error handling with structured responses
9//! - No subprocess overhead
10//! - Native async support
11//! - Direct access to error messages from BIND9
12
13use anyhow::{Context, Result};
14use rndc::RndcClient;
15use std::time::Instant;
16use tracing::{debug, error, info};
17
18use crate::metrics;
19
20/// RNDC configuration parsed from rndc.conf
21#[derive(Debug, Clone)]
22pub struct RndcConfig {
23    pub server: String,
24    pub algorithm: String,
25    pub secret: String,
26}
27
28/// RNDC command executor using native protocol
29pub struct RndcExecutor {
30    client: RndcClient,
31}
32
33impl RndcExecutor {
34    /// Create a new RNDC executor
35    ///
36    /// # Arguments
37    /// * `server` - RNDC server address (e.g., "127.0.0.1:953")
38    /// * `algorithm` - HMAC algorithm, accepts both formats:
39    ///   - With prefix: "hmac-md5", "hmac-sha1", "hmac-sha224", "hmac-sha256", "hmac-sha384", "hmac-sha512"
40    ///   - Without prefix: "md5", "sha1", "sha224", "sha256", "sha384", "sha512"
41    /// * `secret` - Base64-encoded RNDC secret key
42    ///
43    /// # Returns
44    /// A new RndcExecutor instance
45    pub fn new(server: String, algorithm: String, secret: String) -> Result<Self> {
46        // Trim whitespace from all parameters to handle environment variable issues
47        let server = server.trim();
48        let mut algorithm = algorithm.trim().to_string();
49        let secret = secret.trim();
50
51        // The rndc crate v0.1.3 only accepts algorithms WITHOUT the "hmac-" prefix
52        // Strip it if present (rndc.conf files typically use "hmac-sha256" format)
53        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        // Validate algorithm - rndc crate v0.1.3 only supports these values
60        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    /// Execute an RNDC command
76    ///
77    /// # Arguments
78    /// * `command` - Command string (e.g., "status", "reload example.com")
79    ///
80    /// # Returns
81    /// The response text from RNDC on success
82    ///
83    /// # Errors
84    /// Returns an error if the RNDC command fails, including detailed error
85    /// information from the BIND9 server
86    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        // Execute RNDC command using native protocol
93        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        // Check if the RNDC result contains an error
116        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        // Success - return the text response
124        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    /// Get server status
131    pub async fn status(&self) -> Result<String> {
132        self.execute("status").await
133    }
134
135    /// Add a zone
136    ///
137    /// # Arguments
138    /// * `zone_name` - Name of the zone (e.g., "example.com")
139    /// * `zone_config` - Zone configuration block (e.g., "{ type primary; file \"/var/cache/bind/example.com.zone\"; }")
140    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    /// Delete a zone
146    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    /// Reload a zone
152    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    /// Get zone status
158    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    /// Freeze a zone (disable dynamic updates)
164    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    /// Thaw a zone (enable dynamic updates)
170    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    /// Notify secondaries about zone changes
176    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    /// Force a zone retransfer from primary
182    ///
183    /// This command is used on secondary zones to discard the current zone data
184    /// and initiate a fresh transfer from the primary server.
185    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    /// Modify a zone configuration
191    ///
192    /// # Arguments
193    /// * `zone_name` - Name of the zone (e.g., "example.com")
194    /// * `zone_config` - Zone configuration block (e.g., "{ also-notify { 10.0.0.1; }; allow-transfer { 10.0.0.2; }; }")
195    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    /// Show zone configuration
201    ///
202    /// # Arguments
203    /// * `zone_name` - Name of the zone (e.g., "example.com")
204    ///
205    /// # Returns
206    /// The zone configuration in BIND9 format
207    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
221/// Parse RNDC configuration from rndc.conf file
222///
223/// # Arguments
224/// * `path` - Path to rndc.conf file (typically /etc/bind/rndc.conf or /etc/rndc.conf)
225///
226/// # Returns
227/// RndcConfig with server, algorithm, and secret
228///
229/// # Errors
230/// Returns an error if the file cannot be read or parsed
231pub 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    // Use new parser
238    let conf_file = parse_rndc_conf_file(Path::new(path))
239        .map_err(|e| anyhow::anyhow!("Failed to parse rndc.conf: {}", e))?;
240
241    // Extract default key (from options.default_key or first key)
242    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    // Extract server address from options or use default
258    let server = conf_file
259        .options
260        .default_server
261        .clone()
262        .unwrap_or_else(|| "127.0.0.1".to_string());
263
264    // Add port if not present
265    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}