Skip to content

Commit 62f1376

Browse files
committed
Add function for getting interface flags
Closes #128
1 parent 273e96b commit 62f1376

File tree

9 files changed

+149
-13
lines changed

9 files changed

+149
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
1313
* **Security**: in case of vulnerabilities.
1414

1515
## [unreleased]
16+
### Added
17+
- Add function for getting interface flags.
1618

1719

1820
## [0.7.0] - 2025-09-12
@@ -126,4 +128,3 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
126128
## [0.1.0] - 2017-12-20
127129
### Added
128130
- Initial functionality able to control most parts of the PF firewall on macOS
129-

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ travis-ci = { repository = "mullvad/pfctl-rs" }
1616

1717

1818
[dependencies]
19+
bitflags = "2.9.4"
1920
ioctl-sys = "0.8.0"
2021
libc = "0.2.29"
2122
derive_builder = "0.20"

generate_bindings.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ bindgen \
2020
--allowlist-type pfioc_states \
2121
--allowlist-type pfioc_state_kill \
2222
--allowlist-type pfioc_iface \
23+
--allowlist-type pfi_kif \
2324
--allowlist-var PF_.* \
2425
--allowlist-var PFRULE_.* \
2526
--default-enum-style rust \

src/ffi/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ ioctl!(readwrite pf_add_addr with b'D', 52; pfvar::pfioc_pooladdr);
6363
ioctl!(readwrite pf_begin_trans with b'D', 81; pfvar::pfioc_trans);
6464
// DIOCXCOMMIT
6565
ioctl!(readwrite pf_commit_trans with b'D', 82; pfvar::pfioc_trans);
66+
// DIOCIGETIFACES
67+
ioctl!(readwrite pf_get_ifaces with b'D', 87; pfvar::pfioc_iface);
6668
// DIOCSETIFFLAG
6769
ioctl!(readwrite pf_set_iface_flag with b'D', 89; pfvar::pfioc_iface);
6870
// DIOCCLRIFFLAG

src/ffi/pfvar.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* automatically generated by rust-bindgen 0.70.1 */
1+
/* automatically generated by rust-bindgen 0.72.1 */
22

