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(Debug, Clone, PartialEq, Eq)]
105pub struct KeyBlock {
106    pub name: String,
107    pub algorithm: String,
108    pub secret: String,
109}
110
111impl KeyBlock {
112    /// Create a new key block
113    pub fn new(name: String, algorithm: String, secret: String) -> Self {
114        Self {
115            name,
116            algorithm,
117            secret,
118        }
119    }
120
121    /// Serialize to RNDC-compatible key block
122    ///
123    /// Returns the configuration in the format:
124    /// ```text
125    /// {
126    ///     algorithm hmac-sha256;
127    ///     secret "dGVzdC1zZWNyZXQ=";
128    /// };
129    /// ```
130    pub fn to_conf_block(&self) -> String {
131        format!(
132            "{{\n    algorithm {};\n    secret \"{}\";\n}};",
133            self.algorithm, self.secret
134        )
135    }
136}
137
138/// Server block: server-specific configuration
139#[derive(Debug, Clone, PartialEq)]
140pub struct ServerBlock {
141    pub address: ServerAddress,
142    pub key: Option<String>,
143    pub port: Option<u16>,
144    pub addresses: Option<Vec<IpAddr>>,
145}
146
147impl ServerBlock {
148    /// Create a new server block
149    pub fn new(address: ServerAddress) -> Self {
150        Self {
151            address,
152            key: None,
153            port: None,
154            addresses: None,
155        }
156    }
157
158    /// Serialize to RNDC-compatible server block
159    ///
160    /// Returns the configuration in the format:
161    /// ```text
162    /// {
163    ///     key "keyname";
164    ///     port 953;
165    /// };
166    /// ```
167    pub fn to_conf_block(&self) -> String {
168        let mut parts = Vec::new();
169
170        if let Some(ref key) = self.key {
171            parts.push(format!("    key \"{}\";", key));
172        }
173
174        if let Some(port) = self.port {
175            parts.push(format!("    port {};", port));
176        }
177
178        if let Some(ref addrs) = self.addresses {
179            let addr_list = addrs
180                .iter()
181                .map(|ip| format!("        {};", ip))
182                .collect::<Vec<_>>()
183                .join("\n");
184            parts.push(format!("    addresses {{\n{}\n    }};", addr_list));
185        }
186
187        if parts.is_empty() {
188            "{ };".to_string()
189        } else {
190            format!("{{\n{}\n}};", parts.join("\n"))
191        }
192    }
193}
194
195/// Server address: hostname or IP address
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum ServerAddress {
198    Hostname(String),
199    IpAddr(IpAddr),
200}
201
202impl ServerAddress {
203    /// Parse a server address from a string
204    pub fn parse(s: &str) -> Self {
205        match s.parse::<IpAddr>() {
206            Ok(addr) => ServerAddress::IpAddr(addr),
207            Err(_) => ServerAddress::Hostname(s.to_string()),
208        }
209    }
210}
211
212impl std::fmt::Display for ServerAddress {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        match self {
215            ServerAddress::Hostname(h) => write!(f, "{}", h),
216            ServerAddress::IpAddr(ip) => write!(f, "{}", ip),
217        }
218    }
219}
220
221/// Options block: global configuration
222#[derive(Debug, Clone, Default, PartialEq, Eq)]
223pub struct OptionsBlock {
224    pub default_server: Option<String>,
225    pub default_key: Option<String>,
226    pub default_port: Option<u16>,
227}
228
229impl OptionsBlock {
230    /// Create a new options block
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Check if the options block is empty
236    pub fn is_empty(&self) -> bool {
237        self.default_server.is_none() && self.default_key.is_none() && self.default_port.is_none()
238    }
239
240    /// Serialize to RNDC-compatible options block
241    ///
242    /// Returns the configuration in the format:
243    /// ```text
244    /// {
245    ///     default-server localhost;
246    ///     default-key "rndc-key";
247    ///     default-port 953;
248    /// };
249    /// ```
250    pub fn to_conf_block(&self) -> String {
251        let mut parts = Vec::new();
252
253        if let Some(ref server) = self.default_server {
254            parts.push(format!("    default-server {};", server));
255        }
256
257        if let Some(ref key) = self.default_key {
258            parts.push(format!("    default-key \"{}\";", key));
259        }
260
261        if let Some(port) = self.default_port {
262            parts.push(format!("    default-port {};", port));
263        }
264
265        if parts.is_empty() {
266            "{ };".to_string()
267        } else {
268            format!("{{\n{}\n}};", parts.join("\n"))
269        }
270    }
271}
272
273#[cfg(test)]
274#[path = "rndc_conf_types_tests.rs"]
275mod tests;