Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for scrub anchors and rules #112

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading