bindcar/
rndc_conf_types.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! RNDC configuration data types
5//!
6//! This module defines the data structures for representing BIND9 rndc.conf files.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use bindcar::rndc_conf_types::{RndcConfFile, KeyBlock, OptionsBlock};
12//!
13//! let mut conf = RndcConfFile::new();
14//! conf.keys.insert(
15//!     "rndc-key".to_string(),
16//!     KeyBlock::new(
17//!         "rndc-key".to_string(),
18//!         "hmac-sha256".to_string(),
19//!         "dGVzdC1zZWNyZXQ=".to_string(),
20//!     ),
21//! );
22//! conf.options.default_key = Some("rndc-key".to_string());
23//!
24//! let serialized = conf.to_conf_file();
25//! ```
26
27use std::collections::HashMap;
28use std::net::IpAddr;
29use std::path::PathBuf;
30
31/// Complete RNDC configuration file
32#[derive(Debug, Clone, PartialEq)]
33pub struct RndcConfFile {
34    /// Named key blocks
35    pub keys: HashMap<String, KeyBlock>,
36
37    /// Server blocks indexed by address
38    pub servers: HashMap<String, ServerBlock>,
39
40    /// Global options
41    pub options: OptionsBlock,
42
43    /// Included files (resolved paths)
44    pub includes: Vec<PathBuf>,
45}
46
47impl RndcConfFile {
48    /// Create a new empty RNDC configuration
49    pub fn new() -> Self {
50        Self {
51            keys: HashMap::new(),
52            servers: HashMap::new(),
53            options: OptionsBlock::default(),
54            includes: Vec::new(),
55        }
56    }
57
58    /// Get the default key (from options.default_key)
59    pub fn get_default_key(&self) -> Option<&KeyBlock> {
60        let key_name = self.options.default_key.as_ref()?;
61        self.keys.get(key_name)
62    }
63
64    /// Get the default server address (from options.default_server)
65    pub fn get_default_server(&self) -> Option<String> {
66        self.options.default_server.clone()
67    }
68
69    /// Serialize to rndc.conf format
70    pub fn to_conf_file(&self) -> String {
71        let mut output = String::new();
72
73        // Write includes
74        for include_path in &self.includes {
75            output.push_str(&format!("include \"{}\";\n", include_path.display()));
76        }
77
78        // Write keys
79        for (name, key) in &self.keys {
80            output.push_str(&format!("\nkey \"{}\" {}\n", name, key.to_conf_block()));
81        }
82
83        // Write servers
84        for (addr, server) in &self.servers {
85            output.push_str(&format!("\nserver {} {}\n", addr, server.to_conf_block()));
86        }
87
88        // Write options
89        if !self.options.is_empty() {
90            output.push_str(&format!("\noptions {}\n", self.options.to_conf_block()));
91        }
92
93        output
94    }
95}
96
97impl Default for RndcConfFile {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103/// Key block: authentication credentials
104#[derive(Clone, PartialEq, Eq)]
105pub struct KeyBlock {
106    pub name: String,
107    pub algorithm: String,
108    pub secret: String,
109}
110
111// Manual `Debug` that redacts the TSIG secret (A4). Because `RndcConfFile` derives
112// `Debug` and holds `KeyBlock`s, this also keeps the secret out of any debug print
113// of the whole parsed config.
114impl std::fmt::Debug for KeyBlock {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("KeyBlock")
117            .field("name", &self.name)
118            .field("algorithm", &self.algorithm)
119            .field("secret", &"[REDACTED]")
120            .finish()
121    }
122}
123
124impl KeyBlock {
125    /// Create a new key block
126    pub fn new(name: String, algorithm: String, secret: String) -> Self {
127        Self {
128            name,
129            algorithm,
130            secret,
131        }
132    }
133
134    /// Serialize to RNDC-compatible key block
135    ///
136    /// Returns the configuration in the format:
137    /// ```text
138    /// {
139    ///     algorithm hmac-sha256;
140    ///     secret "dGVzdC1zZWNyZXQ=";
141    /// };
142    /// ```
143    pub fn to_conf_block(&self) -> String {
144        format!(
145            "{{\n    algorithm {};\n    secret \"{}\";\n}};",
146            self.algorithm, self.secret
147        )
148    }
149}
150
151/// Server block: server-specific configuration
152#[derive(Debug, Clone, PartialEq)]
153pub struct ServerBlock {
154    pub address: ServerAddress,
155    pub key: Option<String>,
156    pub port: Option<u16>,
157    pub addresses: Option<Vec<IpAddr>>,
158}
159
160impl ServerBlock {
161    /// Create a new server block
162    pub fn new(address: ServerAddress) -> Self {
163        Self {
164            address,
165            key: None,
166            port: None,
167            addresses: None,
168        }
169    }
170
171    /// Serialize to RNDC-compatible server block
172    ///
173    /// Returns the configuration in the format:
174    /// ```text
175    /// {
176    ///     key "keyname";
177    ///     port 953;
178    /// };
179    /// ```
180    pub fn to_conf_block(&self) -> String {
181        let mut parts = Vec::new();
182
183        if let Some(ref key) = self.key {
184            parts.push(format!("    key \"{}\";", key));
185        }
186
187        if let Some(port) = self.port {
188            parts.push(format!("    port {};", port));
189        }
190
191        if let Some(ref addrs) = self.addresses {
192            let addr_list = addrs
193                .iter()
194                .map(|ip| format!("        {};", ip))
195                .collect::<Vec<_>>()
196                .join("\n");
197            parts.push(format!("    addresses {{\n{}\n    }};", addr_list));
198        }
199
200        if parts.is_empty() {
201            "{ };".to_string()
202        } else {
203            format!("{{\n{}\n}};", parts.join("\n"))
204        }
205    }
206}
207
208/// Server address: hostname or IP address
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub enum ServerAddress {
211    Hostname(String),
212    IpAddr(IpAddr),
213}
214
215impl ServerAddress {
216    /// Parse a server address from a string
217    pub fn parse(s: &str) -> Self {
218        match s.parse::<IpAddr>() {
219            Ok(addr) => ServerAddress::IpAddr(addr),
220            Err(_) => ServerAddress::Hostname(s.to_string()),
221        }
222    }
223}
224
225impl std::fmt::Display for ServerAddress {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        match self {
228            ServerAddress::Hostname(h) => write!(f, "{}", h),
229            ServerAddress::IpAddr(ip) => write!(f, "{}", ip),
230        }
231    }
232}
233
234/// Options block: global configuration
235#[derive(Debug, Clone, Default, PartialEq, Eq)]
236pub struct OptionsBlock {
237    pub default_server: Option<String>,
238    pub default_key: Option<String>,
239    pub default_port: Option<u16>,
240}
241
242impl OptionsBlock {
243    /// Create a new options block
244    pub fn new() -> Self {
245        Self::default()
246    }
247
248    /// Check if the options block is empty
249    pub fn is_empty(&self) -> bool {
250        self.default_server.is_none() && self.default_key.is_none() && self.default_port.is_none()
251    }
252
253    /// Serialize to RNDC-compatible options block
254    ///
255    /// Returns the configuration in the format:
256    /// ```text
257    /// {
258    ///     default-server localhost;
259    ///     default-key "rndc-key";
260    ///     default-port 953;
261    /// };
262    /// ```
263    pub fn to_conf_block(&self) -> String {
264        let mut parts = Vec::new();
265
266        if let Some(ref server) = self.default_server {
267            parts.push(format!("    default-server {};", server));
268        }
269
270        if let Some(ref key) = self.default_key {
271            parts.push(format!("    default-key \"{}\";", key));
272        }
273
274        if let Some(port) = self.default_port {
275            parts.push(format!("    default-port {};", port));
276        }
277
278        if parts.is_empty() {
279            "{ };".to_string()
280        } else {
281            format!("{{\n{}\n}};", parts.join("\n"))
282        }
283    }
284}
285
286#[cfg(test)]
287#[path = "rndc_conf_types_tests.rs"]
288mod tests;