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 NAT anchors and rules #114

Merged
merged 5 commits into from
Oct 2, 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
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
Loading