use roxmltree::{Document, Node};
use super::endpoint_parser::EndpointParser;
use crate::Rule;
pub struct PfSenseRulesParser {}
impl PfSenseRulesParser {
pub fn parse(document: &Document) -> Vec<Rule> {
let mut rules = vec![];
if let Some(filter) = document
.descendants()
.find(|e| e.has_tag_name("pfsense"))
.and_then(|e| e.children().find(|ce| ce.has_tag_name("filter")))
{
rules.append(&mut PfSenseRulesParser::parse_rules(filter, "filter"));
}
if let Some(nat) = document
.descendants()
.find(|e| e.has_tag_name("pfsense"))
.and_then(|e| e.children().find(|ce| ce.has_tag_name("nat")))
{
rules.append(&mut PfSenseRulesParser::parse_rules(nat, "nat"));
}
rules
}
fn parse_rules(node: Node<'_, '_>, rule_type: &str) -> Vec<Rule> {
let mut rules = Vec::new();
for (index, rule) in (0_u64..).zip(node.children().filter(|e| e.has_tag_name("rule"))) {
let disabled = rule.children().any(|e| e.has_tag_name("disabled"));
let policy = rule
.children()
.find(|e| e.has_tag_name("type"))
.and_then(|e| e.text())
.unwrap_or("pass")
.to_string();
let ipprotocol = rule
.children()
.find(|e| e.has_tag_name("ipprotocol"))
.and_then(|e| e.text())
.unwrap_or("*");
let protocol = rule
.children()
.find(|e| e.has_tag_name("protocol"))
.and_then(|e| e.text())
.unwrap_or("any");
let description = rule
.children()
.find(|e| e.has_tag_name("descr"))
.and_then(|e| e.text())
.unwrap_or("")
.to_string();
let interface = rule
.children()
.find(|e| e.has_tag_name("interface"))
.and_then(|e| e.text())
.unwrap_or("none")
.to_string();
let (source_addr, source_port, source_type, source_inversed) =
EndpointParser::parse(rule.children().find(|e| e.has_tag_name("source")));
let (destination_addr, destination_port, destination_type, destination_inversed) =
EndpointParser::parse(rule.children().find(|e| e.has_tag_name("destination")));
rules.push(Rule {
disabled,
r#type: rule_type.to_string(),
protocol: format!("{}/{}", Self::map_ipprotocol(ipprotocol), protocol),
policy,
description,
source_port,
source_addr,
source_type,
source_inversed,
destination_addr,
destination_port,
destination_type,
destination_inversed,
interface,
order: index,
});
}
rules
}
#[inline]
fn map_ipprotocol(ipprotocol: &str) -> &str {
match ipprotocol {
"inet" => "IPv4",
"inet6" => "IPv6",
_ => "none",
}
}
}
#[cfg(test)]
mod tests {
use super::PfSenseRulesParser;
use roxmltree::Document;
#[test]
fn test_parse_filter_rules() {
let xml = r#"
<pfsense>
<filter>
<rule>
<type>pass</type>
<ipprotocol>inet</ipprotocol>
<descr>Default allow LAN to any rule</descr>
<interface>lan</interface>
<source>
<network>lan</network>
</source>
<destination>
<any/>
</destination>
</rule>
</filter>
</pfsense>
"#;
let doc = Document::parse(xml).expect("Failed to parse XML");
let rules = PfSenseRulesParser::parse(&doc);
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].disabled, false);
assert_eq!(rules[0].r#type, "filter");
assert_eq!(rules[0].policy, "pass");
assert_eq!(rules[0].protocol, "IPv4/any");
assert_eq!(rules[0].description, "Default allow LAN to any rule");
assert_eq!(rules[0].source_addr, "lan");
assert_eq!(rules[0].source_port, "*");
assert_eq!(rules[0].source_type, "network");
assert_eq!(rules[0].source_inversed, false);
assert_eq!(rules[0].destination_addr, "*");
assert_eq!(rules[0].destination_port, "*");
assert_eq!(rules[0].destination_type, "address");
assert_eq!(rules[0].destination_inversed, false);
assert_eq!(rules[0].interface, "lan");
assert_eq!(rules[0].order, 0);
}
#[test]
fn test_parse_nat_rules() {
let xml = r#"
<pfsense>
<nat>
<rule>
<source>
<any></any>
</source>
<destination>
<network>wanip</network>
<port>8091</port>
</destination>
<ipprotocol>inet6</ipprotocol>
<protocol>tcp</protocol>
<target>172.16.70.20</target>
<local-port>8080</local-port>
<interface>wan</interface>
<descr>NAT Rule</descr>
</rule>
</nat>
</pfsense>
"#;
let doc = Document::parse(xml).expect("Failed to parse XML");
let rules = PfSenseRulesParser::parse(&doc);
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].disabled, false);
assert_eq!(rules[0].r#type, "nat");
assert_eq!(rules[0].policy, "pass");
assert_eq!(rules[0].protocol, "IPv6/tcp");
assert_eq!(rules[0].description, "NAT Rule");
assert_eq!(rules[0].source_addr, "*");
assert_eq!(rules[0].source_port, "*");
assert_eq!(rules[0].source_type, "address");
assert_eq!(rules[0].source_inversed, false);
assert_eq!(rules[0].destination_addr, "wanip");
assert_eq!(rules[0].destination_port, "8091");
assert_eq!(rules[0].destination_type, "network");
assert_eq!(rules[0].destination_inversed, false);
assert_eq!(rules[0].interface, "wan");
assert_eq!(rules[0].order, 0);
}
#[test]
fn test_parse_multiple_rules() {
let xml = r#"
<pfsense>
<filter>
<rule>
<disabled></disabled>
<type>pass</type>
<ipprotocol>inet</ipprotocol>
<descr>Allow LAN</descr>
<interface>lan</interface>
<source>
<network>lan</network>
</source>
<destination>
<any/>
</destination>
</rule>
</filter>
<nat>
<rule>
<disabled/>
<source>
<any></any>
</source>
<destination>
<network>wanip</network>
<port>8091</port>
</destination>
<ipprotocol>inet</ipprotocol>
<protocol>tcp</protocol>
<target>172.16.70.20</target>
<local-port>8080</local-port>
<interface>wan</interface>
<descr>NAT Rule</descr>
</rule>
</nat>
</pfsense>
"#;
let doc = Document::parse(xml).expect("Failed to parse XML");
let rules = PfSenseRulesParser::parse(&doc);
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].disabled, true);
assert_eq!(rules[0].r#type, "filter");
assert_eq!(rules[0].policy, "pass");
assert_eq!(rules[0].protocol, "IPv4/any");
assert_eq!(rules[0].description, "Allow LAN");
assert_eq!(rules[0].source_addr, "lan");
assert_eq!(rules[0].source_port, "*");
assert_eq!(rules[0].source_type, "network");
assert_eq!(rules[0].source_inversed, false);
assert_eq!(rules[0].destination_addr, "*");
assert_eq!(rules[0].destination_port, "*");
assert_eq!(rules[0].destination_type, "address");
assert_eq!(rules[0].destination_inversed, false);
assert_eq!(rules[0].interface, "lan");
assert_eq!(rules[0].order, 0);
assert_eq!(rules[1].disabled, true);
assert_eq!(rules[1].r#type, "nat");
assert_eq!(rules[1].policy, "pass");
assert_eq!(rules[1].protocol, "IPv4/tcp");
assert_eq!(rules[1].description, "NAT Rule");
assert_eq!(rules[1].source_addr, "*");
assert_eq!(rules[1].source_port, "*");
assert_eq!(rules[1].destination_addr, "wanip");
assert_eq!(rules[1].destination_port, "8091");
assert_eq!(rules[1].destination_type, "network");
assert_eq!(rules[1].destination_inversed, false);
assert_eq!(rules[1].interface, "wan");
assert_eq!(rules[1].order, 0);
}
#[test]
fn test_parse_missing_optional_fields() {
let xml = r#"
<pfsense>
<filter>
<rule>
<type>reject</type>
<ipprotocol>inet</ipprotocol>
<descr>Block traffic</descr>
<source>
<any></any>
</source>
<destination>
<address>restricted_zone</address>
</destination>
<interface>opt1</interface>
</rule>
</filter>
</pfsense>
"#;
let doc = Document::parse(xml).expect("Failed to parse XML");
let rules = PfSenseRulesParser::parse(&doc);
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].disabled, false);
assert_eq!(rules[0].r#type, "filter");
assert_eq!(rules[0].policy, "reject");
assert_eq!(rules[0].protocol, "IPv4/any");
assert_eq!(rules[0].description, "Block traffic");
assert_eq!(rules[0].source_addr, "*");
assert_eq!(rules[0].source_port, "*");
assert_eq!(rules[0].source_type, "address");
assert_eq!(rules[0].source_inversed, false);
assert_eq!(rules[0].destination_addr, "restricted_zone");
assert_eq!(rules[0].destination_port, "*");
assert_eq!(rules[0].destination_type, "address");
assert_eq!(rules[0].destination_inversed, false);
assert_eq!(rules[0].interface, "opt1");
assert_eq!(rules[0].order, 0);
}
#[test]
fn test_parse_ordering() {
let xml = r#"
<pfsense>
<filter>
<rule>
<type>reject</type>
<interface>opt1</interface>
</rule>
<rule>
<type>reject</type>
<interface>opt1</interface>
</rule>
<rule>
<type>reject</type>
<interface>opt1</interface>
</rule>
</filter>
</pfsense>
"#;
let doc = Document::parse(xml).expect("Failed to parse XML");
let rules = PfSenseRulesParser::parse(&doc);
assert_eq!(rules.len(), 3);
assert_eq!(rules[0].order, 0);
assert_eq!(rules[1].order, 1);
assert_eq!(rules[2].order, 2);
}
}