bindcar/
rndc_conf_parser.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! RNDC configuration file parser
5//!
6//! This module provides parsers for BIND9 rndc.conf files using nom.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use bindcar::rndc_conf_parser::parse_rndc_conf_str;
12//!
13//! let conf_str = r#"
14//! key "rndc-key" {
15//!     algorithm hmac-sha256;
16//!     secret "dGVzdC1zZWNyZXQ=";
17//! };
18//!
19//! options {
20//!     default-server localhost;
21//!     default-key "rndc-key";
22//! };
23//! "#;
24//!
25//! let config = parse_rndc_conf_str(conf_str).unwrap();
26//! assert_eq!(config.keys.len(), 1);
27//! ```
28
29use crate::rndc_conf_types::{KeyBlock, OptionsBlock, RndcConfFile, ServerAddress, ServerBlock};
30use nom::{
31    branch::alt,
32    bytes::complete::{tag, take_until, take_while, take_while1},
33    character::complete::{char, digit1, multispace0, multispace1},
34    combinator::{map, recognize, value},
35    multi::{many0, separated_list0},
36    sequence::{delimited, preceded},
37    IResult, Parser,
38};
39use std::collections::HashSet;
40use std::net::IpAddr;
41use std::path::{Path, PathBuf};
42use thiserror::Error;
43
44/// RNDC configuration parse errors
45#[derive(Debug, Error)]
46pub enum RndcConfParseError {
47    #[error("Parse error: {0}")]
48    ParseError(String),
49
50    #[error("Invalid server address: {0}")]
51    InvalidServerAddress(String),
52
53    #[error("Invalid IP address: {0}")]
54    InvalidIpAddress(String),
55
56    #[error("Missing required field: {0}")]
57    MissingField(String),
58
59    #[error("Circular include detected: {0}")]
60    CircularInclude(String),
61
62    #[error("File not found: {0}")]
63    FileNotFound(String),
64
65    #[error("IO error: {0}")]
66    IoError(#[from] std::io::Error),
67
68    #[error("Incomplete input")]
69    Incomplete,
70}
71
72pub type ParseResult<T> = Result<T, RndcConfParseError>;
73
74// ========== Comment and Whitespace Parsers ==========
75
76/// Parse C-style line comment: // comment
77fn line_comment(input: &str) -> IResult<&str, ()> {
78    let (input, _) = tag("//")(input)?;
79    let (input, _) = take_while(|c| c != '\n')(input)?;
80    Ok((input, ()))
81}
82
83/// Parse hash comment: # comment
84fn hash_comment(input: &str) -> IResult<&str, ()> {
85    let (input, _) = char('#')(input)?;
86    let (input, _) = take_while(|c| c != '\n')(input)?;
87    Ok((input, ()))
88}
89
90/// Parse C-style block comment: /* comment */
91fn block_comment(input: &str) -> IResult<&str, ()> {
92    value((), (tag("/*"), take_until("*/"), tag("*/"))).parse(input)
93}
94
95/// Parse any type of comment
96fn comment(input: &str) -> IResult<&str, ()> {
97    alt((line_comment, hash_comment, block_comment)).parse(input)
98}
99
100/// Skip whitespace and comments
101fn ws<'a, F, O>(inner: F) -> impl Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>
102where
103    F: Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>,
104{
105    delimited(
106        many0(alt((value((), multispace1), comment))),
107        inner,
108        many0(alt((value((), multispace1), comment))),
109    )
110}
111
112/// Parse a semicolon with surrounding whitespace
113fn semicolon(input: &str) -> IResult<&str, char> {
114    ws(char(';')).parse(input)
115}
116
117// ========== String and Identifier Parsers ==========
118
119/// Parse escaped character in quoted string
120fn escaped_char(input: &str) -> IResult<&str, char> {
121    preceded(
122        char('\\'),
123        alt((
124            value('"', char('"')),
125            value('\\', char('\\')),
126            value('\n', char('n')),
127            value('\r', char('r')),
128            value('\t', char('t')),
129        )),
130    )
131    .parse(input)
132}
133
134/// Parse quoted string with escape sequences: "example"
135fn quoted_string(input: &str) -> IResult<&str, String> {
136    delimited(
137        char('"'),
138        map(
139            many0(alt((
140                map(escaped_char, |c| c.to_string()),
141                map(take_while1(|c| c != '"' && c != '\\'), |s: &str| {
142                    s.to_string()
143                }),
144            ))),
145            |parts| parts.join(""),
146        ),
147        char('"'),
148    )
149    .parse(input)
150}
151
152/// Parse an identifier (alphanumeric with hyphens, underscores, dots, and colons)
153/// Examples: rndc-key, hmac-sha256, 127.0.0.1, localhost, 2001:db8::1
154fn identifier(input: &str) -> IResult<&str, &str> {
155    take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')(
156        input,
157    )
158}
159
160// ========== IP Address and Port Parsers ==========
161
162/// Parse an IPv4 address
163fn ipv4_addr(input: &str) -> IResult<&str, IpAddr> {
164    let (input, addr_str) = recognize((
165        digit1,
166        char('.'),
167        digit1,
168        char('.'),
169        digit1,
170        char('.'),
171        digit1,
172    ))
173    .parse(input)?;
174
175    let addr = match addr_str.parse::<IpAddr>() {
176        Ok(addr) => addr,
177        Err(_) => {
178            return Err(nom::Err::Error(nom::error::Error::new(
179                input,
180                nom::error::ErrorKind::Verify,
181            )))
182        }
183    };
184
185    Ok((input, addr))
186}
187
188/// Parse an IPv6 address
189fn ipv6_addr(input: &str) -> IResult<&str, IpAddr> {
190    let (input, addr_str) =
191        recognize(take_while1(|c: char| c.is_ascii_hexdigit() || c == ':')).parse(input)?;
192
193    // Must contain at least two colons to be valid IPv6
194    if !addr_str.contains("::") && addr_str.matches(':').count() < 2 {
195        return Err(nom::Err::Error(nom::error::Error::new(
196            input,
197            nom::error::ErrorKind::Verify,
198        )));
199    }
200
201    let addr = match addr_str.parse::<IpAddr>() {
202        Ok(addr) => addr,
203        Err(_) => {
204            return Err(nom::Err::Error(nom::error::Error::new(
205                input,
206                nom::error::ErrorKind::Verify,
207            )))
208        }
209    };
210
211    Ok((input, addr))
212}
213
214/// Parse an IP address (IPv4 or IPv6)
215fn ip_addr(input: &str) -> IResult<&str, IpAddr> {
216    alt((ipv6_addr, ipv4_addr)).parse(input)
217}
218
219/// Parse a port number
220fn port_number(input: &str) -> IResult<&str, u16> {
221    map(digit1, |s: &str| s.parse::<u16>().unwrap_or(953)).parse(input)
222}
223
224/// Parse a server address (hostname or IP)
225fn server_address(input: &str) -> IResult<&str, ServerAddress> {
226    // Try IP address first, then fall back to hostname
227    alt((
228        map(ip_addr, ServerAddress::IpAddr),
229        map(identifier, |s: &str| ServerAddress::Hostname(s.to_string())),
230    ))
231    .parse(input)
232}
233
234// ========== Key Block Parser ==========
235
236/// Key block field types
237#[derive(Debug)]
238enum KeyField {
239    Algorithm(String),
240    Secret(String),
241}
242
243/// Parse algorithm field: algorithm hmac-sha256;
244fn parse_algorithm_field(input: &str) -> IResult<&str, KeyField> {
245    let (input, _) = ws(tag("algorithm")).parse(input)?;
246    let (input, algo) = ws(identifier).parse(input)?;
247    let (input, _) = semicolon(input)?;
248    Ok((input, KeyField::Algorithm(algo.to_string())))
249}
250
251/// Parse secret field: secret "base64string";
252fn parse_secret_field(input: &str) -> IResult<&str, KeyField> {
253    let (input, _) = ws(tag("secret")).parse(input)?;
254    let (input, secret) = ws(quoted_string).parse(input)?;
255    let (input, _) = semicolon(input)?;
256    Ok((input, KeyField::Secret(secret)))
257}
258
259/// Parse key field
260fn parse_key_field(input: &str) -> IResult<&str, KeyField> {
261    alt((parse_algorithm_field, parse_secret_field)).parse(input)
262}
263
264/// Parse key block: key "name" { algorithm ...; secret "..."; };
265fn parse_key_block(input: &str) -> IResult<&str, (String, KeyBlock)> {
266    let (input, _) = ws(tag("key")).parse(input)?;
267    let (input, name) = ws(quoted_string).parse(input)?;
268    let (input, fields) =
269        delimited(ws(char('{')), many0(parse_key_field), ws(tag("};"))).parse(input)?;
270
271    let mut algorithm = None;
272    let mut secret = None;
273
274    for field in fields {
275        match field {
276            KeyField::Algorithm(a) => algorithm = Some(a),
277            KeyField::Secret(s) => secret = Some(s),
278        }
279    }
280
281    let key_block = KeyBlock {
282        name: name.clone(),
283        algorithm: algorithm.unwrap_or_else(|| "hmac-sha256".to_string()),
284        secret: secret.unwrap_or_default(),
285    };
286
287    Ok((input, (name, key_block)))
288}
289
290// ========== Server Block Parser ==========
291
292/// Server block field types
293#[derive(Debug)]
294enum ServerField {
295    Key(String),
296    Port(u16),
297    Addresses(Vec<IpAddr>),
298}
299
300/// Parse key field: key "keyname";
301fn parse_server_key_field(input: &str) -> IResult<&str, ServerField> {
302    let (input, _) = ws(tag("key")).parse(input)?;
303    let (input, key) = ws(quoted_string).parse(input)?;
304    let (input, _) = semicolon(input)?;
305    Ok((input, ServerField::Key(key)))
306}
307
308/// Parse port field: port 953;
309fn parse_server_port_field(input: &str) -> IResult<&str, ServerField> {
310    let (input, _) = ws(tag("port")).parse(input)?;
311    let (input, port) = ws(port_number).parse(input)?;
312    let (input, _) = semicolon(input)?;
313    Ok((input, ServerField::Port(port)))
314}
315
316/// Parse addresses field: addresses { ip; ip; };
317fn parse_server_addresses_field(input: &str) -> IResult<&str, ServerField> {
318    let (input, _) = ws(tag("addresses")).parse(input)?;
319    let (input, addrs) = delimited(
320        ws(char('{')),
321        separated_list0(semicolon, ws(ip_addr)),
322        ws(tag("};")),
323    )
324    .parse(input)?;
325    Ok((input, ServerField::Addresses(addrs)))
326}
327
328/// Parse server field
329fn parse_server_field(input: &str) -> IResult<&str, ServerField> {
330    alt((
331        parse_server_key_field,
332        parse_server_port_field,
333        parse_server_addresses_field,
334    ))
335    .parse(input)
336}
337
338/// Parse server block: server address { key "..."; port 953; };
339fn parse_server_block(input: &str) -> IResult<&str, (String, ServerBlock)> {
340    let (input, _) = ws(tag("server")).parse(input)?;
341    let (input, addr) = ws(server_address).parse(input)?;
342    let (input, fields) =
343        delimited(ws(char('{')), many0(parse_server_field), ws(tag("};"))).parse(input)?;
344
345    let mut server = ServerBlock::new(addr.clone());
346
347    for field in fields {
348        match field {
349            ServerField::Key(k) => server.key = Some(k),
350            ServerField::Port(p) => server.port = Some(p),
351            ServerField::Addresses(a) => server.addresses = Some(a),
352        }
353    }
354
355    Ok((input, (addr.to_string(), server)))
356}
357
358// ========== Options Block Parser ==========
359
360/// Options block field types
361#[derive(Debug)]
362#[allow(clippy::enum_variant_names)]
363enum OptionField {
364    DefaultServer(String),
365    DefaultKey(String),
366    DefaultPort(u16),
367}
368
369/// Parse default-server field: default-server localhost;
370fn parse_default_server_field(input: &str) -> IResult<&str, OptionField> {
371    let (input, _) = ws(tag("default-server")).parse(input)?;
372    let (input, server) = ws(identifier).parse(input)?;
373    let (input, _) = semicolon(input)?;
374    Ok((input, OptionField::DefaultServer(server.to_string())))
375}
376
377/// Parse default-key field: default-key "keyname";
378fn parse_default_key_field(input: &str) -> IResult<&str, OptionField> {
379    let (input, _) = ws(tag("default-key")).parse(input)?;
380    let (input, key) = ws(quoted_string).parse(input)?;
381    let (input, _) = semicolon(input)?;
382    Ok((input, OptionField::DefaultKey(key)))
383}
384
385/// Parse default-port field: default-port 953;
386fn parse_default_port_field(input: &str) -> IResult<&str, OptionField> {
387    let (input, _) = ws(tag("default-port")).parse(input)?;
388    let (input, port) = ws(port_number).parse(input)?;
389    let (input, _) = semicolon(input)?;
390    Ok((input, OptionField::DefaultPort(port)))
391}
392
393/// Parse options field
394fn parse_option_field(input: &str) -> IResult<&str, OptionField> {
395    alt((
396        parse_default_server_field,
397        parse_default_key_field,
398        parse_default_port_field,
399    ))
400    .parse(input)
401}
402
403/// Parse options block: options { default-server localhost; };
404fn parse_options_block(input: &str) -> IResult<&str, OptionsBlock> {
405    let (input, _) = ws(tag("options")).parse(input)?;
406    let (input, fields) =
407        delimited(ws(char('{')), many0(parse_option_field), ws(tag("};"))).parse(input)?;
408
409    let mut options = OptionsBlock::new();
410
411    for field in fields {
412        match field {
413            OptionField::DefaultServer(s) => options.default_server = Some(s),
414            OptionField::DefaultKey(k) => options.default_key = Some(k),
415            OptionField::DefaultPort(p) => options.default_port = Some(p),
416        }
417    }
418
419    Ok((input, options))
420}
421
422// ========== Include Statement Parser ==========
423
424/// Parse include statement: include "/path/to/file";
425fn parse_include_stmt(input: &str) -> IResult<&str, PathBuf> {
426    let (input, _) = ws(tag("include")).parse(input)?;
427    let (input, path) = ws(quoted_string).parse(input)?;
428    let (input, _) = semicolon(input)?;
429    Ok((input, PathBuf::from(path)))
430}
431
432// ========== Statement Parser ==========
433
434/// Statement types in rndc.conf
435#[derive(Debug)]
436enum Statement {
437    Include(PathBuf),
438    Key(String, KeyBlock),
439    Server(String, ServerBlock),
440    Options(OptionsBlock),
441}
442
443/// Parse any statement
444fn parse_statement(input: &str) -> IResult<&str, Statement> {
445    alt((
446        map(parse_include_stmt, Statement::Include),
447        map(parse_key_block, |(name, key)| Statement::Key(name, key)),
448        map(parse_server_block, |(addr, srv)| {
449            Statement::Server(addr, srv)
450        }),
451        map(parse_options_block, Statement::Options),
452    ))
453    .parse(input)
454}
455
456// ========== File Parser ==========
457
458/// Parse rndc.conf file content (internal)
459fn parse_rndc_conf_internal(input: &str) -> IResult<&str, RndcConfFile> {
460    let (input, statements) = many0(ws(parse_statement)).parse(input)?;
461    let (input, _) = multispace0(input)?;
462
463    let mut conf = RndcConfFile::new();
464
465    for stmt in statements {
466        match stmt {
467            Statement::Include(path) => conf.includes.push(path),
468            Statement::Key(name, key) => {
469                conf.keys.insert(name, key);
470            }
471            Statement::Server(addr, server) => {
472                conf.servers.insert(addr, server);
473            }
474            Statement::Options(opts) => {
475                // Merge options (last one wins)
476                if opts.default_server.is_some() {
477                    conf.options.default_server = opts.default_server;
478                }
479                if opts.default_key.is_some() {
480                    conf.options.default_key = opts.default_key;
481                }
482                if opts.default_port.is_some() {
483                    conf.options.default_port = opts.default_port;
484                }
485            }
486        }
487    }
488
489    Ok((input, conf))
490}
491
492/// Parse rndc.conf from string
493///
494/// # Examples
495///
496/// ```rust
497/// use bindcar::rndc_conf_parser::parse_rndc_conf_str;
498///
499/// let conf_str = r#"
500/// key "rndc-key" {
501///     algorithm hmac-sha256;
502///     secret "dGVzdC1zZWNyZXQ=";
503/// };
504/// "#;
505///
506/// let config = parse_rndc_conf_str(conf_str).unwrap();
507/// assert_eq!(config.keys.len(), 1);
508/// ```
509pub fn parse_rndc_conf_str(input: &str) -> ParseResult<RndcConfFile> {
510    match parse_rndc_conf_internal(input) {
511        Ok((_, conf)) => Ok(conf),
512        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
513            Err(RndcConfParseError::ParseError(format!("{:?}", e)))
514        }
515        Err(nom::Err::Incomplete(_)) => Err(RndcConfParseError::Incomplete),
516    }
517}
518
519/// Parse rndc.conf from file with include resolution
520///
521/// Handles include directives and detects circular includes.
522///
523/// # Examples
524///
525/// ```rust,no_run
526/// use bindcar::rndc_conf_parser::parse_rndc_conf_file;
527/// use std::path::Path;
528///
529/// let config = parse_rndc_conf_file(Path::new("/etc/bind/rndc.conf")).unwrap();
530/// ```
531pub fn parse_rndc_conf_file(path: &Path) -> ParseResult<RndcConfFile> {
532    let mut visited = HashSet::new();
533    parse_rndc_conf_file_recursive(path, &mut visited)
534}
535
536/// Recursively parse rndc.conf file with include resolution
537fn parse_rndc_conf_file_recursive(
538    path: &Path,
539    visited: &mut HashSet<PathBuf>,
540) -> ParseResult<RndcConfFile> {
541    // Check for circular includes
542    let canonical_path = path
543        .canonicalize()
544        .map_err(|_| RndcConfParseError::FileNotFound(path.display().to_string()))?;
545
546    if visited.contains(&canonical_path) {
547        return Err(RndcConfParseError::CircularInclude(
548            canonical_path.display().to_string(),
549        ));
550    }
551
552    visited.insert(canonical_path.clone());
553
554    // Read and parse main file
555    let content = std::fs::read_to_string(path)?;
556    let mut conf = parse_rndc_conf_str(&content)?;
557
558    // Resolve includes
559    let includes = conf.includes.clone();
560    conf.includes.clear();
561
562    for include_path in includes {
563        // Resolve relative paths
564        let resolved_path = if include_path.is_absolute() {
565            include_path
566        } else {
567            path.parent()
568                .unwrap_or_else(|| Path::new("."))
569                .join(include_path)
570        };
571
572        // Parse included file
573        let included_conf = parse_rndc_conf_file_recursive(&resolved_path, visited)?;
574
575        // Merge configurations
576        for (name, key) in included_conf.keys {
577            conf.keys.entry(name).or_insert(key);
578        }
579
580        for (addr, server) in included_conf.servers {
581            conf.servers.entry(addr).or_insert(server);
582        }
583
584        // Merge options (main file takes precedence)
585        if conf.options.default_server.is_none() {
586            conf.options.default_server = included_conf.options.default_server;
587        }
588        if conf.options.default_key.is_none() {
589            conf.options.default_key = included_conf.options.default_key;
590        }
591        if conf.options.default_port.is_none() {
592            conf.options.default_port = included_conf.options.default_port;
593        }
594
595        conf.includes.push(resolved_path);
596    }
597
598    Ok(conf)
599}
600
601#[cfg(test)]
602#[path = "rndc_conf_parser_tests.rs"]
603mod tests;