1use 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, map_res, 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#[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
74fn 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
83fn 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
90fn block_comment(input: &str) -> IResult<&str, ()> {
92 value((), (tag("/*"), take_until("*/"), tag("*/"))).parse(input)
93}
94
95fn comment(input: &str) -> IResult<&str, ()> {
97 alt((line_comment, hash_comment, block_comment)).parse(input)
98}
99
100fn 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
112fn semicolon(input: &str) -> IResult<&str, char> {
114 ws(char(';')).parse(input)
115}
116
117fn 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
134fn 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
152fn identifier(input: &str) -> IResult<&str, &str> {
155 take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')(
156 input,
157 )
158}
159
160fn 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
188fn 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 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
214fn ip_addr(input: &str) -> IResult<&str, IpAddr> {
216 alt((ipv6_addr, ipv4_addr)).parse(input)
217}
218
219fn port_number(input: &str) -> IResult<&str, u16> {
225 map_res(digit1, |s: &str| s.parse::<u16>()).parse(input)
226}
227
228fn server_address(input: &str) -> IResult<&str, ServerAddress> {
230 alt((
232 map(ip_addr, ServerAddress::IpAddr),
233 map(identifier, |s: &str| ServerAddress::Hostname(s.to_string())),
234 ))
235 .parse(input)
236}
237
238enum KeyField {
242 Algorithm(String),
243 Secret(String),
244}
245
246impl std::fmt::Debug for KeyField {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 match self {
251 KeyField::Algorithm(a) => f.debug_tuple("Algorithm").field(a).finish(),
252 KeyField::Secret(_) => f.debug_tuple("Secret").field(&"[REDACTED]").finish(),
253 }
254 }
255}
256
257fn parse_algorithm_field(input: &str) -> IResult<&str, KeyField> {
259 let (input, _) = ws(tag("algorithm")).parse(input)?;
260 let (input, algo) = ws(identifier).parse(input)?;
261 let (input, _) = semicolon(input)?;
262 Ok((input, KeyField::Algorithm(algo.to_string())))
263}
264
265fn parse_secret_field(input: &str) -> IResult<&str, KeyField> {
267 let (input, _) = ws(tag("secret")).parse(input)?;
268 let (input, secret) = ws(quoted_string).parse(input)?;
269 let (input, _) = semicolon(input)?;
270 Ok((input, KeyField::Secret(secret)))
271}
272
273fn parse_key_field(input: &str) -> IResult<&str, KeyField> {
275 alt((parse_algorithm_field, parse_secret_field)).parse(input)
276}
277
278fn parse_key_block(input: &str) -> IResult<&str, (String, KeyBlock)> {
280 let (input, _) = ws(tag("key")).parse(input)?;
281 let (input, name) = ws(quoted_string).parse(input)?;
282 let (input, fields) =
283 delimited(ws(char('{')), many0(parse_key_field), ws(tag("};"))).parse(input)?;
284
285 let mut algorithm = None;
286 let mut secret = None;
287
288 for field in fields {
289 match field {
290 KeyField::Algorithm(a) => algorithm = Some(a),
291 KeyField::Secret(s) => secret = Some(s),
292 }
293 }
294
295 let key_block = KeyBlock {
296 name: name.clone(),
297 algorithm: algorithm.unwrap_or_else(|| "hmac-sha256".to_string()),
298 secret: secret.unwrap_or_default(),
299 };
300
301 Ok((input, (name, key_block)))
302}
303
304#[derive(Debug)]
308enum ServerField {
309 Key(String),
310 Port(u16),
311 Addresses(Vec<IpAddr>),
312}
313
314fn parse_server_key_field(input: &str) -> IResult<&str, ServerField> {
316 let (input, _) = ws(tag("key")).parse(input)?;
317 let (input, key) = ws(quoted_string).parse(input)?;
318 let (input, _) = semicolon(input)?;
319 Ok((input, ServerField::Key(key)))
320}
321
322fn parse_server_port_field(input: &str) -> IResult<&str, ServerField> {
324 let (input, _) = ws(tag("port")).parse(input)?;
325 let (input, port) = ws(port_number).parse(input)?;
326 let (input, _) = semicolon(input)?;
327 Ok((input, ServerField::Port(port)))
328}
329
330fn parse_server_addresses_field(input: &str) -> IResult<&str, ServerField> {
332 let (input, _) = ws(tag("addresses")).parse(input)?;
333 let (input, addrs) = delimited(
334 ws(char('{')),
335 separated_list0(semicolon, ws(ip_addr)),
336 ws(tag("};")),
337 )
338 .parse(input)?;
339 Ok((input, ServerField::Addresses(addrs)))
340}
341
342fn parse_server_field(input: &str) -> IResult<&str, ServerField> {
344 alt((
345 parse_server_key_field,
346 parse_server_port_field,
347 parse_server_addresses_field,
348 ))
349 .parse(input)
350}
351
352fn parse_server_block(input: &str) -> IResult<&str, (String, ServerBlock)> {
354 let (input, _) = ws(tag("server")).parse(input)?;
355 let (input, addr) = ws(server_address).parse(input)?;
356 let (input, fields) =
357 delimited(ws(char('{')), many0(parse_server_field), ws(tag("};"))).parse(input)?;
358
359 let mut server = ServerBlock::new(addr.clone());
360
361 for field in fields {
362 match field {
363 ServerField::Key(k) => server.key = Some(k),
364 ServerField::Port(p) => server.port = Some(p),
365 ServerField::Addresses(a) => server.addresses = Some(a),
366 }
367 }
368
369 Ok((input, (addr.to_string(), server)))
370}
371
372#[derive(Debug)]
376#[allow(clippy::enum_variant_names)]
377enum OptionField {
378 DefaultServer(String),
379 DefaultKey(String),
380 DefaultPort(u16),
381}
382
383fn parse_default_server_field(input: &str) -> IResult<&str, OptionField> {
385 let (input, _) = ws(tag("default-server")).parse(input)?;
386 let (input, server) = ws(identifier).parse(input)?;
387 let (input, _) = semicolon(input)?;
388 Ok((input, OptionField::DefaultServer(server.to_string())))
389}
390
391fn parse_default_key_field(input: &str) -> IResult<&str, OptionField> {
393 let (input, _) = ws(tag("default-key")).parse(input)?;
394 let (input, key) = ws(quoted_string).parse(input)?;
395 let (input, _) = semicolon(input)?;
396 Ok((input, OptionField::DefaultKey(key)))
397}
398
399fn parse_default_port_field(input: &str) -> IResult<&str, OptionField> {
401 let (input, _) = ws(tag("default-port")).parse(input)?;
402 let (input, port) = ws(port_number).parse(input)?;
403 let (input, _) = semicolon(input)?;
404 Ok((input, OptionField::DefaultPort(port)))
405}
406
407fn parse_option_field(input: &str) -> IResult<&str, OptionField> {
409 alt((
410 parse_default_server_field,
411 parse_default_key_field,
412 parse_default_port_field,
413 ))
414 .parse(input)
415}
416
417fn parse_options_block(input: &str) -> IResult<&str, OptionsBlock> {
419 let (input, _) = ws(tag("options")).parse(input)?;
420 let (input, fields) =
421 delimited(ws(char('{')), many0(parse_option_field), ws(tag("};"))).parse(input)?;
422
423 let mut options = OptionsBlock::new();
424
425 for field in fields {
426 match field {
427 OptionField::DefaultServer(s) => options.default_server = Some(s),
428 OptionField::DefaultKey(k) => options.default_key = Some(k),
429 OptionField::DefaultPort(p) => options.default_port = Some(p),
430 }
431 }
432
433 Ok((input, options))
434}
435
436fn parse_include_stmt(input: &str) -> IResult<&str, PathBuf> {
440 let (input, _) = ws(tag("include")).parse(input)?;
441 let (input, path) = ws(quoted_string).parse(input)?;
442 let (input, _) = semicolon(input)?;
443 Ok((input, PathBuf::from(path)))
444}
445
446#[derive(Debug)]
450enum Statement {
451 Include(PathBuf),
452 Key(String, KeyBlock),
453 Server(String, ServerBlock),
454 Options(OptionsBlock),
455}
456
457fn parse_statement(input: &str) -> IResult<&str, Statement> {
459 alt((
460 map(parse_include_stmt, Statement::Include),
461 map(parse_key_block, |(name, key)| Statement::Key(name, key)),
462 map(parse_server_block, |(addr, srv)| {
463 Statement::Server(addr, srv)
464 }),
465 map(parse_options_block, Statement::Options),
466 ))
467 .parse(input)
468}
469
470fn parse_rndc_conf_internal(input: &str) -> IResult<&str, RndcConfFile> {
474 let (input, statements) = many0(ws(parse_statement)).parse(input)?;
475 let (input, _) = multispace0(input)?;
476
477 let mut conf = RndcConfFile::new();
478
479 for stmt in statements {
480 match stmt {
481 Statement::Include(path) => conf.includes.push(path),
482 Statement::Key(name, key) => {
483 conf.keys.insert(name, key);
484 }
485 Statement::Server(addr, server) => {
486 conf.servers.insert(addr, server);
487 }
488 Statement::Options(opts) => {
489 if opts.default_server.is_some() {
491 conf.options.default_server = opts.default_server;
492 }
493 if opts.default_key.is_some() {
494 conf.options.default_key = opts.default_key;
495 }
496 if opts.default_port.is_some() {
497 conf.options.default_port = opts.default_port;
498 }
499 }
500 }
501 }
502
503 Ok((input, conf))
504}
505
506pub fn parse_rndc_conf_str(input: &str) -> ParseResult<RndcConfFile> {
524 match parse_rndc_conf_internal(input) {
525 Ok((remaining, conf)) => {
526 if !remaining.trim().is_empty() {
533 let offset = input.len().saturating_sub(remaining.len());
534 return Err(RndcConfParseError::ParseError(format!(
535 "malformed rndc.conf: unparsed input near byte {}",
536 offset
537 )));
538 }
539 Ok(conf)
540 }
541 Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
542 let offset = input.len().saturating_sub(e.input.len());
547 Err(RndcConfParseError::ParseError(format!(
548 "malformed rndc.conf near byte {} ({:?})",
549 offset, e.code
550 )))
551 }
552 Err(nom::Err::Incomplete(_)) => Err(RndcConfParseError::Incomplete),
553 }
554}
555
556pub fn parse_rndc_conf_file(path: &Path) -> ParseResult<RndcConfFile> {
569 let mut visited = HashSet::new();
570 parse_rndc_conf_file_recursive(path, &mut visited, 0)
571}
572
573const MAX_INCLUDE_DEPTH: usize = 32;
579
580fn parse_rndc_conf_file_recursive(
582 path: &Path,
583 visited: &mut HashSet<PathBuf>,
584 depth: usize,
585) -> ParseResult<RndcConfFile> {
586 if depth > MAX_INCLUDE_DEPTH {
587 return Err(RndcConfParseError::ParseError(format!(
588 "include nesting exceeds maximum depth of {}",
589 MAX_INCLUDE_DEPTH
590 )));
591 }
592
593 let canonical_path = path
595 .canonicalize()
596 .map_err(|_| RndcConfParseError::FileNotFound(path.display().to_string()))?;
597
598 if visited.contains(&canonical_path) {
599 return Err(RndcConfParseError::CircularInclude(
600 canonical_path.display().to_string(),
601 ));
602 }
603
604 visited.insert(canonical_path.clone());
605
606 let content = std::fs::read_to_string(path)?;
608 let mut conf = parse_rndc_conf_str(&content)?;
609
610 let includes = conf.includes.clone();
612 conf.includes.clear();
613
614 for include_path in includes {
615 let resolved_path = if include_path.is_absolute() {
617 include_path
618 } else {
619 path.parent()
620 .unwrap_or_else(|| Path::new("."))
621 .join(include_path)
622 };
623
624 let included_conf = parse_rndc_conf_file_recursive(&resolved_path, visited, depth + 1)?;
626
627 for (name, key) in included_conf.keys {
629 conf.keys.entry(name).or_insert(key);
630 }
631
632 for (addr, server) in included_conf.servers {
633 conf.servers.entry(addr).or_insert(server);
634 }
635
636 if conf.options.default_server.is_none() {
638 conf.options.default_server = included_conf.options.default_server;
639 }
640 if conf.options.default_key.is_none() {
641 conf.options.default_key = included_conf.options.default_key;
642 }
643 if conf.options.default_port.is_none() {
644 conf.options.default_port = included_conf.options.default_port;
645 }
646
647 conf.includes.push(resolved_path);
648 }
649
650 Ok(conf)
651}
652
653#[cfg(test)]
654#[path = "rndc_conf_parser_tests.rs"]
655mod tests;