Skip to content

Commit

Permalink
Merge branch 'add-scrub-anchors'
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Sep 4, 2024
2 parents ed875b5 + f9d446f commit ce047a2
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion examples/add_anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
9 changes: 8 additions & 1 deletion examples/add_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand All @@ -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:");
Expand Down
4 changes: 4 additions & 0 deletions examples/flush_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions examples/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ 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<AnchorKind> for u8 {
fn from(anchor_kind: AnchorKind) -> 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,
}
}
}
15 changes: 14 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ffi::pfvar::pfioc_rule>() };

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()
Expand Down Expand Up @@ -476,7 +489,7 @@ impl PfCtl {
///
/// - Returns Result<R> from call to closure on match.
/// - Returns `ErrorKind::AnchorDoesNotExist` on mismatch, the closure is not called in that
/// case.
/// case.
fn with_anchor_rule<F, R>(&self, name: &str, kind: AnchorKind, f: F) -> Result<R>
where
F: FnOnce(ffi::pfvar::pfioc_rule) -> Result<R>,
Expand Down
19 changes: 19 additions & 0 deletions src/rule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,25 @@ impl TryCopyTo<ffi::pfvar::pf_rule> 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<ffi::pfvar::pf_rule> 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<AddrFamily> {
match (af1, af2) {
(af1, af2) if af1 == af2 => Ok(af1),
Expand Down
16 changes: 16 additions & 0 deletions src/rule/rule_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,19 @@ impl From<RedirectRuleAction> 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<ScrubRuleAction> 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,
}
}
}
3 changes: 3 additions & 0 deletions src/ruleset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ 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<RulesetKind> for i32 {
fn from(ruleset_kind: RulesetKind) -> Self {
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,
}
}
}
49 changes: 49 additions & 0 deletions src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use crate::{
conversion::TryCopyTo, ffi, utils, FilterRule, PoolAddrList, RedirectRule, Result, RulesetKind,
ScrubRule,
};
use std::{
collections::HashMap,
Expand Down Expand Up @@ -69,6 +70,16 @@ impl Transaction {
.map(|rules| (anchor.clone(), rules))
})
.collect();
let scrub_changes: Vec<(String, Vec<ScrubRule>)> = 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
Expand All @@ -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::<Result<_>>()?;
Self::setup_trans(&mut pfioc_trans, pfioc_elements.as_mut_slice());

Expand Down Expand Up @@ -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))
}

Expand Down Expand Up @@ -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::<ffi::pfvar::pfioc_rule>() };
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,
Expand Down Expand Up @@ -200,6 +243,7 @@ impl Transaction {
pub struct AnchorChange {
filter_rules: Option<Vec<FilterRule>>,
redirect_rules: Option<Vec<RedirectRule>>,
scrub_rules: Option<Vec<ScrubRule>>,
}

impl Default for AnchorChange {
Expand All @@ -214,6 +258,7 @@ impl AnchorChange {
AnchorChange {
filter_rules: None,
redirect_rules: None,
scrub_rules: None,
}
}

Expand All @@ -224,4 +269,8 @@ impl AnchorChange {
pub fn set_redirect_rules(&mut self, rules: Vec<RedirectRule>) {
self.redirect_rules = Some(rules);
}

pub fn set_scrub_rules(&mut self, rules: Vec<ScrubRule>) {
self.scrub_rules = Some(rules);
}
}
72 changes: 72 additions & 0 deletions tests/scrub_rules.rs
Original file line number Diff line number Diff line change
@@ -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"]
);
});
Loading

0 comments on commit ce047a2

Please sign in to comment.