Skip to content

Commit

Permalink
Merge branch 'add-nat-rule'
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon committed Oct 2, 2024
2 parents 18deb13 + 678ee52 commit e6666bd
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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 NAT anchors and rules.


## [0.6.0] - 2024-09-04
Expand Down
4 changes: 4 additions & 0 deletions examples/flush_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::ffi;
#[non_exhaustive]
pub enum AnchorKind {
Filter,
Nat,
Redirect,
Scrub,
}
Expand All @@ -21,6 +22,7 @@ impl From<AnchorKind> 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,
}
Expand Down
28 changes: 28 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,33 @@ 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::<ffi::pfvar::pfioc_rule>() };
utils::copy_anchor_name(anchor, &mut pfioc_rule.anchor[..])?;
rule.try_copy_to(&mut pfioc_rule.rule)?;

let pool_ticket = utils::get_pool_ticket(self.fd())?;

if let Some(nat_to) = rule.get_nat_to() {
// register NAT address in newly created address pool
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::<ffi::pfvar::pfioc_rule>() };
Expand Down Expand Up @@ -402,6 +429,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()),
};
Expand Down
109 changes: 108 additions & 1 deletion src/rule/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use crate::{
ffi, Error, ErrorInternal, Result,
};
use ipnetwork::IpNetwork;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
ops::Deref,
};

mod addr_family;
pub use self::addr_family::*;
Expand Down Expand Up @@ -159,6 +162,110 @@ impl TryCopyTo<ffi::pfvar::pf_rule> 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,
}

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<AddrFamily> {
let endpoint_af = compatible_af(self.from.get_af(), self.to.get_af())?;
if let Some(nat_to) = self.get_nat_to() {
let nat_af = compatible_af(endpoint_af, nat_to.0.get_af())?;
compatible_af(self.af, nat_af)
} else {
compatible_af(self.af, endpoint_af)
}
}

/// Accessor for `nat_to`
pub fn get_nat_to(&self) -> Option<NatEndpoint> {
match self.action {
NatRuleAction::Nat { nat_to } => Some(nat_to),
NatRuleAction::NoNat => None,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NatEndpoint(Endpoint);

impl Deref for NatEndpoint {
type Target = Endpoint;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl From<Ip> 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 Default for NatEndpoint {
fn default() -> Self {
Self::from(Ip::Any)
}
}

impl From<Endpoint> for NatEndpoint {
fn from(endpoint: Endpoint) -> Self {
Self(endpoint)
}
}

impl From<Ipv4Addr> for NatEndpoint {
fn from(ip: Ipv4Addr) -> Self {
Self::from(Ip::from(ip))
}
}

impl From<Ipv6Addr> for NatEndpoint {
fn from(ip: Ipv6Addr) -> Self {
Self::from(Ip::from(ip))
}
}

impl TryCopyTo<ffi::pfvar::pf_rule> 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"))]
Expand Down
18 changes: 17 additions & 1 deletion src/rule/rule_action.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 crate::ffi;
use crate::{ffi, NatEndpoint};

/// Enum describing what should happen to a packet that matches a filter rule.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -58,6 +58,22 @@ impl From<DropAction> 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 { nat_to: NatEndpoint },
NoNat,
}

impl From<NatRuleAction> for u8 {
fn from(rule_action: NatRuleAction) -> Self {
match rule_action {
NatRuleAction::Nat { .. } => ffi::pfvar::PF_NAT as u8,
NatRuleAction::NoNat => ffi::pfvar::PF_NONAT 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 {
Expand Down
2 changes: 2 additions & 0 deletions src/ruleset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::ffi;
#[non_exhaustive]
pub enum RulesetKind {
Filter,
Nat,
Redirect,
Scrub,
}
Expand All @@ -21,6 +22,7 @@ impl From<RulesetKind> 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,
}
Expand Down
58 changes: 56 additions & 2 deletions src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -60,6 +60,13 @@ impl Transaction {
.map(|rules| (anchor.clone(), rules))
})
.collect();
let nat_changes: Vec<(String, Vec<NatRule>)> = 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<RedirectRule>)> = self
.change_by_anchor
.iter_mut()
Expand Down Expand Up @@ -87,6 +94,11 @@ impl Transaction {
let mut pfioc_elements: Vec<ffi::pfvar::pfioc_trans_pfioc_trans_e> = 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()
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -170,6 +191,33 @@ 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::<ffi::pfvar::pfioc_rule>() };
utils::copy_anchor_name(anchor, &mut pfioc_rule.anchor[..])?;
rule.try_copy_to(&mut pfioc_rule.rule)?;

let pool_ticket = utils::get_pool_ticket(fd)?;

if let Some(nat_to) = rule.get_nat_to() {
// register NAT address in newly created address pool
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
Expand Down Expand Up @@ -242,6 +290,7 @@ impl Transaction {
#[derive(Debug)]
pub struct AnchorChange {
filter_rules: Option<Vec<FilterRule>>,
nat_rules: Option<Vec<NatRule>>,
redirect_rules: Option<Vec<RedirectRule>>,
scrub_rules: Option<Vec<ScrubRule>>,
}
Expand All @@ -257,6 +306,7 @@ impl AnchorChange {
pub fn new() -> Self {
AnchorChange {
filter_rules: None,
nat_rules: None,
redirect_rules: None,
scrub_rules: None,
}
Expand All @@ -266,6 +316,10 @@ impl AnchorChange {
self.filter_rules = Some(rules);
}

pub fn set_nat_rules(&mut self, rules: Vec<NatRule>) {
self.nat_rules = Some(rules);
}

pub fn set_redirect_rules(&mut self, rules: Vec<RedirectRule>) {
self.redirect_rules = Some(rules);
}
Expand Down
2 changes: 2 additions & 0 deletions tests/helper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ macro_rules! test {
($name:ident $block:block) => {
#[test]
fn $name() {
eprintln!("NOTE: Make sure there are not other PF rules interfering with this test.");

let mut pf_state = helper::PfState::new();
pf_state.save();

Expand Down
6 changes: 5 additions & 1 deletion tests/helper/pfcli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ pub fn is_enabled() -> bool {
} else if str.starts_with("Status: Disabled") {
false
} else {
panic!("Invalid response.");
let stderr = str_from_stdout(&output.stderr);
panic!(
"Invalid output from pfctl ({}), stdout:\n{str}\nstderr:\n{stderr}",
output.status
);
}
}

Expand Down
Loading

0 comments on commit e6666bd

Please sign in to comment.