From 5b5dd0b88ed494f41c1b0e650006e3e9abf4bd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 25 Sep 2024 15:35:30 +0200 Subject: [PATCH] Add NAT rule type --- examples/flush_rules.rs | 4 ++ src/anchor.rs | 2 + src/lib.rs | 26 +++++++++++++ src/rule/mod.rs | 84 +++++++++++++++++++++++++++++++++++++++++ src/rule/rule_action.rs | 14 +++++++ src/ruleset.rs | 2 + src/transaction.rs | 56 ++++++++++++++++++++++++++- tests/transaction.rs | 43 +++++++++++++++++++-- 8 files changed, 225 insertions(+), 6 deletions(-) diff --git a/examples/flush_rules.rs b/examples/flush_rules.rs index 6425ea2..c67150f 100644 --- a/examples/flush_rules.rs +++ b/examples/flush_rules.rs @@ -17,6 +17,10 @@ fn main() { .expect("Unable to flush filter rules"); println!("Flushed filter rules under anchor {}", anchor_name); + pf.flush_rules(&anchor_name, pfctl::RulesetKind::Nat) + .expect("Unable to flush nat rules"); + println!("Flushed nat rules under anchor {}", anchor_name); + pf.flush_rules(&anchor_name, pfctl::RulesetKind::Redirect) .expect("Unable to flush redirect rules"); println!("Flushed redirect rules under anchor {}", anchor_name); diff --git a/src/anchor.rs b/src/anchor.rs index 6fb2f3e..ef7fa1d 100644 --- a/src/anchor.rs +++ b/src/anchor.rs @@ -13,6 +13,7 @@ use crate::ffi; #[non_exhaustive] pub enum AnchorKind { Filter, + Nat, Redirect, Scrub, } @@ -21,6 +22,7 @@ impl From for u8 { fn from(anchor_kind: AnchorKind) -> u8 { match anchor_kind { AnchorKind::Filter => ffi::pfvar::PF_PASS as u8, + AnchorKind::Nat => ffi::pfvar::PF_NAT 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 97a8477..e1428bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -360,6 +360,31 @@ impl PfCtl { trans.commit() } + pub fn add_nat_rule(&mut self, anchor: &str, rule: &NatRule) -> 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)?; + + // register NAT address in newly created address pool + let nat_to = rule.get_nat_to(); + let pool_ticket = utils::get_pool_ticket(self.fd())?; + utils::add_pool_address(self.fd(), nat_to.ip(), pool_ticket)?; + + // copy address pool in pf_rule + let nat_pool = nat_to.ip().to_pool_addr_list()?; + pfioc_rule.rule.rpool.list = unsafe { nat_pool.to_palist() }; + nat_to.port().try_copy_to(&mut pfioc_rule.rule.rpool)?; + + // set tickets + pfioc_rule.pool_ticket = pool_ticket; + pfioc_rule.ticket = utils::get_ticket(self.fd(), anchor, AnchorKind::Nat)?; + + // append 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 add_redirect_rule(&mut self, anchor: &str, rule: &RedirectRule) -> Result<()> { // prepare pfioc_rule let mut pfioc_rule = unsafe { mem::zeroed::() }; @@ -402,6 +427,7 @@ impl PfCtl { let mut anchor_change = AnchorChange::new(); match kind { RulesetKind::Filter => anchor_change.set_filter_rules(Vec::new()), + RulesetKind::Nat => anchor_change.set_nat_rules(Vec::new()), RulesetKind::Redirect => anchor_change.set_redirect_rules(Vec::new()), RulesetKind::Scrub => anchor_change.set_scrub_rules(Vec::new()), }; diff --git a/src/rule/mod.rs b/src/rule/mod.rs index 88290ee..18f1879 100644 --- a/src/rule/mod.rs +++ b/src/rule/mod.rs @@ -159,6 +159,90 @@ impl TryCopyTo for FilterRule { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_builder::Builder)] +#[builder(setter(into))] +#[builder(build_fn(error = "Error"))] +pub struct NatRule { + action: NatRuleAction, + #[builder(default)] + interface: Interface, + #[builder(default)] + af: AddrFamily, + #[builder(default)] + from: Endpoint, + #[builder(default)] + to: Endpoint, + nat_to: NatEndpoint, +} + +impl NatRule { + /// Returns the `AddrFamily` this rule matches against. Returns an `InvalidRuleCombination` + /// error if this rule has an invalid combination of address families. + fn get_af(&self) -> Result { + let endpoint_af = compatible_af(self.from.get_af(), self.to.get_af())?; + let nat_af = compatible_af(endpoint_af, self.nat_to.0.get_af())?; + compatible_af(self.af, nat_af) + } + + /// Accessor for `nat_to` + pub fn get_nat_to(&self) -> Endpoint { + self.nat_to.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct NatEndpoint(Endpoint); + +impl From for NatEndpoint { + fn from(ip: Ip) -> Self { + // Default NAT port range + const NAT_LOWER_DEFAULT: u16 = 32768; + const NAT_UPPER_DEFAULT: u16 = 49151; + + Self(Endpoint::new( + ip, + Port::Range( + NAT_LOWER_DEFAULT, + NAT_UPPER_DEFAULT, + PortRangeModifier::Inclusive, + ), + )) + } +} + +impl From for NatEndpoint { + fn from(endpoint: Endpoint) -> Self { + Self(endpoint) + } +} + +impl From for NatEndpoint { + fn from(ip: Ipv4Addr) -> Self { + Self::from(Ip::from(ip)) + } +} + +impl From for NatEndpoint { + fn from(ip: Ipv6Addr) -> Self { + Self::from(Ip::from(ip)) + } +} + +impl TryCopyTo for NatRule { + type Error = crate::Error; + + fn try_copy_to(&self, pf_rule: &mut ffi::pfvar::pf_rule) -> Result<()> { + pf_rule.action = self.action.into(); + self.interface.try_copy_to(&mut pf_rule.ifname)?; + pf_rule.af = self.get_af()?.into(); + + self.from.try_copy_to(&mut pf_rule.src)?; + self.to.try_copy_to(&mut pf_rule.dst)?; + + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, derive_builder::Builder)] #[builder(setter(into))] #[builder(build_fn(error = "Error"))] diff --git a/src/rule/rule_action.rs b/src/rule/rule_action.rs index d21c732..538d14b 100644 --- a/src/rule/rule_action.rs +++ b/src/rule/rule_action.rs @@ -58,6 +58,20 @@ impl From for u32 { } } +/// Enum describing what should happen to a packet that matches a NAT rule. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NatRuleAction { + Nat, +} + +impl From for u8 { + fn from(rule_action: NatRuleAction) -> Self { + match rule_action { + NatRuleAction::Nat => ffi::pfvar::PF_NAT as u8, + } + } +} + /// Enum describing what should happen to a packet that matches a redirect rule. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum RedirectRuleAction { diff --git a/src/ruleset.rs b/src/ruleset.rs index 9f9509f..cc0fab9 100644 --- a/src/ruleset.rs +++ b/src/ruleset.rs @@ -13,6 +13,7 @@ use crate::ffi; #[non_exhaustive] pub enum RulesetKind { Filter, + Nat, Redirect, Scrub, } @@ -21,6 +22,7 @@ impl From for i32 { fn from(ruleset_kind: RulesetKind) -> Self { match ruleset_kind { RulesetKind::Filter => ffi::pfvar::PF_RULESET_FILTER as i32, + RulesetKind::Nat => ffi::pfvar::PF_RULESET_NAT 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 07082d5..3d00dc7 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -7,8 +7,8 @@ // except according to those terms. use crate::{ - conversion::TryCopyTo, ffi, utils, FilterRule, PoolAddrList, RedirectRule, Result, RulesetKind, - ScrubRule, + conversion::TryCopyTo, ffi, utils, FilterRule, NatRule, PoolAddrList, RedirectRule, Result, + RulesetKind, ScrubRule, }; use std::{ collections::HashMap, @@ -60,6 +60,13 @@ impl Transaction { .map(|rules| (anchor.clone(), rules)) }) .collect(); + let nat_changes: Vec<(String, Vec)> = self + .change_by_anchor + .iter_mut() + .filter_map(|(anchor, change)| { + change.nat_rules.take().map(|rules| (anchor.clone(), rules)) + }) + .collect(); let redirect_changes: Vec<(String, Vec)> = self .change_by_anchor .iter_mut() @@ -87,6 +94,11 @@ impl Transaction { let mut pfioc_elements: Vec = filter_changes .iter() .map(|(anchor, _)| Self::new_trans_element(anchor, RulesetKind::Filter)) + .chain( + nat_changes + .iter() + .map(|(anchor, _)| Self::new_trans_element(anchor, RulesetKind::Nat)), + ) .chain( redirect_changes .iter() @@ -115,6 +127,15 @@ impl Transaction { } } + // add NAT rules into transaction + for ((anchor_name, nat_rules), ticket) in + nat_changes.into_iter().zip(ticket_iterator.by_ref()) + { + for nat_rule in nat_rules.iter() { + Self::add_nat_rule(fd, &anchor_name, nat_rule, ticket)?; + } + } + // add redirect rules into transaction for ((anchor_name, redirect_rules), ticket) in redirect_changes.into_iter().zip(ticket_iterator.by_ref()) @@ -170,6 +191,31 @@ impl Transaction { Ok(()) } + /// Internal helper to add nat rule into transaction + fn add_nat_rule(fd: RawFd, anchor: &str, rule: &NatRule, 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)?; + + // register NAT address in newly created address pool + let nat_to = rule.get_nat_to(); + let pool_ticket = utils::get_pool_ticket(fd)?; + utils::add_pool_address(fd, nat_to.ip(), pool_ticket)?; + + // copy address pool in pf_rule + let nat_pool = nat_to.ip().to_pool_addr_list()?; + pfioc_rule.rule.rpool.list = unsafe { nat_pool.to_palist() }; + nat_to.port().try_copy_to(&mut pfioc_rule.rule.rpool)?; + + // set tickets + pfioc_rule.pool_ticket = pool_ticket; + pfioc_rule.ticket = ticket; + + // add rule into transaction + ioctl_guard!(ffi::pf_add_rule(fd, &mut pfioc_rule)) + } + /// Internal helper to add redirect rule into transaction fn add_redirect_rule(fd: RawFd, anchor: &str, rule: &RedirectRule, ticket: u32) -> Result<()> { // prepare pfioc_rule @@ -242,6 +288,7 @@ impl Transaction { #[derive(Debug)] pub struct AnchorChange { filter_rules: Option>, + nat_rules: Option>, redirect_rules: Option>, scrub_rules: Option>, } @@ -257,6 +304,7 @@ impl AnchorChange { pub fn new() -> Self { AnchorChange { filter_rules: None, + nat_rules: None, redirect_rules: None, scrub_rules: None, } @@ -266,6 +314,10 @@ impl AnchorChange { self.filter_rules = Some(rules); } + pub fn set_nat_rules(&mut self, rules: Vec) { + self.nat_rules = Some(rules); + } + pub fn set_redirect_rules(&mut self, rules: Vec) { self.redirect_rules = Some(rules); } diff --git a/tests/transaction.rs b/tests/transaction.rs index eb29d3e..6d906e2 100644 --- a/tests/transaction.rs +++ b/tests/transaction.rs @@ -9,7 +9,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 ANCHOR3_NAME: &str = "pfctl-rs.integration.testing.transactions-3"; -const ANCHORS: [&str; 3] = [ANCHOR1_NAME, ANCHOR2_NAME, ANCHOR3_NAME]; +const ANCHOR4_NAME: &str = "pfctl-rs.integration.testing.transactions-4"; +const ANCHORS: [&str; 3] = [ANCHOR1_NAME, ANCHOR2_NAME, ANCHOR3_NAME, ANCHOR4_NAME]; fn before_each() { for anchor_name in ANCHORS.iter() { @@ -17,6 +18,10 @@ fn before_each() { .unwrap() .try_add_anchor(anchor_name, pfctl::AnchorKind::Filter) .unwrap(); + pfctl::PfCtl::new() + .unwrap() + .try_add_anchor(anchor_name, pfctl::AnchorKind::Nat) + .unwrap(); pfctl::PfCtl::new() .unwrap() .try_add_anchor(anchor_name, pfctl::AnchorKind::Redirect) @@ -61,6 +66,16 @@ fn get_filter_rules() -> Vec { vec![rule1, rule2] } +fn get_nat_rules() -> Vec { + let rule1 = pfctl::NatRuleBuilder::default() + .action(pfctl::NatRuleAction::Nat) + .to(Ipv4Addr::new(1, 2, 3, 4)) + .nat_to(Ipv4Addr::new(127, 0, 0, 1)) + .build() + .unwrap(); + vec![rule1] +} + fn get_redirect_rules() -> Vec { let rdr_rule1 = pfctl::RedirectRuleBuilder::default() .action(pfctl::RedirectRuleAction::Redirect) @@ -134,7 +149,7 @@ fn get_rules_filtered(anchor: &str, filter: impl Fn(&str) -> bool) -> Vec any port 4000", "rdr inet from 1.2.3.4 to any port = 5000 -> any port 6000", @@ -142,6 +157,20 @@ fn verify_redirect_rules(anchor: &str) { ); } +fn verify_nat_rules(anchor: &str) { + assert_eq!( + get_nat_rules_filtered(anchor, |rule| rule.contains("nat")), + &["nat inet from any to 1.2.3.4 -> 127.0.0.1",] + ); +} + +fn get_nat_rules_filtered(anchor: &str, filter: impl Fn(&str) -> bool) -> Vec { + pfcli::get_nat_rules(anchor) + .into_iter() + .filter(|rule| filter(rule)) + .collect::>() +} + fn verify_filter_marker(anchor: &str) { assert_eq!(pfcli::get_rules(anchor), &["pass all no state"]); } @@ -159,6 +188,7 @@ test!(replace_many_rulesets_in_one_anchor { let mut change = pfctl::AnchorChange::new(); change.set_filter_rules(get_filter_rules()); + change.set_nat_rules(get_nat_rules()); change.set_redirect_rules(get_redirect_rules()); change.set_scrub_rules(get_scrub_rules()); @@ -196,19 +226,24 @@ test!(replace_one_ruleset_in_many_anchors { change2.set_filter_rules(get_filter_rules()); let mut change3 = pfctl::AnchorChange::new(); - change3.set_scrub_rules(get_scrub_rules()); + change3.set_nat_rules(get_nat_rules()); + + let mut change4 = pfctl::AnchorChange::new(); + change4.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); + trans.add_change(ANCHOR4_NAME, change4); 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_nat_rules(ANCHOR3_NAME); + verify_scrub_rules(ANCHOR4_NAME); verify_redirect_marker(ANCHOR2_NAME); });