use crate::rndc_types::{
AutoDnssecMode, CheckNamesMode, DnsClass, ForwardMode, ForwarderSpec, MasterfileFormat,
NotifyMode, PrimarySpec, ZoneConfig, ZoneType,
};
use nom::{
branch::alt,
bytes::complete::{tag, take_until, take_while1},
character::complete::{char, multispace0},
combinator::{map, opt, recognize},
multi::many0,
sequence::{delimited, preceded, terminated},
IResult, Parser,
};
use std::net::IpAddr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RndcParseError {
#[error("Parse error: {0}")]
ParseError(String),
#[error("Invalid zone type: {0}")]
InvalidZoneType(String),
#[error("Invalid DNS class: {0}")]
InvalidDnsClass(String),
#[error("Invalid IP address: {0}")]
InvalidIpAddress(String),
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Incomplete input")]
Incomplete,
}
pub type ParseResult<T> = Result<T, RndcParseError>;
fn ws<'a, F, O>(inner: F) -> impl Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>
where
F: Parser<&'a str, Output = O, Error = nom::error::Error<&'a str>>,
{
delimited(multispace0, inner, multispace0)
}
fn semicolon(input: &str) -> IResult<&str, char> {
ws(char(';')).parse(input)
}
pub(crate) fn quoted_string(input: &str) -> IResult<&str, String> {
let (input, content) = delimited(char('"'), take_until("\""), char('"')).parse(input)?;
Ok((input, content.to_string()))
}
fn identifier(input: &str) -> IResult<&str, &str> {
take_while1(|c: char| c.is_alphanumeric() || c == '_' || c == '-')(input)
}
pub(crate) fn ip_addr(input: &str) -> IResult<&str, IpAddr> {
let (input, addr_str) = recognize(take_while1(|c: char| {
c.is_ascii_hexdigit() || c == '.' || c == ':'
}))
.parse(input)?;
let addr = match addr_str.parse::<IpAddr>() {
Ok(addr) => addr,
Err(_) => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Verify,
)))
}
};
let (input, _) =
opt(preceded(char('/'), take_while1(|c: char| c.is_numeric()))).parse(input)?;
Ok((input, addr))
}
pub(crate) fn ip_with_port(input: &str) -> IResult<&str, PrimarySpec> {
let (input, addr) = ws(ip_addr).parse(input)?;
let (input, port) = opt(preceded(
ws(tag("port")),
map(take_while1(|c: char| c.is_numeric()), |s: &str| {
s.parse::<u16>().ok()
}),
))
.parse(input)?;
Ok((
input,
PrimarySpec {
address: addr,
port: port.flatten(),
},
))
}
fn ip_list(input: &str) -> IResult<&str, Vec<IpAddr>> {
delimited(
ws(char('{')),
many0(terminated(ws(ip_addr), semicolon)),
ws(char('}')),
)
.parse(input)
}
fn primary_list(input: &str) -> IResult<&str, Vec<PrimarySpec>> {
delimited(
ws(char('{')),
many0(terminated(ip_with_port, semicolon)),
ws(char('}')),
)
.parse(input)
}
#[derive(Debug)]
#[allow(dead_code)]
enum ZoneStatement {
Type(ZoneType),
File(String),
Primaries(Vec<PrimarySpec>),
AlsoNotify(Vec<IpAddr>),
Notify(NotifyMode),
AllowQuery(Vec<IpAddr>),
AllowTransfer(Vec<IpAddr>),
AllowUpdate(Vec<IpAddr>),
AllowUpdateRaw(String),
AllowUpdateForwarding(Vec<IpAddr>),
AllowNotify(Vec<IpAddr>),
MaxTransferTimeIn(u32),
MaxTransferTimeOut(u32),
MaxTransferIdleIn(u32),
MaxTransferIdleOut(u32),
TransferSource(IpAddr),
TransferSourceV6(IpAddr),
NotifySource(IpAddr),
NotifySourceV6(IpAddr),
UpdatePolicy(String),
Journal(String),
IxfrFromDifferences(bool),
InlineSigning(bool),
AutoDnssec(AutoDnssecMode),
KeyDirectory(String),
SigValidityInterval(u32),
DnskeySigValidity(u32),
Forward(ForwardMode),
Forwarders(Vec<ForwarderSpec>),
CheckNames(CheckNamesMode),
CheckMx(CheckNamesMode),
CheckIntegrity(bool),
MasterfileFormat(MasterfileFormat),
MaxZoneTtl(u32),
MaxRefreshTime(u32),
MinRefreshTime(u32),
MaxRetryTime(u32),
MinRetryTime(u32),
MultiMaster(bool),
RequestIxfr(bool),
RequestExpire(bool),
Unknown(String, String), }
fn parse_type_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, _) = ws(tag("type")).parse(input)?;
let (input, type_str) = ws(identifier).parse(input)?;
let (input, _) = semicolon(input)?;
let zone_type = ZoneType::parse(type_str).ok_or_else(|| {
nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
})?;
Ok((input, ZoneStatement::Type(zone_type)))
}
fn parse_file_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, _) = ws(tag("file")).parse(input)?;
let (input, file) = ws(quoted_string).parse(input)?;
let (input, _) = semicolon(input)?;
Ok((input, ZoneStatement::File(file)))
}
fn parse_primaries_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, _) = ws(alt((tag("primaries"), tag("masters")))).parse(input)?;
let (input, primaries) = primary_list(input)?;
let (input, _) = semicolon(input)?;
Ok((input, ZoneStatement::Primaries(primaries)))
}
fn parse_also_notify_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, _) = ws(tag("also-notify")).parse(input)?;
let (input, addrs) = ip_list(input)?;
let (input, _) = semicolon(input)?;
Ok((input, ZoneStatement::AlsoNotify(addrs)))
}
fn parse_allow_transfer_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, _) = ws(tag("allow-transfer")).parse(input)?;
let (input, addrs) = ip_list(input)?;
let (input, _) = semicolon(input)?;
Ok((input, ZoneStatement::AllowTransfer(addrs)))
}
fn parse_allow_update_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, _) = ws(tag("allow-update")).parse(input)?;
let start_input = input;
let (input, _) = ws(char('{')).parse(input)?;
let mut addrs = Vec::new();
let mut has_key_ref = false;
let mut remaining = input;
loop {
let (input, _) = multispace0(remaining)?;
if let Ok((input, _)) = char::<_, nom::error::Error<&str>>('}')(input) {
remaining = input;
break;
}
if let Ok((input, _)) = ws(tag("key")).parse(input) {
has_key_ref = true;
let (input, _) = take_until(";")(input)?;
let (input, _) = char(';')(input)?;
remaining = input;
} else if let Ok((input, addr)) = ip_addr(input) {
addrs.push(addr);
let (input, _) = semicolon(input)?;
remaining = input;
} else {
let (input, _) = take_until(";")(input)?;
let (input, _) = char(';')(input)?;
remaining = input;
}
}
let (input, _) = semicolon(remaining)?;
if has_key_ref {
let raw_len = start_input.len() - input.len();
let raw_content = &start_input[..raw_len];
Ok((
input,
ZoneStatement::AllowUpdateRaw(raw_content.to_string()),
))
} else {
Ok((input, ZoneStatement::AllowUpdate(addrs)))
}
}
fn parse_unknown_statement(input: &str) -> IResult<&str, ZoneStatement> {
let (input, option_name) = ws(identifier).parse(input)?;
let start_input = input;
let (input, _value) = alt((
delimited(ws(char('{')), take_until("}"), ws(char('}'))),
take_until(";"),
))
.parse(input)?;
let value_len = start_input.len() - input.len();
let raw_value = start_input[..value_len].trim().to_string();
let (input, _) = semicolon(input)?;
Ok((
input,
ZoneStatement::Unknown(option_name.to_string(), raw_value),
))
}
fn parse_zone_statement(input: &str) -> IResult<&str, ZoneStatement> {
alt((
parse_type_statement,
parse_file_statement,
parse_primaries_statement,
parse_also_notify_statement,
parse_allow_transfer_statement,
parse_allow_update_statement,
parse_unknown_statement,
))
.parse(input)
}
fn parse_zone_config_internal(input: &str) -> IResult<&str, ZoneConfig> {
let (input, _) = ws(tag("zone")).parse(input)?;
let (input, zone_name) = ws(quoted_string).parse(input)?;
let (input, class) = opt(ws(alt((tag("IN"), tag("CH"), tag("HS"))))).parse(input)?;
let class = match class {
Some("IN") => DnsClass::IN,
Some("CH") => DnsClass::CH,
Some("HS") => DnsClass::HS,
_ => DnsClass::IN, };
let (input, statements) =
delimited(ws(char('{')), many0(parse_zone_statement), ws(tag("};"))).parse(input)?;
let mut config = ZoneConfig::new(zone_name, ZoneType::Primary); config.class = class;
for stmt in statements {
match stmt {
ZoneStatement::Type(t) => config.zone_type = t,
ZoneStatement::File(f) => config.file = Some(f),
ZoneStatement::Primaries(p) => config.primaries = Some(p),
ZoneStatement::AlsoNotify(a) => config.also_notify = Some(a),
ZoneStatement::Notify(n) => config.notify = Some(n),
ZoneStatement::AllowQuery(a) => config.allow_query = Some(a),
ZoneStatement::AllowTransfer(a) => config.allow_transfer = Some(a),
ZoneStatement::AllowUpdate(a) => config.allow_update = Some(a),
ZoneStatement::AllowUpdateRaw(raw) => config.allow_update_raw = Some(raw),
ZoneStatement::AllowUpdateForwarding(a) => config.allow_update_forwarding = Some(a),
ZoneStatement::AllowNotify(a) => config.allow_notify = Some(a),
ZoneStatement::MaxTransferTimeIn(v) => config.max_transfer_time_in = Some(v),
ZoneStatement::MaxTransferTimeOut(v) => config.max_transfer_time_out = Some(v),
ZoneStatement::MaxTransferIdleIn(v) => config.max_transfer_idle_in = Some(v),
ZoneStatement::MaxTransferIdleOut(v) => config.max_transfer_idle_out = Some(v),
ZoneStatement::TransferSource(ip) => config.transfer_source = Some(ip),
ZoneStatement::TransferSourceV6(ip) => config.transfer_source_v6 = Some(ip),
ZoneStatement::NotifySource(ip) => config.notify_source = Some(ip),
ZoneStatement::NotifySourceV6(ip) => config.notify_source_v6 = Some(ip),
ZoneStatement::UpdatePolicy(p) => config.update_policy = Some(p),
ZoneStatement::Journal(j) => config.journal = Some(j),
ZoneStatement::IxfrFromDifferences(v) => config.ixfr_from_differences = Some(v),
ZoneStatement::InlineSigning(v) => config.inline_signing = Some(v),
ZoneStatement::AutoDnssec(m) => config.auto_dnssec = Some(m),
ZoneStatement::KeyDirectory(d) => config.key_directory = Some(d),
ZoneStatement::SigValidityInterval(v) => config.sig_validity_interval = Some(v),
ZoneStatement::DnskeySigValidity(v) => config.dnskey_sig_validity = Some(v),
ZoneStatement::Forward(m) => config.forward = Some(m),
ZoneStatement::Forwarders(f) => config.forwarders = Some(f),
ZoneStatement::CheckNames(m) => config.check_names = Some(m),
ZoneStatement::CheckMx(m) => config.check_mx = Some(m),
ZoneStatement::CheckIntegrity(v) => config.check_integrity = Some(v),
ZoneStatement::MasterfileFormat(f) => config.masterfile_format = Some(f),
ZoneStatement::MaxZoneTtl(v) => config.max_zone_ttl = Some(v),
ZoneStatement::MaxRefreshTime(v) => config.max_refresh_time = Some(v),
ZoneStatement::MinRefreshTime(v) => config.min_refresh_time = Some(v),
ZoneStatement::MaxRetryTime(v) => config.max_retry_time = Some(v),
ZoneStatement::MinRetryTime(v) => config.min_retry_time = Some(v),
ZoneStatement::MultiMaster(v) => config.multi_master = Some(v),
ZoneStatement::RequestIxfr(v) => config.request_ixfr = Some(v),
ZoneStatement::RequestExpire(v) => config.request_expire = Some(v),
ZoneStatement::Unknown(key, value) => {
config.raw_options.insert(key, value);
}
}
}
Ok((input, config))
}
pub fn parse_showzone(input: &str) -> ParseResult<ZoneConfig> {
match parse_zone_config_internal(input.trim()) {
Ok((_, config)) => Ok(config),
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
Err(RndcParseError::ParseError(format!("Parse failed: {:?}", e)))
}
Err(nom::Err::Incomplete(_)) => Err(RndcParseError::Incomplete),
}
}