33
pub const PF_UNSPEC: u32 = 0;
44
pub const PF_LOCAL: u32 = 1;
@@ -1266,6 +1266,34 @@ const _: () = {
12661266
};
12671267
#[repr(C)]
12681268
#[derive(Debug, Copy, Clone)]
1269+
pub struct pfi_kif {
1270+
pub pfik_name: [::std::os::raw::c_char; 16usize],
1271+
pub pfik_packets: [[[u_int64_t; 2usize]; 2usize]; 2usize],
1272+
pub pfik_bytes: [[[u_int64_t; 2usize]; 2usize]; 2usize],
1273+
pub pfik_tzero: u_int64_t,
1274+
pub pfik_flags: ::std::os::raw::c_int,
1275+
pub pfik_states: ::std::os::raw::c_int,
1276+
pub pfik_rules: ::std::os::raw::c_int,
1277+
}
1278+
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
1279+
const _: () = {
1280+
["Size of pfi_kif"][::std::mem::size_of::<pfi_kif>() - 168usize];
1281+
["Alignment of pfi_kif"][::std::mem::align_of::<pfi_kif>() - 8usize];
1282+
["Offset of field: pfi_kif::pfik_name"][::std::mem::offset_of!(pfi_kif, pfik_name) - 0usize];
1283+
["Offset of field: pfi_kif::pfik_packets"]
1284+
[::std::mem::offset_of!(pfi_kif, pfik_packets) - 16usize];
1285+
["Offset of field: pfi_kif::pfik_bytes"][::std::mem::offset_of!(pfi_kif, pfik_bytes) - 80usize];
1286+
["Offset of field: pfi_kif::pfik_tzero"]
1287+
[::std::mem::offset_of!(pfi_kif, pfik_tzero) - 144usize];
1288+
["Offset of field: pfi_kif::pfik_flags"]
1289+
[::std::mem::offset_of!(pfi_kif, pfik_flags) - 152usize];
1290+
["Offset of field: pfi_kif::pfik_states"]
1291+
[::std::mem::offset_of!(pfi_kif, pfik_states) - 156usize];
1292+
["Offset of field: pfi_kif::pfik_rules"]
1293+
[::std::mem::offset_of!(pfi_kif, pfik_rules) - 160usize];
1294+
};
1295+
#[repr(C)]
1296+
#[derive(Debug, Copy, Clone)]
12691297
pub struct pf_status {
12701298
pub counters: [u_int64_t; 17usize],
12711299
pub lcounters: [u_int64_t; 7usize],

src/lib.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ impl PfCtl {
508508
) -> Result<()> {
509509
let mut iface = unsafe { mem::zeroed::<ffi::pfvar::pfioc_iface>() };
510510
interface.try_copy_to(&mut iface.pfiio_name)?;
511-
iface.pfiio_flags = flags as i32;
511+
iface.pfiio_flags = flags.bits();
512512
ioctl_guard!(ffi::pf_set_iface_flag(self.fd(), &mut iface))?;
513513
Ok(())
514514
}
@@ -523,11 +523,51 @@ impl PfCtl {
523523
) -> Result<()> {
524524
let mut iface = unsafe { mem::zeroed::<ffi::pfvar::pfioc_iface>() };
525525
interface.try_copy_to(&mut iface.pfiio_name)?;
526-
iface.pfiio_flags = flags as i32;
526+
iface.pfiio_flags = flags.bits();
527527
ioctl_guard!(ffi::pf_clear_iface_flag(self.fd(), &mut iface))?;
528528
Ok(())
529529
}
530530

531+
/// Get interfaces with corresponding flags.
532+
///
533+
/// https://man.freebsd.org/cgi/man.cgi?pf(4)
534+
pub fn get_interface_flags(
535+
&mut self,
536+
interface: Interface,
537+
) -> Result<Vec<InterfaceDescription>> {
538+
// This should be sufficient capacity on average machine to avoid reallocation
539+
const INITIAL_CAPACITY: usize = 30;
540+
541+
let mut buf: Vec<ffi::pfvar::pfi_kif> = Vec::with_capacity(INITIAL_CAPACITY);
542+
let mut iface = unsafe { mem::zeroed::<ffi::pfvar::pfioc_iface>() };
543+
interface.try_copy_to(&mut iface.pfiio_name)?;
544+
iface.pfiio_esize = mem::size_of::<ffi::pfvar::pfi_kif>() as i32;
545+
546+
let mut num_system_interfaces = 0;
547+
for _ in 0..2 {
548+
iface.pfiio_buffer = buf.as_mut_ptr() as _;
549+
iface.pfiio_size = buf.capacity() as _;
550+
551+
ioctl_guard!(ffi::pf_get_ifaces(self.fd(), &mut iface))?;
552+
num_system_interfaces = usize::try_from(iface.pfiio_size).unwrap_or_default();
553+
554+
// Reserve additional space and retry if number of system interfaces exceeds capacity
555+
if num_system_interfaces > buf.capacity() {
556+
buf.reserve(num_system_interfaces);
557+
} else {
558+
break;
559+
}
560+
}
561+
562+
let new_len = std::cmp::min(num_system_interfaces, buf.capacity());
563+
// SAFETY: safe since new_len is capped at capacity
564+
unsafe { buf.set_len(new_len) };
565+
566+
buf.into_iter()
567+
.map(InterfaceDescription::try_from)
568+
.collect()
569+
}
570+
531571
/// Get all states created by stateful rules
532572
fn get_states_inner(&mut self) -> Result<Vec<ffi::pfvar::pfsync_state>> {
533573
let num_states = self.get_num_states()?;

src/rule/interface.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
// option. This file may not be copied, modified, or distributed
77
// except according to those terms.
88

9-
use crate::conversion::TryCopyTo;
10-
use crate::{Error, ErrorInternal};
9+
use crate::{Error, ErrorInternal, conversion::TryCopyTo};
1110

1211
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1312
pub struct InterfaceName(String);
@@ -44,10 +43,38 @@ impl TryCopyTo<[i8]> for Interface {
4443
}
4544
}
4645

46+
bitflags::bitflags! {
47+
#[repr(transparent)]
48+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49+
pub struct InterfaceFlags: i32 {
50+
/// Set or clear the skip flag on an interface.
51+
/// This is equivalent to PFI_IFLAG_SKIP.
52+
const SKIP = 0x0100;
53+
}
54+
}
55+
4756
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48-
#[repr(i32)]
49-
pub enum InterfaceFlags {
50-
/// Set or clear the skip flag on an interface.
51-
/// This is equivalent to PFI_IFLAG_SKIP.
52-
Skip = 0x0100,
57+
pub struct InterfaceDescription {
58+
pub name: String,
59+
pub flags: InterfaceFlags,
60+
}
61+
62+
impl TryFrom<crate::ffi::pfvar::pfi_kif> for InterfaceDescription {
63+
type Error = crate::Error;
64+
65+
fn try_from(kif: crate::ffi::pfvar::pfi_kif) -> Result<Self, Self::Error> {
66+
let c_chars_ptr = kif.pfik_name.as_ptr() as *const u8;
67+
// SAFETY: valid as long as `kif` is within the scope
68+
let c_chars_u8 = unsafe { std::slice::from_raw_parts(c_chars_ptr, kif.pfik_name.len()) };
69+
70+
let name = std::ffi::CStr::from_bytes_until_nul(c_chars_u8)
71+
.map_err(|_| Error::from(ErrorInternal::InvalidInterfaceName("missing null byte")))?
72+
.to_str()
73+
.map_err(|_| Error::from(ErrorInternal::InvalidInterfaceName("invalid utf8 encoding")))?
74+
.to_owned();
75+
76+
let flags = InterfaceFlags::from_bits_retain(kif.pfik_flags);
77+
78+
Ok(InterfaceDescription { name, flags })
79+
}
5380
}

tests/interface.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,54 @@ test!(set_and_reset_interface_flag {
2323
slice::from_ref(&temp_tun_name),
2424
);
2525

26-
pf.set_interface_flag(interface.clone(), InterfaceFlags::Skip).unwrap();
26+
pf.set_interface_flag(interface.clone(), InterfaceFlags::SKIP).unwrap();
2727

2828
assert_eq!(
2929
get_interface_flags(&temp_tun_name),
3030
&[format!("{temp_tun_name} (skip)")],
3131
"expected skip flag to be set",
3232
);
3333

34-
pf.clear_interface_flag(interface, InterfaceFlags::Skip).unwrap();
34+
pf.clear_interface_flag(interface.clone(), InterfaceFlags::SKIP).unwrap();
3535

3636
assert_eq!(
3737
get_interface_flags(&temp_tun_name),
3838
&[temp_tun_name],
3939
"expected skip flag to be cleared",
4040
);
4141
});
42+
43+
test!(get_all_interfaces {
44+
let temp_tun = tun::Device::new(&Configuration::default()).unwrap();
45+
let temp_tun_name = temp_tun.tun_name().unwrap();
46+
47+
let mut pf = pfctl::PfCtl::new().unwrap();
48+
pf.set_interface_flag(pfctl::Interface::from(&temp_tun_name), InterfaceFlags::SKIP).unwrap();
49+
50+
let iface = pf.get_interface_flags(pfctl::Interface::Any)
51+
.unwrap()
52+
.into_iter()
53+
.find(|iface| iface.name == temp_tun_name)
54+
.unwrap();
55+
assert!(iface.flags.contains(InterfaceFlags::SKIP), "expected skip flag to be set");
56+
});
57+
58+
test!(get_single_interface {
59+
let temp_tun = tun::Device::new(&Configuration::default()).unwrap();
60+
let temp_tun_name = temp_tun.tun_name().unwrap();
61+
62+
let mut pf = pfctl::PfCtl::new().unwrap();
63+
pf.set_interface_flag(pfctl::Interface::from(&temp_tun_name), InterfaceFlags::SKIP).unwrap();
64+
65+
let ifaces = pf.get_interface_flags(pfctl::Interface::from(&temp_tun_name))
66+
.unwrap();
67+
68+
assert_eq!(ifaces.len(), 1);
69+
70+
let iface = ifaces
71+
.into_iter()
72+
.next()
73+
.unwrap();
74+
assert!(iface.name == temp_tun_name, "expected tun interface to be returned");
75+
assert!(iface.flags.contains(InterfaceFlags::SKIP), "expected skip flag to be set");
76+
});

0 commit comments

Comments
 (0)