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 connection state methods #106

Merged
merged 3 commits into from
Jul 24, 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 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
Loading