Skip to content

Commit

Permalink
Merge branch 'improve-state-flush'
Browse files Browse the repository at this point in the history
  • Loading branch information
dlon authored and faern committed Jul 24, 2024
2 parents cb212b2 + 7bae6e8 commit c3b94b0
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 24 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 function for listing all states created by PF anchor rules.
- Add function for removing individual states created by PF anchor rules.

### Changed
* Upgrade `ipnetwork` dependency from 0.16 to 0.20. This is a breaking change since
`ipnetwork` is part of the public API.
Expand Down
47 changes: 45 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub use crate::anchor::*;
mod ruleset;
pub use crate::ruleset::*;

mod state;
pub use crate::state::*;

mod transaction;
pub use crate::transaction::*;

Expand All @@ -110,6 +113,12 @@ pub enum ErrorKind {
InvalidPortRange,
/// The supplied rule label is not compatible with PF.
InvalidLabel,
/// The address family is invalid
InvalidAddressFamily,
/// The direction is invalid
InvalidDirection,
/// The transport protocol is invalid
InvalidTransportProtocol,
/// The target state was already active
StateAlreadyActive,
/// This PF anchor does not exist
Expand All @@ -130,6 +139,9 @@ enum ErrorInternal {
InvalidAnchorName(&'static str),
InvalidPortRange,
InvalidLabel(&'static str),
InvalidAddressFamily(u8),
InvalidDirection(u8),
InvalidTransportProtocol(u8),
StateAlreadyActive,
AnchorDoesNotExist,
Ioctl(std::io::Error),
Expand All @@ -147,6 +159,9 @@ impl Error {
InvalidAnchorName(..) => ErrorKind::InvalidAnchorName,
InvalidPortRange => ErrorKind::InvalidPortRange,
InvalidLabel(..) => ErrorKind::InvalidLabel,
InvalidAddressFamily(_) => ErrorKind::InvalidAddressFamily,
InvalidDirection(_) => ErrorKind::InvalidDirection,
InvalidTransportProtocol(_) => ErrorKind::InvalidTransportProtocol,
StateAlreadyActive => ErrorKind::StateAlreadyActive,
AnchorDoesNotExist => ErrorKind::AnchorDoesNotExist,
Ioctl(_) => ErrorKind::Ioctl,
Expand All @@ -173,6 +188,11 @@ impl fmt::Display for Error {
InvalidAnchorName(reason) => write!(f, "Invalid anchor name ({reason})"),
InvalidPortRange => write!(f, "Lower port is greater than upper port"),
InvalidLabel(reason) => write!(f, "Invalid rule label ({reason}"),
InvalidAddressFamily(family) => write!(f, "Invalid address family ({family})"),
InvalidDirection(direction) => write!(f, "Invalid direction ({direction})"),
InvalidTransportProtocol(protocol) => {
write!(f, "Invalid transport protocol ({protocol})")
}
StateAlreadyActive => write!(f, "Target state is already active"),
AnchorDoesNotExist => write!(f, "Anchor does not exist"),
Ioctl(_) => write!(f, "Error during ioctl syscall"),
Expand Down Expand Up @@ -380,7 +400,7 @@ impl PfCtl {
/// Returns total number of removed states upon success, otherwise
/// ErrorKind::AnchorDoesNotExist if anchor does not exist.
pub fn clear_states(&mut self, anchor_name: &str, kind: AnchorKind) -> Result<u32> {
let pfsync_states = self.get_states()?;
let pfsync_states = self.get_states_inner()?;
if !pfsync_states.is_empty() {
self.with_anchor_rule(anchor_name, kind, |anchor_rule| {
pfsync_states
Expand Down Expand Up @@ -414,7 +434,30 @@ impl PfCtl {
}

/// Get all states created by stateful rules
fn get_states(&mut self) -> Result<Vec<ffi::pfvar::pfsync_state>> {
pub fn get_states(&mut self) -> Result<Vec<State>> {
let wrapped_states = self
.get_states_inner()?
.into_iter()
.map(|state| {
// SAFETY: `state` is zero-initialized by `setup_pfioc_states`.
unsafe { State::new(state) }
})
.collect();
Ok(wrapped_states)
}

/// Remove the specified state.
///
/// All current states can be obtained via [get_states].
pub fn kill_state(&mut self, state: &State) -> Result<()> {
let mut pfioc_state_kill = unsafe { mem::zeroed::<ffi::pfvar::pfioc_state_kill>() };
setup_pfioc_state_kill(state.as_raw(), &mut pfioc_state_kill);
ioctl_guard!(ffi::pf_kill_states(self.fd(), &mut pfioc_state_kill))?;
Ok(())
}

/// Get all states created by stateful rules
fn get_states_inner(&mut self) -> Result<Vec<ffi::pfvar::pfsync_state>> {
let num_states = self.get_num_states()?;
if num_states > 0 {
let (mut pfioc_states, pfsync_states) = setup_pfioc_states(num_states);
Expand Down
28 changes: 19 additions & 9 deletions src/rule/addr_family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,21 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use crate::ffi;
use crate::{ffi, Error, ErrorInternal, Result};
use std::fmt;

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum AddrFamily {
#[default]
Any,
Ipv4,
Ipv6,
Any = ffi::pfvar::PF_UNSPEC as u8,
Ipv4 = ffi::pfvar::PF_INET as u8,
Ipv6 = ffi::pfvar::PF_INET6 as u8,
}

impl From<AddrFamily> for u8 {
fn from(af: AddrFamily) -> Self {
match af {
AddrFamily::Any => ffi::pfvar::PF_UNSPEC as u8,
AddrFamily::Ipv4 => ffi::pfvar::PF_INET as u8,
AddrFamily::Ipv6 => ffi::pfvar::PF_INET6 as u8,
}
af as u8
}
}

Expand All @@ -37,3 +34,16 @@ impl fmt::Display for AddrFamily {
.fmt(f)
}
}

impl TryFrom<u8> for AddrFamily {
type Error = crate::Error;

fn try_from(family: u8) -> Result<Self> {
match family {
v if v == AddrFamily::Any as u8 => Ok(AddrFamily::Any),
v if v == AddrFamily::Ipv4 as u8 => Ok(AddrFamily::Ipv4),
v if v == AddrFamily::Ipv6 as u8 => Ok(AddrFamily::Ipv6),
_ => Err(Error::from(ErrorInternal::InvalidAddressFamily(family))),
}
}
}
24 changes: 17 additions & 7 deletions src/rule/direction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,33 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use crate::ffi;
use crate::{ffi, Error, ErrorInternal, Result};

/// Enum describing matching of rule towards packet flow direction.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Direction {
#[default]
Any,
In,
Out,
Any = ffi::pfvar::PF_INOUT as u8,
In = ffi::pfvar::PF_IN as u8,
Out = ffi::pfvar::PF_OUT as u8,
}

impl From<Direction> for u8 {
fn from(direction: Direction) -> Self {
direction as u8
}
}

impl TryFrom<u8> for Direction {
type Error = crate::Error;

fn try_from(direction: u8) -> Result<Self> {
match direction {
Direction::Any => ffi::pfvar::PF_INOUT as u8,
Direction::In => ffi::pfvar::PF_IN as u8,
Direction::Out => ffi::pfvar::PF_OUT as u8,
v if v == Direction::Any as u8 => Ok(Direction::Any),
v if v == Direction::In as u8 => Ok(Direction::In),
v if v == Direction::Out as u8 => Ok(Direction::Out),
other => Err(Error::from(ErrorInternal::InvalidDirection(other))),
}
}
}
28 changes: 23 additions & 5 deletions src/rule/proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use crate::{Error, ErrorInternal, Result};

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Proto {
#[default]
Any,
Tcp,
Udp,
Icmp,
IcmpV6,
Any = libc::IPPROTO_IP as u8,
Tcp = libc::IPPROTO_TCP as u8,
Udp = libc::IPPROTO_UDP as u8,
Icmp = libc::IPPROTO_ICMP as u8,
IcmpV6 = libc::IPPROTO_ICMPV6 as u8,
}

impl From<Proto> for u8 {
Expand All @@ -27,3 +30,18 @@ impl From<Proto> for u8 {
}
}
}

impl TryFrom<u8> for Proto {
type Error = crate::Error;

fn try_from(proto: u8) -> Result<Self> {
match proto {
v if v == Proto::Any as u8 => Ok(Proto::Any),
v if v == Proto::Tcp as u8 => Ok(Proto::Tcp),
v if v == Proto::Udp as u8 => Ok(Proto::Udp),
v if v == Proto::Icmp as u8 => Ok(Proto::Icmp),
v if v == Proto::IcmpV6 as u8 => Ok(Proto::IcmpV6),
_ => Err(Error::from(ErrorInternal::InvalidTransportProtocol(proto))),
}
}
}
126 changes: 126 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};

use crate::ffi::pfvar::pfsync_state_host;
use crate::{ffi::pfvar::pfsync_state, Direction, Proto};
use crate::{Error, ErrorInternal, Result};

/// PF connection state created by a stateful rule
#[derive(Clone)]
pub struct State {
sync_state: pfsync_state,
}

// Manually derive `Debug` since `pfsync_state` contains unions.
impl fmt::Debug for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("State")
.field("direction", &self.direction())
.field("proto", &self.proto())
.field("local_address", &self.local_address())
.field("remote_address", &self.remote_address())
.finish()
}
}

impl State {
/// Wrap a `pfsync_state` so that it can be accessed safely.
///
/// # Safety
///
/// All bytes in `sync_state` must be initialized.
pub(crate) unsafe fn new(sync_state: pfsync_state) -> State {
State { sync_state }
}

/// Return the direction for this state
pub fn direction(&self) -> Result<Direction> {
Direction::try_from(self.sync_state.direction)
}

/// Return the transport protocol for this state
pub fn proto(&self) -> Result<Proto> {
Proto::try_from(self.sync_state.proto)
}

/// Return the local socket address for this state
pub fn local_address(&self) -> Result<SocketAddr> {
// SAFETY: The address and port are initialized according to the contract of `Self::new`.
unsafe { parse_address(self.sync_state.af_lan, self.sync_state.lan) }
}

/// Return the remote socket address for this state
pub fn remote_address(&self) -> Result<SocketAddr> {
// SAFETY: The address and port are initialized according to the contract of `Self::new`.
unsafe { parse_address(self.sync_state.af_lan, self.sync_state.ext_lan) }
}

/// Return a reference to the inner `pfsync_state` state
pub(crate) fn as_raw(&self) -> &pfsync_state {
&self.sync_state
}
}

/// Parse an IP address and port from a `pfsync_sync_host`, normally provided by `pfsync_state`.
///
/// # Safety
///
/// `host` must contain a valid address and a port:
/// * If `family == PF_INET`, then `host.addr.pfa._v4addr` must be initialized.
/// * If `family == PF_INET6`, then `host.addr.pfa._v6addr` must be initialized.
/// * `host.xport.port` must always be initialized.
unsafe fn parse_address(family: u8, host: pfsync_state_host) -> Result<SocketAddr> {
let ip = match u32::from(family) {
crate::ffi::pfvar::PF_INET => {
// SAFETY: The caller has initialized this memory
Ipv4Addr::from(u32::from_be(unsafe { host.addr.pfa._v4addr.s_addr })).into()
}
crate::ffi::pfvar::PF_INET6 => {
// SAFETY: The caller has initialized this memory
Ipv6Addr::from(unsafe { host.addr.pfa._v6addr.__u6_addr.__u6_addr8 }).into()
}
_ => return Err(Error::from(ErrorInternal::InvalidAddressFamily(family))),
};

// SAFETY: The caller has initialized this memory
let port = u16::from_be(unsafe { host.xport.port });

Ok(SocketAddr::new(ip, port))
}

#[cfg(test)]
mod tests {
use super::pfsync_state_host;
use crate::{state::parse_address, AddrFamily};
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};

#[test]
fn test_parse_ipv4_address() {
const EXPECTED_IP: Ipv4Addr = Ipv4Addr::new(1, 2, 3, 4);
const EXPECTED_PORT: u16 = 12345;

let mut host: pfsync_state_host = unsafe { std::mem::zeroed() };
host.addr.pfa._v4addr.s_addr = u32::from_be_bytes(EXPECTED_IP.octets()).to_be();
host.xport.port = EXPECTED_PORT.to_be();

let family = u8::from(AddrFamily::Ipv4);

let address = unsafe { parse_address(family, host) }.unwrap();
assert_eq!(address, SocketAddr::new(EXPECTED_IP.into(), EXPECTED_PORT));
}

#[test]
fn test_parse_ipv6_address() {
const EXPECTED_IP: Ipv6Addr = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 0x7f);
const EXPECTED_PORT: u16 = 12345;

let mut host: pfsync_state_host = unsafe { std::mem::zeroed() };
host.addr.pfa._v6addr.__u6_addr.__u6_addr8 = EXPECTED_IP.octets();
host.xport.port = EXPECTED_PORT.to_be();

let family = u8::from(AddrFamily::Ipv6);

let address = unsafe { parse_address(family, host) }.unwrap();
assert_eq!(address, SocketAddr::new(EXPECTED_IP.into(), EXPECTED_PORT));
}
}
Loading

0 comments on commit c3b94b0

Please sign in to comment.