#![deny(rust_2018_idioms)]
use std::{
ffi::CStr,
fmt,
fs::File,
mem,
os::unix::io::{AsRawFd, RawFd},
slice,
};
pub use ipnetwork;
mod ffi;
#[macro_use]
mod macros;
mod utils;
mod rule;
pub use crate::rule::*;
mod pooladdr;
pub use crate::pooladdr::*;
mod anchor;
pub use crate::anchor::*;
mod ruleset;
pub use crate::ruleset::*;
mod state;
pub use crate::state::*;
mod transaction;
pub use crate::transaction::*;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
DeviceOpen,
InvalidRuleCombination,
UninitializedFieldError,
InvalidInterfaceName,
InvalidAnchorName,
InvalidPortRange,
InvalidLabel,
InvalidAddressFamily,
InvalidDirection,
InvalidTransportProtocol,
StateAlreadyActive,
AnchorDoesNotExist,
Ioctl,
}
#[derive(Debug)]
pub struct Error(ErrorInternal);
#[derive(Debug)]
enum ErrorInternal {
DeviceOpen(&'static str, std::io::Error),
InvalidRuleCombination(String),
UninitializedFieldError(derive_builder::UninitializedFieldError),
InvalidInterfaceName(&'static str),
InvalidAnchorName(&'static str),
InvalidPortRange,
InvalidLabel(&'static str),
InvalidAddressFamily(u8),
InvalidDirection(u8),
InvalidTransportProtocol(u8),
StateAlreadyActive,
AnchorDoesNotExist,
Ioctl(std::io::Error),
}
impl Error {
pub fn kind(&self) -> ErrorKind {
use ErrorInternal::*;
match self.0 {
DeviceOpen(..) => ErrorKind::DeviceOpen,
InvalidRuleCombination(_) => ErrorKind::InvalidRuleCombination,
UninitializedFieldError(_) => ErrorKind::UninitializedFieldError,
InvalidInterfaceName(..) => ErrorKind::InvalidInterfaceName,
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,
}
}
}
impl From<ErrorInternal> for Error {
fn from(e: ErrorInternal) -> Self {
Error(e)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ErrorInternal::*;
match &self.0 {
DeviceOpen(device_path, _) => {
write!(f, "Unable to open PF device file ({device_path})")
}
InvalidRuleCombination(msg) => write!(f, "Invalid rule combination: {msg}"),
UninitializedFieldError(inner) => inner.fmt(f),
InvalidInterfaceName(reason) => write!(f, "Invalid interface name ({reason})"),
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"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ErrorInternal::*;
match &self.0 {
DeviceOpen(_, e) => Some(e),
Ioctl(e) => Some(e),
_ => None,
}
}
}
impl From<derive_builder::UninitializedFieldError> for Error {
fn from(value: derive_builder::UninitializedFieldError) -> Self {
Error::from(ErrorInternal::UninitializedFieldError(value))
}
}
macro_rules! ignore_error_kind {
($result:expr, $kind:expr) => {
match $result {
Err(e) if e.kind() == $kind => Ok(()),
result => result,
}
};
}
mod conversion {
pub trait CopyTo<T: ?Sized> {
fn copy_to(&self, dst: &mut T);
}
pub trait TryCopyTo<T: ?Sized> {
type Error;
fn try_copy_to(&self, dst: &mut T) -> Result<(), Self::Error>;
}
}
use crate::conversion::*;
fn compare_cstr_safe(s: &str, c_chars: &[std::os::raw::c_char]) -> bool {
let c_chars_ptr = c_chars.as_ptr() as *const u8;
let c_chars_u8 = unsafe { slice::from_raw_parts(c_chars_ptr, c_chars.len()) };
let cs = CStr::from_bytes_until_nul(c_chars_u8)
.expect("System returned C String without terminating null byte");
s.as_bytes() == cs.to_bytes()
}
pub struct PfCtl {
file: File,
}
impl PfCtl {
pub fn new() -> Result<Self> {
let file = utils::open_pf()?;
Ok(PfCtl { file })
}
pub fn enable(&mut self) -> Result<()> {
ioctl_guard!(ffi::pf_start(self.fd()))
}
pub fn try_enable(&mut self) -> Result<()> {
ignore_error_kind!(self.enable(), ErrorKind::StateAlreadyActive)
}
pub fn disable(&mut self) -> Result<()> {
ioctl_guard!(ffi::pf_stop(self.fd()), libc::ENOENT)
}
pub fn try_disable(&mut self) -> Result<()> {
ignore_error_kind!(self.disable(), ErrorKind::StateAlreadyActive)
}
pub fn is_enabled(&mut self) -> Result<bool> {
let mut pf_status = unsafe { mem::zeroed::<ffi::pfvar::pf_status>() };
ioctl_guard!(ffi::pf_get_status(self.fd(), &mut pf_status))?;
Ok(pf_status.running == 1)
}
pub fn add_anchor(&mut self, name: &str, kind: AnchorKind) -> Result<()> {
let mut pfioc_rule = unsafe { mem::zeroed::<ffi::pfvar::pfioc_rule>() };
pfioc_rule.rule.action = kind.into();
utils::copy_anchor_name(name, &mut pfioc_rule.anchor_call[..])?;
ioctl_guard!(ffi::pf_insert_rule(self.fd(), &mut pfioc_rule))?;
Ok(())
}
pub fn try_add_anchor(&mut self, name: &str, kind: AnchorKind) -> Result<()> {
ignore_error_kind!(self.add_anchor(name, kind), ErrorKind::StateAlreadyActive)
}
pub fn remove_anchor(&mut self, name: &str, kind: AnchorKind) -> Result<()> {
self.with_anchor_rule(name, kind, |mut anchor_rule| {
ioctl_guard!(ffi::pf_delete_rule(self.fd(), &mut anchor_rule))
})
}
pub fn try_remove_anchor(&mut self, name: &str, kind: AnchorKind) -> Result<()> {
ignore_error_kind!(
self.remove_anchor(name, kind),
ErrorKind::AnchorDoesNotExist
)
}
pub fn add_rule(&mut self, anchor: &str, rule: &FilterRule) -> Result<()> {
let mut pfioc_rule = unsafe { mem::zeroed::<ffi::pfvar::pfioc_rule>() };
pfioc_rule.pool_ticket = utils::get_pool_ticket(self.fd())?;
pfioc_rule.ticket = utils::get_ticket(self.fd(), anchor, AnchorKind::Filter)?;
utils::copy_anchor_name(anchor, &mut pfioc_rule.anchor[..])?;
rule.try_copy_to(&mut pfioc_rule.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 set_rules(&mut self, anchor: &str, change: AnchorChange) -> Result<()> {
let mut trans = Transaction::new();
trans.add_change(anchor, change);
trans.commit()
}
pub fn add_nat_rule(&mut self, anchor: &str, rule: &NatRule) -> Result<()> {
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() {
utils::add_pool_address(self.fd(), nat_to.ip(), pool_ticket)?;
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)?;
}
pfioc_rule.pool_ticket = pool_ticket;
pfioc_rule.ticket = utils::get_ticket(self.fd(), anchor, AnchorKind::Nat)?;
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<()> {
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 redirect_to = rule.get_redirect_to();
let pool_ticket = utils::get_pool_ticket(self.fd())?;
utils::add_pool_address(self.fd(), redirect_to.ip(), pool_ticket)?;
let redirect_pool = redirect_to.ip().to_pool_addr_list()?;
pfioc_rule.rule.rpool.list = unsafe { redirect_pool.to_palist() };
redirect_to.port().try_copy_to(&mut pfioc_rule.rule.rpool)?;
pfioc_rule.pool_ticket = pool_ticket;
pfioc_rule.ticket = utils::get_ticket(self.fd(), anchor, AnchorKind::Redirect)?;
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_scrub_rule(&mut self, anchor: &str, rule: &ScrubRule) -> Result<()> {
let mut pfioc_rule = unsafe { mem::zeroed::<ffi::pfvar::pfioc_rule>() };
pfioc_rule.pool_ticket = utils::get_pool_ticket(self.fd())?;
pfioc_rule.ticket = utils::get_ticket(self.fd(), anchor, AnchorKind::Scrub)?;
utils::copy_anchor_name(anchor, &mut pfioc_rule.anchor[..])?;
rule.try_copy_to(&mut pfioc_rule.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 flush_rules(&mut self, anchor: &str, kind: RulesetKind) -> Result<()> {
let mut trans = Transaction::new();
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()),
};
trans.add_change(anchor, anchor_change);
trans.commit()
}
pub fn clear_states(&mut self, anchor_name: &str, kind: AnchorKind) -> Result<u32> {
let pfsync_states = self.get_states_inner()?;
if !pfsync_states.is_empty() {
self.with_anchor_rule(anchor_name, kind, |anchor_rule| {
pfsync_states
.iter()
.filter(|pfsync_state| pfsync_state.anchor == anchor_rule.nr)
.map(|pfsync_state| {
let mut pfioc_state_kill =
unsafe { mem::zeroed::<ffi::pfvar::pfioc_state_kill>() };
setup_pfioc_state_kill(pfsync_state, &mut pfioc_state_kill);
ioctl_guard!(ffi::pf_kill_states(self.fd(), &mut pfioc_state_kill))?;
Ok(pfioc_state_kill.psk_af as u32)
})
.collect::<Result<Vec<_>>>()
.map(|v| v.iter().sum())
})
} else {
Ok(0)
}
}
pub fn clear_interface_states(&mut self, interface: Interface) -> Result<u32> {
let mut pfioc_state_kill = unsafe { mem::zeroed::<ffi::pfvar::pfioc_state_kill>() };
interface.try_copy_to(&mut pfioc_state_kill.psk_ifname)?;
ioctl_guard!(ffi::pf_clear_states(self.fd(), &mut pfioc_state_kill))?;
Ok(pfioc_state_kill.psk_af as u32)
}
pub fn get_states(&mut self) -> Result<Vec<State>> {
let wrapped_states = self
.get_states_inner()?
.into_iter()
.map(|state| {
unsafe { State::new(state) }
})
.collect();
Ok(wrapped_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(())
}
pub fn set_interface_flag(
&mut self,
interface: Interface,
flags: InterfaceFlags,
) -> Result<()> {
let mut iface = unsafe { mem::zeroed::<ffi::pfvar::pfioc_iface>() };
interface.try_copy_to(&mut iface.pfiio_name)?;
iface.pfiio_flags = flags as i32;
ioctl_guard!(ffi::pf_set_iface_flag(self.fd(), &mut iface))?;
Ok(())
}
pub fn clear_interface_flag(
&mut self,
interface: Interface,
flags: InterfaceFlags,
) -> Result<()> {
let mut iface = unsafe { mem::zeroed::<ffi::pfvar::pfioc_iface>() };
interface.try_copy_to(&mut iface.pfiio_name)?;
iface.pfiio_flags = flags as i32;
ioctl_guard!(ffi::pf_clear_iface_flag(self.fd(), &mut iface))?;
Ok(())
}
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);
ioctl_guard!(ffi::pf_get_states(self.fd(), &mut pfioc_states))?;
Ok(pfsync_states)
} else {
Ok(vec![])
}
}
fn with_anchor_rule<F, R>(&self, name: &str, kind: AnchorKind, f: F) -> Result<R>
where
F: FnOnce(ffi::pfvar::pfioc_rule) -> Result<R>,
{
let mut pfioc_rule = unsafe { mem::zeroed::<ffi::pfvar::pfioc_rule>() };
pfioc_rule.rule.action = kind.into();
ioctl_guard!(ffi::pf_get_rules(self.fd(), &mut pfioc_rule))?;
pfioc_rule.action = ffi::pfvar::PF_GET_NONE as u32;
for i in 0..pfioc_rule.nr {
pfioc_rule.nr = i;
ioctl_guard!(ffi::pf_get_rule(self.fd(), &mut pfioc_rule))?;
if compare_cstr_safe(name, &pfioc_rule.anchor_call) {
return f(pfioc_rule);
}
}
Err(Error::from(ErrorInternal::AnchorDoesNotExist))
}
fn get_num_states(&self) -> Result<u32> {
let mut pfioc_states = unsafe { mem::zeroed::<ffi::pfvar::pfioc_states>() };
ioctl_guard!(ffi::pf_get_states(self.fd(), &mut pfioc_states))?;
let element_size = mem::size_of::<ffi::pfvar::pfsync_state>() as u32;
let buffer_size = pfioc_states.ps_len as u32;
Ok(buffer_size / element_size)
}
fn fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}
fn setup_pfioc_states(
num_states: u32,
) -> (ffi::pfvar::pfioc_states, Vec<ffi::pfvar::pfsync_state>) {
let mut pfioc_states = unsafe { mem::zeroed::<ffi::pfvar::pfioc_states>() };
let element_size = mem::size_of::<ffi::pfvar::pfsync_state>() as i32;
pfioc_states.ps_len = element_size * (num_states as i32);
let mut pfsync_states = (0..num_states)
.map(|_| unsafe { mem::zeroed::<ffi::pfvar::pfsync_state>() })
.collect::<Vec<_>>();
pfioc_states.ps_u.psu_states = pfsync_states.as_mut_ptr();
(pfioc_states, pfsync_states)
}
fn setup_pfioc_state_kill(
pfsync_state: &ffi::pfvar::pfsync_state,
pfioc_state_kill: &mut ffi::pfvar::pfioc_state_kill,
) {
pfioc_state_kill.psk_af = pfsync_state.af_lan;
pfioc_state_kill.psk_proto = pfsync_state.proto;
pfioc_state_kill.psk_proto_variant = pfsync_state.proto_variant;
pfioc_state_kill.psk_ifname = pfsync_state.ifname;
pfioc_state_kill.psk_src.addr.v.a.addr = pfsync_state.lan.addr;
pfioc_state_kill.psk_dst.addr.v.a.addr = pfsync_state.ext_lan.addr;
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
#[should_panic]
fn compare_cstr_without_nul() {
let cstr = CString::new("Hello").unwrap();
let cchars: &[i8] = unsafe { mem::transmute(cstr.as_bytes()) };
compare_cstr_safe("Hello", cchars);
}
#[test]
fn compare_same_strings() {
let cstr = CString::new("Hello").unwrap();
let cchars: &[i8] = unsafe { mem::transmute(cstr.as_bytes_with_nul()) };
assert!(compare_cstr_safe("Hello", cchars));
}
#[test]
fn compare_different_strings() {
let cstr = CString::new("Hello").unwrap();
let cchars: &[i8] = unsafe { mem::transmute(cstr.as_bytes_with_nul()) };
assert!(!compare_cstr_safe("olleH", cchars));
}
#[test]
fn compare_long_short_strings() {
let cstr = CString::new("veryverylong").unwrap();
let cchars: &[i8] = unsafe { mem::transmute(cstr.as_bytes_with_nul()) };
assert!(!compare_cstr_safe("short", cchars));
}
#[test]
fn compare_short_long_strings() {
let cstr = CString::new("short").unwrap();
let cchars: &[i8] = unsafe { mem::transmute(cstr.as_bytes_with_nul()) };
assert!(!compare_cstr_safe("veryverylong", cchars));
}
}