diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c69da..213daf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). * **Security**: in case of vulnerabilities. ## [unreleased] +### Added +- Add support for scrub anchors and rules. Since this modifies the public enums `AnchorKind` and + `RulesetKind`, it is a breaking change. They have been marked as `non_exhaustive` to prevent + future additions from being breaking. ## [0.5.0] - 2024-07-24 diff --git a/examples/add_anchor.rs b/examples/add_anchor.rs index fee7637..55dcbe9 100644 --- a/examples/add_anchor.rs +++ b/examples/add_anchor.rs @@ -17,7 +17,9 @@ fn main() { .expect("Unable to add filter anchor"); pf.try_add_anchor(&anchor_name, pfctl::AnchorKind::Redirect) .expect("Unable to add redirect anchor"); + pf.try_add_anchor(&anchor_name, pfctl::AnchorKind::Scrub) + .expect("Unable to add scrub anchor"); - println!("Added {} as both a redirect and filter anchor", anchor_name); + println!("Added {} as every anchor type", anchor_name); } } diff --git a/examples/add_rules.rs b/examples/add_rules.rs index 321394b..54d8373 100644 --- a/examples/add_rules.rs +++ b/examples/add_rules.rs @@ -6,7 +6,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use pfctl::{ipnetwork, FilterRuleBuilder, PfCtl, RedirectRuleBuilder}; +use pfctl::{ipnetwork, FilterRuleBuilder, PfCtl, RedirectRuleBuilder, ScrubRuleBuilder}; use std::net::Ipv4Addr; static ANCHOR_NAME: &str = "test.anchor"; @@ -87,6 +87,11 @@ fn main() { .build() .unwrap(); + let scrub_rule = ScrubRuleBuilder::default() + .action(pfctl::ScrubRuleAction::Scrub) + .build() + .unwrap(); + // Add the rules to the test anchor pf.add_rule(ANCHOR_NAME, &pass_all_rule) .expect("Unable to add rule"); @@ -106,6 +111,8 @@ fn main() { .expect("Unable to add rule"); pf.add_redirect_rule(ANCHOR_NAME, &redirect_incoming_tcp_from_port_3000_to_4000) .expect("Unable to add redirect rule"); + pf.add_scrub_rule(ANCHOR_NAME, &scrub_rule) + .expect("Unable to add scrub rule"); println!("Added a bunch of rules to the {} anchor.", ANCHOR_NAME); println!("Run this command to remove them:"); diff --git a/examples/flush_rules.rs b/examples/flush_rules.rs index 2a314a3..6425ea2 100644 --- a/examples/flush_rules.rs +++ b/examples/flush_rules.rs @@ -20,5 +20,9 @@ fn main() { pf.flush_rules(&anchor_name, pfctl::RulesetKind::Redirect) .expect("Unable to flush redirect rules"); println!("Flushed redirect rules under anchor {}", anchor_name); + + pf.flush_rules(&anchor_name, pfctl::RulesetKind::Scrub) + .expect("Unable to flush scrub rules"); + println!("Flushed scrub rules under anchor {}", anchor_name); } } diff --git a/examples/transaction.rs b/examples/transaction.rs index 6bcfe24..f8e7ec0 100644 --- a/examples/transaction.rs +++ b/examples/transaction.rs @@ -17,6 +17,8 @@ fn main() { .expect("Unable to add test filter anchor"); pf.try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect) .expect("Unable to add test redirect anchor"); + pf.try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Scrub) + .expect("Unable to add test scrub anchor"); // Create some firewall rules that we want to set in one atomic transaction. let trans_rule1 = pfctl::FilterRuleBuilder::default() @@ -36,11 +38,16 @@ fn main() { .redirect_to(pfctl::Port::from(1338)) .build() .unwrap(); + let trans_rule4 = pfctl::ScrubRuleBuilder::default() + .action(pfctl::ScrubRuleAction::Scrub) + .build() + .unwrap(); // Create a transaction changeset and add the rules to it. let mut trans_change = pfctl::AnchorChange::new(); trans_change.set_filter_rules(vec![trans_rule1, trans_rule2]); trans_change.set_redirect_rules(vec![trans_rule3]); + trans_change.set_scrub_rules(vec![trans_rule4]); // Execute the transaction. This will OVERWRITE any existing rules under this anchor as it's // a set operation, not an add operation. diff --git a/src/anchor.rs b/src/anchor.rs index d91b906..6fb2f3e 100644 --- a/src/anchor.rs +++ b/src/anchor.rs @@ -10,9 +10,11 @@ use crate::ffi; /// Enum describing the kinds of anchors #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum AnchorKind { Filter, Redirect, + Scrub, } impl From for u8 { @@ -20,6 +22,7 @@ impl From for u8 { match anchor_kind { AnchorKind::Filter => ffi::pfvar::PF_PASS as u8, AnchorKind::Redirect => ffi::pfvar::PF_RDR as u8, + AnchorKind::Scrub => ffi::pfvar::PF_SCRUB as u8, } } } diff --git a/src/lib.rs b/src/lib.rs index c3289c6..97a8477 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -385,12 +385,25 @@ impl PfCtl { ioctl_guard!(ffi::pf_change_rule(self.fd(), &mut pfioc_rule)) } + pub fn add_scrub_rule(&mut self, anchor: &str, rule: &ScrubRule) -> Result<()> { + let mut pfioc_rule = unsafe { mem::zeroed::() }; + + pfioc_rule.pool_ticket = utils::get_pool_ticket(self.fd())?; + pfioc_rule.ticket = utils::get_ticket(self.fd(), anchor, AnchorKind::Scrub)?; + utils::copy_anchor_name(anchor, &mut pfioc_rule.anchor[..])?; + rule.try_copy_to(&mut pfioc_rule.rule)?; + + pfioc_rule.action = ffi::pfvar::PF_CHANGE_ADD_TAIL as u32; + ioctl_guard!(ffi::pf_change_rule(self.fd(), &mut pfioc_rule)) + } + pub fn flush_rules(&mut self, anchor: &str, kind: RulesetKind) -> Result<()> { let mut trans = Transaction::new(); let mut anchor_change = AnchorChange::new(); match kind { RulesetKind::Filter => anchor_change.set_filter_rules(Vec::new()), RulesetKind::Redirect => anchor_change.set_redirect_rules(Vec::new()), + RulesetKind::Scrub => anchor_change.set_scrub_rules(Vec::new()), }; trans.add_change(anchor, anchor_change); trans.commit() @@ -476,7 +489,7 @@ impl PfCtl { /// /// - Returns Result from call to closure on match. /// - Returns `ErrorKind::AnchorDoesNotExist` on mismatch, the closure is not called in that - /// case. + /// case. fn with_anchor_rule(&self, name: &str, kind: AnchorKind, f: F) -> Result where F: FnOnce(ffi::pfvar::pfioc_rule) -> Result, diff --git a/src/rule/mod.rs b/src/rule/mod.rs index ff9ab7c..88290ee 100644 --- a/src/rule/mod.rs +++ b/src/rule/mod.rs @@ -228,6 +228,25 @@ impl TryCopyTo for RedirectRule { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_builder::Builder)] +#[builder(setter(into))] +#[builder(build_fn(error = "Error"))] +pub struct ScrubRule { + action: ScrubRuleAction, + #[builder(default)] + direction: Direction, +} + +impl TryCopyTo for ScrubRule { + type Error = crate::Error; + + fn try_copy_to(&self, pf_rule: &mut ffi::pfvar::pf_rule) -> Result<()> { + pf_rule.action = self.action.into(); + pf_rule.direction = self.direction.into(); + Ok(()) + } +} + fn compatible_af(af1: AddrFamily, af2: AddrFamily) -> Result { match (af1, af2) { (af1, af2) if af1 == af2 => Ok(af1), diff --git a/src/rule/rule_action.rs b/src/rule/rule_action.rs index 581d8d9..d21c732 100644 --- a/src/rule/rule_action.rs +++ b/src/rule/rule_action.rs @@ -73,3 +73,19 @@ impl From for u8 { } } } + +/// Enum describing what should happen to a packet that matches a scrub rule. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ScrubRuleAction { + Scrub, + NoScrub, +} + +impl From for u8 { + fn from(rule_action: ScrubRuleAction) -> Self { + match rule_action { + ScrubRuleAction::Scrub => ffi::pfvar::PF_SCRUB as u8, + ScrubRuleAction::NoScrub => ffi::pfvar::PF_NOSCRUB as u8, + } + } +} diff --git a/src/ruleset.rs b/src/ruleset.rs index e425935..9f9509f 100644 --- a/src/ruleset.rs +++ b/src/ruleset.rs @@ -10,9 +10,11 @@ use crate::ffi; /// Enum describing the kinds of rulesets #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum RulesetKind { Filter, Redirect, + Scrub, } impl From for i32 { @@ -20,6 +22,7 @@ impl From for i32 { match ruleset_kind { RulesetKind::Filter => ffi::pfvar::PF_RULESET_FILTER as i32, RulesetKind::Redirect => ffi::pfvar::PF_RULESET_RDR as i32, + RulesetKind::Scrub => ffi::pfvar::PF_RULESET_SCRUB as i32, } } } diff --git a/src/transaction.rs b/src/transaction.rs index 940791d..07082d5 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -8,6 +8,7 @@ use crate::{ conversion::TryCopyTo, ffi, utils, FilterRule, PoolAddrList, RedirectRule, Result, RulesetKind, + ScrubRule, }; use std::{ collections::HashMap, @@ -69,6 +70,16 @@ impl Transaction { .map(|rules| (anchor.clone(), rules)) }) .collect(); + let scrub_changes: Vec<(String, Vec)> = self + .change_by_anchor + .iter_mut() + .filter_map(|(anchor, change)| { + change + .scrub_rules + .take() + .map(|rules| (anchor.clone(), rules)) + }) + .collect(); // create one transaction element for each unique combination of anchor name and // `RulesetKind` and order them so elements for filter rules go first followed by redirect @@ -81,6 +92,11 @@ impl Transaction { .iter() .map(|(anchor, _)| Self::new_trans_element(anchor, RulesetKind::Redirect)), ) + .chain( + scrub_changes + .iter() + .map(|(anchor, _)| Self::new_trans_element(anchor, RulesetKind::Scrub)), + ) .collect::>()?; Self::setup_trans(&mut pfioc_trans, pfioc_elements.as_mut_slice()); @@ -108,6 +124,15 @@ impl Transaction { } } + // add scrub rules into transaction + for ((anchor_name, scrub_rules), ticket) in + scrub_changes.into_iter().zip(ticket_iterator.by_ref()) + { + for scrub_rule in scrub_rules.iter() { + Self::add_scrub_rule(fd, &anchor_name, scrub_rule, ticket)?; + } + } + ioctl_guard!(ffi::pf_commit_trans(fd, &mut pfioc_trans)) } @@ -170,6 +195,24 @@ impl Transaction { ioctl_guard!(ffi::pf_add_rule(fd, &mut pfioc_rule)) } + /// Internal helper to add scrub rule into transaction + fn add_scrub_rule(fd: RawFd, anchor: &str, rule: &ScrubRule, ticket: u32) -> Result<()> { + // prepare pfioc_rule + let mut pfioc_rule = unsafe { mem::zeroed::() }; + utils::copy_anchor_name(anchor, &mut pfioc_rule.anchor[..])?; + rule.try_copy_to(&mut pfioc_rule.rule)?; + + // request new address pool + let pool_ticket = utils::get_pool_ticket(fd)?; + + // set tickets + pfioc_rule.ticket = ticket; + pfioc_rule.pool_ticket = pool_ticket; + + // add rule into transaction + ioctl_guard!(ffi::pf_add_rule(fd, &mut pfioc_rule)) + } + /// Internal helper to wire up pfioc_trans and pfioc_trans_e fn setup_trans( pfioc_trans: &mut ffi::pfvar::pfioc_trans, @@ -200,6 +243,7 @@ impl Transaction { pub struct AnchorChange { filter_rules: Option>, redirect_rules: Option>, + scrub_rules: Option>, } impl Default for AnchorChange { @@ -214,6 +258,7 @@ impl AnchorChange { AnchorChange { filter_rules: None, redirect_rules: None, + scrub_rules: None, } } @@ -224,4 +269,8 @@ impl AnchorChange { pub fn set_redirect_rules(&mut self, rules: Vec) { self.redirect_rules = Some(rules); } + + pub fn set_scrub_rules(&mut self, rules: Vec) { + self.scrub_rules = Some(rules); + } } diff --git a/tests/scrub_rules.rs b/tests/scrub_rules.rs new file mode 100644 index 0000000..90be423 --- /dev/null +++ b/tests/scrub_rules.rs @@ -0,0 +1,72 @@ +#[macro_use] +#[allow(dead_code)] +mod helper; + +use crate::helper::pfcli; +use assert_matches::assert_matches; + +static ANCHOR_NAME: &str = "pfctl-rs.integration.testing.scrub-rules"; + +fn before_each() { + pfctl::PfCtl::new() + .unwrap() + .try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Scrub) + .unwrap(); +} + +fn after_each() { + pfcli::flush_rules(ANCHOR_NAME, pfcli::FlushOptions::All); + pfctl::PfCtl::new() + .unwrap() + .try_remove_anchor(ANCHOR_NAME, pfctl::AnchorKind::Scrub) + .unwrap(); +} + +fn scrub_rule() -> pfctl::ScrubRule { + pfctl::ScrubRuleBuilder::default() + .action(pfctl::ScrubRuleAction::Scrub) + .build() + .unwrap() +} + +fn no_scrub_rule() -> pfctl::ScrubRule { + pfctl::ScrubRuleBuilder::default() + .action(pfctl::ScrubRuleAction::NoScrub) + .build() + .unwrap() +} + +test!(flush_scrub_rules { + let mut pf = pfctl::PfCtl::new().unwrap(); + let test_rules = [scrub_rule(), no_scrub_rule()]; + for rule in test_rules.iter() { + assert_matches!(pf.add_scrub_rule(ANCHOR_NAME, rule), Ok(())); + assert_eq!(pfcli::get_rules(ANCHOR_NAME).len(), 1); + + assert_matches!(pf.flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Scrub), Ok(())); + assert_eq!( + pfcli::get_rules(ANCHOR_NAME), + &[] as &[&str] + ); + } +}); + +test!(add_scrub_rule { + let mut pf = pfctl::PfCtl::new().unwrap(); + let rule = scrub_rule(); + assert_matches!(pf.add_scrub_rule(ANCHOR_NAME, &rule), Ok(())); + assert_eq!( + pfcli::get_rules(ANCHOR_NAME), + &["scrub all fragment reassemble"] + ); +}); + +test!(add_no_scrub_rule { + let mut pf = pfctl::PfCtl::new().unwrap(); + let rule = no_scrub_rule(); + assert_matches!(pf.add_scrub_rule(ANCHOR_NAME, &rule), Ok(())); + assert_eq!( + pfcli::get_rules(ANCHOR_NAME), + &["no scrub all"] + ); +}); diff --git a/tests/transaction.rs b/tests/transaction.rs index 59bad35..eb29d3e 100644 --- a/tests/transaction.rs +++ b/tests/transaction.rs @@ -8,7 +8,8 @@ use std::net::Ipv4Addr; const ANCHOR1_NAME: &str = "pfctl-rs.integration.testing.transactions-1"; const ANCHOR2_NAME: &str = "pfctl-rs.integration.testing.transactions-2"; -const ANCHORS: [&str; 2] = [ANCHOR1_NAME, ANCHOR2_NAME]; +const ANCHOR3_NAME: &str = "pfctl-rs.integration.testing.transactions-3"; +const ANCHORS: [&str; 3] = [ANCHOR1_NAME, ANCHOR2_NAME, ANCHOR3_NAME]; fn before_each() { for anchor_name in ANCHORS.iter() { @@ -20,6 +21,10 @@ fn before_each() { .unwrap() .try_add_anchor(anchor_name, pfctl::AnchorKind::Redirect) .unwrap(); + pfctl::PfCtl::new() + .unwrap() + .try_add_anchor(anchor_name, pfctl::AnchorKind::Scrub) + .unwrap(); } } @@ -35,6 +40,10 @@ fn after_each() { .unwrap() .try_remove_anchor(anchor_name, pfctl::AnchorKind::Redirect) .unwrap(); + pfctl::PfCtl::new() + .unwrap() + .try_remove_anchor(anchor_name, pfctl::AnchorKind::Scrub) + .unwrap(); } } @@ -70,6 +79,18 @@ fn get_redirect_rules() -> Vec { vec![rdr_rule1, rdr_rule2] } +fn get_scrub_rules() -> Vec { + let scrub_rule1 = pfctl::ScrubRuleBuilder::default() + .action(pfctl::ScrubRuleAction::Scrub) + .build() + .unwrap(); + let scrub_rule2 = pfctl::ScrubRuleBuilder::default() + .action(pfctl::ScrubRuleAction::NoScrub) + .build() + .unwrap(); + vec![scrub_rule1, scrub_rule2] +} + fn get_marker_filter_rule() -> pfctl::FilterRule { pfctl::FilterRuleBuilder::default() .action(pfctl::FilterRuleAction::Pass) @@ -87,8 +108,10 @@ fn get_marker_redirect_rule() -> pfctl::RedirectRule { } fn verify_filter_rules(anchor: &str) { + let rules = get_rules_filtered(anchor, |rule| !rule.contains("scrub")); + assert_eq!( - pfcli::get_rules(anchor), + rules, &[ "pass inet from any to 1.2.3.4 no state", "pass inet from any to 9.8.7.6 no state", @@ -96,6 +119,19 @@ fn verify_filter_rules(anchor: &str) { ); } +fn verify_scrub_rules(anchor: &str) { + let rules = get_rules_filtered(anchor, |rule| rule.contains("scrub")); + + assert_eq!(rules, &["scrub all fragment reassemble", "no scrub all",],); +} + +fn get_rules_filtered(anchor: &str, filter: impl Fn(&str) -> bool) -> Vec { + pfcli::get_rules(anchor) + .into_iter() + .filter(|rule| filter(rule)) + .collect::>() +} + fn verify_redirect_rules(anchor: &str) { assert_eq!( pfcli::get_nat_rules(anchor), @@ -124,10 +160,12 @@ test!(replace_many_rulesets_in_one_anchor { let mut change = pfctl::AnchorChange::new(); change.set_filter_rules(get_filter_rules()); change.set_redirect_rules(get_redirect_rules()); + change.set_scrub_rules(get_scrub_rules()); pf.set_rules(ANCHOR1_NAME, change).unwrap(); verify_filter_rules(ANCHOR1_NAME); + verify_scrub_rules(ANCHOR1_NAME); verify_redirect_rules(ANCHOR1_NAME); }); @@ -157,15 +195,20 @@ test!(replace_one_ruleset_in_many_anchors { let mut change2 = pfctl::AnchorChange::new(); change2.set_filter_rules(get_filter_rules()); + let mut change3 = pfctl::AnchorChange::new(); + change3.set_scrub_rules(get_scrub_rules()); + // create and run transaction let mut trans = pfctl::Transaction::new(); trans.add_change(ANCHOR1_NAME, change1); trans.add_change(ANCHOR2_NAME, change2); + trans.add_change(ANCHOR3_NAME, change3); assert_matches!(trans.commit(), Ok(())); // do final rules verification after transaction verify_filter_marker(ANCHOR1_NAME); verify_redirect_rules(ANCHOR1_NAME); verify_filter_rules(ANCHOR2_NAME); + verify_scrub_rules(ANCHOR3_NAME); verify_redirect_marker(ANCHOR2_NAME); });