1use pest::{Parser, iterators::Pair};
2use pest_derive::Parser;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Parser)]
8#[grammar = "./grammar.pest"]
9pub struct FirewallGrammar;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "lowercase")]
14pub enum FirewallRule {
15 Service(ServiceRule),
17 Address(AddressRule),
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct ServiceRule {
23 pub action: Action,
24 pub service: String,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct AddressRule {
29 pub action: Action,
30 pub direction: Option<Direction>,
31 pub interface: Option<String>,
32 pub from: Option<Address>,
33 pub to: Option<Address>,
34 pub port: Option<u16>,
35 pub proto: Option<Protocol>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum Action {
41 Allow,
42 Deny,
43 Reject,
44 Limit,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum Direction {
50 In,
51 Out,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum Protocol {
57 Tcp,
58 Udp,
59 Any,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(tag = "type", content = "value", rename_all = "lowercase")]
64pub enum Address {
65 Any,
66 Internal,
67 External,
68 IpCidr(String),
69}
70
71#[derive(Debug, Error)]
72pub enum ParseError {
73 #[error("pest parse error: {0}")]
74 Pest(Box<pest::error::Error<Rule>>),
75 #[error("{0}")]
76 Message(String),
77}
78
79type ParseResult<T> = Result<T, ParseError>;
80
81impl From<pest::error::Error<Rule>> for ParseError {
82 fn from(value: pest::error::Error<Rule>) -> Self {
83 Self::Pest(Box::new(value))
84 }
85}
86
87pub fn parse_rules(input: &str) -> ParseResult<Vec<FirewallRule>> {
89 let mut file_pairs = FirewallGrammar::parse(Rule::file, input)?;
90 let file_pair = file_pairs
91 .next()
92 .ok_or_else(|| ParseError::Message("expected file pair to be present".into()))?;
93
94 let mut rules = Vec::new();
95 for pair in file_pair.into_inner() {
96 match pair.as_rule() {
97 Rule::service_rule => {
98 rules.push(FirewallRule::Service(parse_service_rule(pair)?));
99 }
100 Rule::addr_rule => {
101 rules.push(FirewallRule::Address(parse_address_rule(pair)?));
102 }
103 Rule::line | Rule::NEWLINE | Rule::COMMENT | Rule::EOI => {}
104 unexpected => {
105 return Err(ParseError::Message(format!(
106 "unexpected rule inside file: {unexpected:?}"
107 )));
108 }
109 }
110 }
111
112 Ok(rules)
113}
114
115fn parse_service_rule(pair: Pair<Rule>) -> ParseResult<ServiceRule> {
116 let mut inner = pair.into_inner();
117 let action_pair = inner
118 .next()
119 .ok_or_else(|| ParseError::Message("service rule missing action".into()))?;
120 let ident_pair = inner
121 .next()
122 .ok_or_else(|| ParseError::Message("service rule missing identifier".into()))?;
123
124 Ok(ServiceRule {
125 action: parse_action(action_pair.as_str())?,
126 service: ident_pair.as_str().to_string(),
127 })
128}
129
130fn parse_address_rule(pair: Pair<Rule>) -> ParseResult<AddressRule> {
131 let mut inner = pair.into_inner();
132 let action_pair = inner
133 .next()
134 .ok_or_else(|| ParseError::Message("address rule missing action".into()))?;
135 let action = parse_action(action_pair.as_str())?;
136
137 let mut rule = AddressRule {
138 action,
139 direction: None,
140 interface: None,
141 from: None,
142 to: None,
143 port: None,
144 proto: None,
145 };
146
147 for sub_pair in inner {
148 match sub_pair.as_rule() {
149 Rule::direction => {
150 rule.direction = Some(parse_direction(sub_pair.as_str())?);
151 }
152 Rule::interface_clause => {
153 let ident = sub_pair.into_inner().next().ok_or_else(|| {
154 ParseError::Message("interface clause missing identifier".into())
155 })?;
156 rule.interface = Some(ident.as_str().to_string());
157 }
158 Rule::from_clause => {
159 let addr_pair = sub_pair
160 .into_inner()
161 .next()
162 .ok_or_else(|| ParseError::Message("from clause missing address".into()))?;
163 rule.from = Some(parse_address(addr_pair)?);
164 }
165 Rule::to_clause => {
166 let addr_pair = sub_pair
167 .into_inner()
168 .next()
169 .ok_or_else(|| ParseError::Message("to clause missing address".into()))?;
170 rule.to = Some(parse_address(addr_pair)?);
171 }
172 Rule::port_clause => {
173 let port_pair = sub_pair
174 .into_inner()
175 .next()
176 .ok_or_else(|| ParseError::Message("port clause missing number".into()))?;
177 rule.port = Some(parse_port(port_pair.as_str())?);
178 }
179 Rule::proto_clause => {
180 let proto_pair = sub_pair
181 .into_inner()
182 .next()
183 .ok_or_else(|| ParseError::Message("proto clause missing proto".into()))?;
184 rule.proto = Some(parse_protocol(proto_pair.as_str())?);
185 }
186 other => {
187 return Err(ParseError::Message(format!(
188 "unexpected rule inside addr_rule: {other:?}"
189 )));
190 }
191 }
192 }
193
194 Ok(rule)
195}
196
197fn parse_action(text: &str) -> ParseResult<Action> {
198 match text {
199 "allow" => Ok(Action::Allow),
200 "deny" => Ok(Action::Deny),
201 "reject" => Ok(Action::Reject),
202 "limit" => Ok(Action::Limit),
203 other => Err(ParseError::Message(format!("invalid action: {other}"))),
204 }
205}
206
207fn parse_direction(text: &str) -> ParseResult<Direction> {
208 match text {
209 "in" => Ok(Direction::In),
210 "out" => Ok(Direction::Out),
211 other => Err(ParseError::Message(format!("invalid direction: {other}"))),
212 }
213}
214
215fn parse_protocol(text: &str) -> ParseResult<Protocol> {
216 match text {
217 "tcp" => Ok(Protocol::Tcp),
218 "udp" => Ok(Protocol::Udp),
219 "any" => Ok(Protocol::Any),
220 other => Err(ParseError::Message(format!("invalid protocol: {other}"))),
221 }
222}
223
224fn parse_address(pair: Pair<Rule>) -> ParseResult<Address> {
225 let text = pair.as_str();
226 match text {
227 "any" => Ok(Address::Any),
228 "internal" => Ok(Address::Internal),
229 "external" => Ok(Address::External),
230 _ => Ok(Address::IpCidr(text.to_string())),
231 }
232}
233
234fn parse_port(text: &str) -> ParseResult<u16> {
235 text.parse::<u16>()
236 .map_err(|_| ParseError::Message(format!("invalid port value: {text}")))
237}
238
239pub mod grammar_docs {
241 pub const WHITESPACE: &str = r#"WHITESPACE = _{ " " | "\t" }"#;
243 pub const NEWLINE: &str = r#"NEWLINE = _{ "\r\n" | "\n" }"#;
245 pub const COMMENT: &str = r##"COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* }"##;
246 pub const ACTION: &str = r#"action = { "allow" | "deny" | "reject" | "limit" }"#;
247 pub const DIRECTION: &str = r#"direction = { "in" | "out" }"#;
249 pub const IDENT: &str = r#"ident = @{ (ASCII_ALPHANUMERIC | "_" | "-")+ }"#;
250 pub const IP: &str = r#"ip = @{ (ASCII_DIGIT | "." | "/")+ }"#;
252 pub const ADDR: &str = r#"addr = { "any" | "internal" | "external" | ip }"#;
254 pub const PORT_NUMBER: &str = r#"port_number = @{ ASCII_DIGIT+ }"#;
256 pub const PORT_CLAUSE: &str = r#"port_clause = { "port" ~ port_number }"#;
257 pub const PROTO: &str = r#"proto = { "tcp" | "udp" | "any" }"#;
259 pub const PROTO_CLAUSE: &str = r#"proto_clause = { "proto" ~ proto }"#;
260 pub const INTERFACE_CLAUSE: &str = r#"interface_clause = { "on" ~ ident }"#;
261 pub const FROM_CLAUSE: &str = r#"from_clause = { "from" ~ addr }"#;
263 pub const TO_CLAUSE: &str = r#"to_clause = { "to" ~ addr }"#;
264 pub const ADDR_RULE: &str = r#"addr_rule = { action ~ direction? ~ interface_clause? ~ (from_clause | to_clause | port_clause | proto_clause)+ }"#;
266 pub const SERVICE_RULE: &str = r#"service_rule = { action ~ ident }"#;
267 pub const LINE: &str = r#"line = _{ (addr_rule | service_rule) ~ COMMENT? | COMMENT }"#;
268 pub const FILE: &str = r#"file = { SOI ~ (line? ~ NEWLINE)* ~ EOI }"#;
269}