use std::net::IpAddr;
use crate::enums::{Action, Protocol, RuleStatus};
use crate::error::UfwError;
use crate::models::{IpRule, PortRule};
pub(crate) fn parse_is_enabled(raw: &str) -> Result<bool, UfwError> {
let line = raw.lines().next().ok_or(UfwError::EmptyOutput)?;
let trimmed = line.trim();
if trimmed == "Status: active" {
Ok(true)
} else if trimmed == "Status: inactive" {
Ok(false)
} else {
Err(UfwError::UnexpectedStatusLine(trimmed.to_string()))
}
}
pub(crate) fn parse_port_rules(raw: &str) -> Result<Vec<PortRule>, UfwError> {
let mut rules = Vec::new();
for line in raw.lines() {
let trimmed = line.trim();
if !trimmed.starts_with('[') {
continue;
}
let Some(content) = trimmed.split(']').nth(1) else {
continue;
};
let content = content.trim();
if let Some(parsed) = parse_port_rule_line(content) {
rules.push(parsed);
}
}
Ok(rules)
}
fn parse_port_rule_line(content: &str) -> Option<PortRule> {
let (target, action) = split_at_action(content)?;
let target = target.trim();
let port_proto = target.split_whitespace().next()?;
let port_proto = port_proto.strip_suffix("(v6)").unwrap_or(port_proto);
let (port_str, proto_str) = port_proto.split_once('/')?;
let port: u16 = port_str.parse().ok()?;
let protocol = match proto_str {
"tcp" => Protocol::Tcp,
"udp" => Protocol::Udp,
_ => return None,
};
let status = match action {
Action::Allow => RuleStatus::Allowed,
Action::Deny => RuleStatus::Denied,
};
Some(PortRule {
port,
protocol,
status,
})
}
pub(crate) fn parse_ip_rules(raw: &str) -> Result<Vec<IpRule>, UfwError> {
let mut rules = Vec::new();
for line in raw.lines() {
let trimmed = line.trim();
if !trimmed.starts_with('[') {
continue;
}
let Some(content) = trimmed.split(']').nth(1) else {
continue;
};
let content = content.trim();
if let Some(parsed) = parse_ip_rule_line(content) {
rules.push(parsed);
}
}
Ok(rules)
}
fn parse_ip_rule_line(content: &str) -> Option<IpRule> {
let (target, action) = split_at_action(content)?;
let target = target.trim();
if !target.starts_with("Anywhere") {
return None;
}
let action_marker = match action {
Action::Allow => "ALLOW IN",
Action::Deny => "DENY IN",
};
let source = content[content.find(action_marker)? + action_marker.len()..].trim();
let source = source.strip_suffix(" (v6)").unwrap_or(source);
let ip: IpAddr = source.parse().ok()?;
let protocol = Protocol::Both;
let status = match action {
Action::Allow => RuleStatus::Allowed,
Action::Deny => RuleStatus::Denied,
};
Some(IpRule {
ip,
protocol,
status,
})
}
fn split_at_action(content: &str) -> Option<(&str, Action)> {
if let Some(pos) = content.find("ALLOW IN") {
Some((&content[..pos], Action::Allow))
} else if let Some(pos) = content.find("DENY IN") {
Some((&content[..pos], Action::Deny))
} else {
None
}
}
pub(crate) fn find_port_status(rules: &[PortRule], port: u16, protocol: Protocol) -> RuleStatus {
match protocol {
Protocol::Both => {
let tcp = rules
.iter()
.find(|r| r.port == port && r.protocol == Protocol::Tcp);
let udp = rules
.iter()
.find(|r| r.port == port && r.protocol == Protocol::Udp);
match (tcp.map(|r| r.status), udp.map(|r| r.status)) {
(Some(RuleStatus::Allowed), Some(RuleStatus::Allowed)) => RuleStatus::Allowed,
(Some(RuleStatus::Denied), Some(RuleStatus::Denied)) => RuleStatus::Denied,
(None, None) => RuleStatus::None,
_ => RuleStatus::None,
}
}
proto => {
let matched = rules.iter().find(|r| r.port == port && r.protocol == proto);
matched.map_or(RuleStatus::None, |r| r.status)
}
}
}
pub(crate) fn find_ip_status(rules: &[IpRule], ip: IpAddr, protocol: Protocol) -> RuleStatus {
match protocol {
Protocol::Both => {
let matched = rules.iter().find(|r| r.ip == ip);
matched.map_or(RuleStatus::None, |r| r.status)
}
_ => {
let matched = rules.iter().find(|r| r.ip == ip);
matched.map_or(RuleStatus::None, |r| r.status)
}
}
}
#[cfg(test)]
mod tests {
use std::net::IpAddr;
use super::*;
#[test]
fn parse_is_enabled_active() {
assert!(parse_is_enabled("Status: active\n").expect("valid active"));
}
#[test]
fn parse_is_enabled_inactive() {
assert!(!parse_is_enabled("Status: inactive\n").expect("valid inactive"));
}
#[test]
fn parse_is_enabled_active_with_extra_lines() {
let output = "\
Status: active
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
";
assert!(parse_is_enabled(output).expect("active with trailing"));
}
#[test]
fn parse_is_enabled_empty() {
let result = parse_is_enabled("");
assert!(matches!(result, Err(UfwError::EmptyOutput)));
}
#[test]
fn parse_is_enabled_unexpected_line() {
let result = parse_is_enabled("Something weird\n");
match result {
Err(UfwError::UnexpectedStatusLine(line)) => {
assert_eq!(line, "Something weird");
}
_ => panic!("expected UnexpectedStatusLine"),
}
}
#[test]
fn parse_is_enabled_whitespace_padded() {
assert!(parse_is_enabled(" Status: active \n").expect("whitespace active"));
assert!(!parse_is_enabled("\tStatus: inactive\n").expect("tab inactive"));
}
const PORT_RULES_BASIC: &str = "\
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 8080/udp DENY IN Anywhere
[ 3] 443/tcp ALLOW IN Anywhere
[ 4] 53/udp ALLOW IN Anywhere
";
#[test]
fn parse_port_rules_basic_allow() {
let rules = parse_port_rules(PORT_RULES_BASIC).expect("parse");
let rule = rules
.iter()
.find(|r| r.port == 22 && r.protocol == Protocol::Tcp);
let rule = rule.expect("22/tcp rule");
assert_eq!(rule.status, RuleStatus::Allowed);
}
#[test]
fn parse_port_rules_basic_deny() {
let rules = parse_port_rules(PORT_RULES_BASIC).expect("parse");
let rule = rules
.iter()
.find(|r| r.port == 8080 && r.protocol == Protocol::Udp);
let rule = rule.expect("8080/udp rule");
assert_eq!(rule.status, RuleStatus::Denied);
}
#[test]
fn parse_port_rules_basic_multiple() {
let rules = parse_port_rules(PORT_RULES_BASIC).expect("parse");
assert_eq!(rules.len(), 4);
assert!(
rules
.iter()
.any(|r| r.port == 443 && r.protocol == Protocol::Tcp)
);
assert!(
rules
.iter()
.any(|r| r.port == 53 && r.protocol == Protocol::Udp)
);
}
#[test]
fn parse_port_rules_no_match_returns_empty_vec() {
let rules = parse_port_rules("").expect("empty");
assert!(rules.is_empty());
}
#[test]
fn parse_port_rules_header_only_returns_empty() {
let output = "\
Status: active
To Action From
-- ------ ----
";
let rules = parse_port_rules(output).expect("header only");
assert!(rules.is_empty());
}
const PORT_RULES_V6: &str = "\
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 22/tcp (v6) ALLOW IN Anywhere (v6)
[ 3] 8080/udp (v6) DENY IN Anywhere (v6)
";
#[test]
fn parse_port_rules_v6_does_not_crash() {
let rules = parse_port_rules(PORT_RULES_V6).expect("parse");
assert!(
rules
.iter()
.any(|r| r.port == 22 && r.protocol == Protocol::Tcp)
);
assert!(
rules
.iter()
.any(|r| r.port == 8080 && r.protocol == Protocol::Udp)
);
}
#[test]
fn parse_port_rules_v6_correct_status() {
let rules = parse_port_rules(PORT_RULES_V6).expect("parse");
let rule = rules
.iter()
.find(|r| r.port == 8080 && r.protocol == Protocol::Udp);
let rule = rule.expect("8080/udp rule from v6 output");
assert_eq!(rule.status, RuleStatus::Denied);
}
const PORT_RULES_DUPLICATE: &str = "\
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 22/tcp ALLOW IN Anywhere
";
#[test]
fn parse_port_rules_duplicates_are_not_deduplicated() {
let rules = parse_port_rules(PORT_RULES_DUPLICATE).expect("parse");
assert_eq!(rules.len(), 2);
let count = rules
.iter()
.filter(|r| r.port == 22 && r.protocol == Protocol::Tcp)
.count();
assert_eq!(count, 2);
}
const PORT_RULES_MALFORMED: &str = "\
Status: active
To Action From
-- ------ ----
[ 1] Not a real rule
[ 2] 22/tcp ALLOW IN Anywhere
random garbage
[ 3] also-bad
";
#[test]
fn parse_port_rules_malformed_lines_are_skipped() {
let rules = parse_port_rules(PORT_RULES_MALFORMED).expect("parse");
let rule = rules
.iter()
.find(|r| r.port == 22 && r.protocol == Protocol::Tcp);
assert!(rule.is_some(), "22/tcp should still be parsed");
assert_eq!(rule.expect("22/tcp").status, RuleStatus::Allowed);
}
#[test]
fn parse_port_rules_malformed_does_not_panic() {
let rules = parse_port_rules(PORT_RULES_MALFORMED).expect("parse");
assert_eq!(rules.len(), 1);
}
#[test]
fn parse_port_rules_empty_line_between_rules_skipped() {
let input = "\
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 443/tcp ALLOW IN Anywhere
";
let rules = parse_port_rules(input).expect("parse");
assert_eq!(rules.len(), 2);
}
const IP_RULES_BASIC: &str = "\
Status: active
To Action From
-- ------ ----
[ 1] Anywhere ALLOW IN 192.168.1.10
[ 2] Anywhere DENY IN 10.0.0.5
[ 3] Anywhere ALLOW IN 2001:db8::1
[ 4] Anywhere DENY IN ::1
";
#[test]
fn parse_ip_rules_ipv4_allow() {
let rules = parse_ip_rules(IP_RULES_BASIC).expect("parse");
let ip: IpAddr = "192.168.1.10".parse().expect("valid ip");
let rule = rules.iter().find(|r| r.ip == ip);
let rule = rule.expect("192.168.1.10 rule");
assert_eq!(rule.status, RuleStatus::Allowed);
}
#[test]
fn parse_ip_rules_ipv4_deny() {
let rules = parse_ip_rules(IP_RULES_BASIC).expect("parse");
let ip: IpAddr = "10.0.0.5".parse().expect("valid ip");
let rule = rules.iter().find(|r| r.ip == ip);
let rule = rule.expect("10.0.0.5 rule");
assert_eq!(rule.status, RuleStatus::Denied);
}
#[test]
fn parse_ip_rules_ipv6_allow() {
let rules = parse_ip_rules(IP_RULES_BASIC).expect("parse");
let ip: IpAddr = "2001:db8::1".parse().expect("valid ip");
let rule = rules.iter().find(|r| r.ip == ip);
let rule = rule.expect("2001:db8::1 rule");
assert_eq!(rule.status, RuleStatus::Allowed);
}
#[test]
fn parse_ip_rules_ipv6_deny() {
let rules = parse_ip_rules(IP_RULES_BASIC).expect("parse");
let ip: IpAddr = "::1".parse().expect("valid ip");
let rule = rules.iter().find(|r| r.ip == ip);
let rule = rule.expect("::1 rule");
assert_eq!(rule.status, RuleStatus::Denied);
}
#[test]
fn parse_ip_rules_missing_returns_empty() {
let rules = parse_ip_rules("").expect("empty");
assert!(rules.is_empty());
}
#[test]
fn parse_ip_rules_skips_port_lines() {
let input = "\
Status: active
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] Anywhere ALLOW IN 192.168.1.10
";
let rules = parse_ip_rules(input).expect("parse");
assert_eq!(rules.len(), 1);
let ip: IpAddr = "192.168.1.10".parse().expect("valid ip");
assert!(rules.iter().any(|r| r.ip == ip));
}
#[test]
#[ignore]
fn todo_parse_ip_rules_with_protocol_format() {
}
const PORT_RULES_STATUS: &str = "\
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 53/udp ALLOW IN Anywhere
[ 3] 8080/udp DENY IN Anywhere
";
#[test]
fn find_port_status_tcp_allowed() {
let rules = parse_port_rules(PORT_RULES_STATUS).expect("parse");
assert_eq!(
find_port_status(&rules, 22, Protocol::Tcp),
RuleStatus::Allowed
);
}
#[test]
fn find_port_status_udp_denied() {
let rules = parse_port_rules(PORT_RULES_STATUS).expect("parse");
assert_eq!(
find_port_status(&rules, 8080, Protocol::Udp),
RuleStatus::Denied
);
}
#[test]
fn find_port_status_nonexistent_port() {
let rules = parse_port_rules(PORT_RULES_STATUS).expect("parse");
assert_eq!(
find_port_status(&rules, 9999, Protocol::Tcp),
RuleStatus::None
);
}
#[test]
fn find_port_status_wrong_protocol() {
let rules = parse_port_rules(PORT_RULES_STATUS).expect("parse");
assert_eq!(
find_port_status(&rules, 53, Protocol::Tcp),
RuleStatus::None
);
}
#[test]
fn find_port_status_both_both_allowed() {
let input = "\
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 22/udp ALLOW IN Anywhere
";
let rules = parse_port_rules(input).expect("parse");
assert_eq!(
find_port_status(&rules, 22, Protocol::Both),
RuleStatus::Allowed
);
}
#[test]
fn find_port_status_both_both_denied() {
let input = "\
[ 1] 22/tcp DENY IN Anywhere
[ 2] 22/udp DENY IN Anywhere
";
let rules = parse_port_rules(input).expect("parse");
assert_eq!(
find_port_status(&rules, 22, Protocol::Both),
RuleStatus::Denied
);
}
#[test]
fn find_port_status_both_mixed_returns_none() {
let input = "\
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 22/udp DENY IN Anywhere
";
let rules = parse_port_rules(input).expect("parse");
assert_eq!(
find_port_status(&rules, 22, Protocol::Both),
RuleStatus::None
);
}
#[test]
fn find_port_status_both_one_missing_returns_none() {
let input = "\
[ 1] 22/tcp ALLOW IN Anywhere
";
let rules = parse_port_rules(input).expect("parse");
assert_eq!(
find_port_status(&rules, 22, Protocol::Both),
RuleStatus::None
);
}
#[test]
fn find_port_status_both_neither_exists() {
let rules = parse_port_rules("").expect("parse");
assert_eq!(
find_port_status(&rules, 99, Protocol::Both),
RuleStatus::None
);
}
const IP_RULES_STATUS: &str = "\
[ 1] Anywhere ALLOW IN 192.168.1.10
[ 2] Anywhere DENY IN 10.0.0.5
";
#[test]
fn find_ip_status_both_allowed() {
let rules = parse_ip_rules(IP_RULES_STATUS).expect("parse");
let ip: IpAddr = "192.168.1.10".parse().expect("valid ip");
assert_eq!(
find_ip_status(&rules, ip, Protocol::Both),
RuleStatus::Allowed
);
}
#[test]
fn find_ip_status_both_denied() {
let rules = parse_ip_rules(IP_RULES_STATUS).expect("parse");
let ip: IpAddr = "10.0.0.5".parse().expect("valid ip");
assert_eq!(
find_ip_status(&rules, ip, Protocol::Both),
RuleStatus::Denied
);
}
#[test]
fn find_ip_status_both_nonexistent() {
let rules = parse_ip_rules(IP_RULES_STATUS).expect("parse");
let ip: IpAddr = "127.0.0.1".parse().expect("valid ip");
assert_eq!(find_ip_status(&rules, ip, Protocol::Both), RuleStatus::None);
}
#[test]
fn find_ip_status_tcp_with_generic_rule() {
let rules = parse_ip_rules(IP_RULES_STATUS).expect("parse");
let ip: IpAddr = "192.168.1.10".parse().expect("valid ip");
assert_eq!(
find_ip_status(&rules, ip, Protocol::Tcp),
RuleStatus::Allowed
);
}
#[test]
fn action_as_str() {
assert_eq!(Action::Allow.as_str(), "allow");
assert_eq!(Action::Deny.as_str(), "deny");
}
}