use crate::{ByteThreshold, Error, Result};
#[cfg(all(feature = "socket-bpf", target_os = "linux"))]
use std::os::fd::AsRawFd;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SkipDecision {
pub inode: u64,
pub file_offset: u64,
pub skip_length: u64,
}
pub struct KernelFilter {
thresholds: Vec<ByteThreshold>,
active: bool,
pending_skips: Vec<SkipDecision>,
}
impl KernelFilter {
pub fn try_attach(thresholds: &[ByteThreshold]) -> Result<Option<Self>> {
if thresholds.is_empty() {
return Err(Error::InvalidConfiguration {
reason: "kernel filter requires at least one byte threshold".to_string(),
fix: "provide one or more ByteThreshold values",
});
}
if !Self::prerequisites_met() {
return Ok(None);
}
#[cfg(all(target_os = "linux", feature = "kernel-bpf"))]
{
return Self::load_and_attach_bpf(thresholds);
}
#[allow(unreachable_code)]
Ok(None)
}
fn prerequisites_met() -> bool {
#[cfg(target_os = "linux")]
{
#[allow(unsafe_code)]
let euid = unsafe { libc::geteuid() };
if euid != 0 {
return false;
}
if !std::path::Path::new("/sys/kernel/btf/vmlinux").exists() {
return false;
}
if let Ok(release) = std::fs::read_to_string("/proc/sys/kernel/osrelease") {
if let Some(version) = parse_kernel_version(&release) {
return version >= (5, 8, 0);
}
}
false
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
pub fn poll_skips(&mut self) -> &[SkipDecision] {
if !self.active {
return &[];
}
self.pending_skips.clear();
#[cfg(all(target_os = "linux", feature = "kernel-bpf"))]
{
self.drain_ring_buffer();
}
&self.pending_skips
}
#[must_use]
pub fn is_active(&self) -> bool {
self.active
}
#[must_use]
pub fn thresholds(&self) -> &[ByteThreshold] {
&self.thresholds
}
pub fn detach(&mut self) {
self.active = false;
self.pending_skips.clear();
}
}
impl Drop for KernelFilter {
fn drop(&mut self) {
self.detach();
}
}
#[cfg(feature = "socket-bpf")]
use crate::ByteFrequencyFilter;
#[cfg(feature = "socket-bpf")]
use ebpfkit::assembler::BpfInsn;
#[cfg(feature = "socket-bpf")]
pub fn byte_frequency_filter_to_literal_pattern(filter: &ByteFrequencyFilter) -> Result<Vec<u8>> {
let mut merged = [0u16; 256];
for t in filter.thresholds() {
let i = t.byte as usize;
merged[i] = merged[i].max(t.min_count);
}
let mut pattern = Vec::new();
for byte in 0_u16..256 {
let c = merged[byte as usize];
if c == 0 {
continue;
}
let n = usize::from(c);
let next_len = pattern.len().saturating_add(n);
if next_len > ebpfkit::compiler::MAX_BPF_PATTERN_LEN {
return Err(Error::InvalidConfiguration {
reason: format!(
"encoded BPF literal would be {next_len} bytes, max is {}",
ebpfkit::compiler::MAX_BPF_PATTERN_LEN
),
fix: "lower min_count values or use userspace-only filtering for this filter",
});
}
if let Ok(byte_u8) = u8::try_from(byte) {
pattern.extend(std::iter::repeat_n(byte_u8, n));
}
}
if pattern.is_empty() {
return Err(Error::InvalidConfiguration {
reason: "no literal bytes derived from filter thresholds".to_string(),
fix: "ensure the filter has at least one ByteThreshold",
});
}
Ok(pattern)
}
#[cfg(feature = "socket-bpf")]
pub fn compile_socket_filter_program(filter: &ByteFrequencyFilter) -> Result<Vec<BpfInsn>> {
let pattern = byte_frequency_filter_to_literal_pattern(filter)?;
let insns = ebpfkit::compiler::compile_literal_search(&pattern)?;
Ok(insns)
}
#[cfg(all(feature = "socket-bpf", target_os = "linux"))]
pub struct SocketFilterProgram {
prog_fd: std::os::fd::OwnedFd,
}
#[cfg(all(feature = "socket-bpf", target_os = "linux"))]
impl SocketFilterProgram {
#[allow(unsafe_code)]
pub fn try_load(filter: &ByteFrequencyFilter) -> Result<Option<Self>> {
let euid = unsafe { libc::geteuid() };
if euid != 0 {
return Ok(None);
}
let insns = compile_socket_filter_program(filter)?;
let raw_fd =
ebpfkit::loader::load_filter(&insns).map_err(|source| Error::EbpfKernel { source })?;
let prog_fd = unsafe { std::os::fd::FromRawFd::from_raw_fd(raw_fd) };
Ok(Some(Self { prog_fd }))
}
pub fn attach_to_fd(&self, fd: std::os::unix::io::RawFd) -> Result<()> {
ebpfkit::loader::attach_to_socket(self.prog_fd.as_raw_fd(), fd)
.map_err(|source| Error::EbpfKernel { source })
}
#[must_use]
pub fn program_fd(&self) -> std::os::unix::io::RawFd {
self.prog_fd.as_raw_fd()
}
}
#[cfg(target_os = "linux")]
fn parse_kernel_version(release: &str) -> Option<(u32, u32, u32)> {
let trimmed = release.trim();
let mut parts = trimmed.split(|c: char| !c.is_ascii_digit());
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
Some((major, minor, patch))
}
#[cfg(all(target_os = "linux", feature = "kernel-bpf"))]
impl KernelFilter {
fn load_and_attach_bpf(thresholds: &[ByteThreshold]) -> Result<Option<Self>> {
use aya::maps::HashMap as BpfHashMap;
use aya::{programs::FEntry, Ebpf};
let mut bpf = Ebpf::load(include_bytes!(concat!(env!("OUT_DIR"), "/sieve.bpf.o")))
.map_err(|e| Error::InvalidConfiguration {
reason: format!("failed to load BPF program: {e}"),
fix: "ensure the BPF program was compiled correctly",
})?;
let mut threshold_map: BpfHashMap<_, u8, u16> =
BpfHashMap::try_from(bpf.map_mut("thresholds").ok_or_else(|| {
Error::InvalidConfiguration {
reason: "BPF program missing 'thresholds' map".to_string(),
fix: "rebuild the BPF program with the threshold map",
}
})?)
.map_err(|e| Error::InvalidConfiguration {
reason: format!("failed to open threshold map: {e}"),
fix: "check BPF map type compatibility",
})?;
for threshold in thresholds {
threshold_map
.insert(threshold.byte, threshold.min_count, 0)
.map_err(|e| Error::InvalidConfiguration {
reason: format!("failed to insert threshold: {e}"),
fix: "check threshold map capacity",
})?;
}
let program: &mut FEntry = bpf
.program_mut("sieve_vfs_read")
.ok_or_else(|| Error::InvalidConfiguration {
reason: "BPF program missing 'sieve_vfs_read' function".to_string(),
fix: "rebuild the BPF program with the correct entry point",
})?
.try_into()
.map_err(|e| Error::InvalidConfiguration {
reason: format!("program type mismatch: {e}"),
fix: "ensure the BPF program uses fentry section",
})?;
program
.load(
"vfs_read",
&aya::Btf::from_sys_fs().map_err(|e| Error::InvalidConfiguration {
reason: format!("BTF not available: {e}"),
fix: "ensure kernel has BTF support enabled",
})?,
)
.map_err(|e| Error::InvalidConfiguration {
reason: format!("failed to load fentry program: {e}"),
fix: "check kernel version supports fentry",
})?;
program.attach().map_err(|e| Error::InvalidConfiguration {
reason: format!("failed to attach to vfs_read: {e}"),
fix: "check CAP_BPF capability and kernel BTF",
})?;
Ok(Some(Self {
thresholds: thresholds.to_vec(),
active: true,
pending_skips: Vec::with_capacity(256),
}))
}
fn drain_ring_buffer(&mut self) {
if self.active {
self.active = false;
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::pedantic
)]
mod tests {
use super::*;
#[test]
fn prerequisites_checks_do_not_panic() {
let _met = KernelFilter::prerequisites_met();
}
#[test]
fn try_attach_returns_none_when_not_root() {
let result = KernelFilter::try_attach(&[ByteThreshold::new(b'a', 2)]);
match result {
Ok(None) => {} Ok(Some(mut filter)) => {
assert!(filter.is_active());
filter.detach();
}
Err(_) => {} }
}
#[test]
fn try_attach_rejects_empty_thresholds() {
let result = KernelFilter::try_attach(&[]);
assert!(result.is_err());
}
#[test]
fn poll_skips_returns_empty_when_inactive() {
let met = KernelFilter::prerequisites_met();
if !met {
assert!(!met);
}
}
#[cfg(feature = "socket-bpf")]
#[test]
fn literal_pattern_merges_duplicate_byte_thresholds() {
let filter = crate::ByteFrequencyFilter::new([
ByteThreshold::new(b'z', 1),
ByteThreshold::new(b'z', 3),
ByteThreshold::new(b'm', 2),
])
.unwrap();
let pat = byte_frequency_filter_to_literal_pattern(&filter).unwrap();
assert_eq!(pat, b"mmzzz");
}
#[cfg(feature = "socket-bpf")]
#[test]
fn compile_socket_filter_produces_instructions() {
let filter = crate::ByteFrequencyFilter::new([ByteThreshold::new(b'x', 2)]).unwrap();
let insns = compile_socket_filter_program(&filter).unwrap();
assert!(!insns.is_empty());
}
#[cfg(target_os = "linux")]
#[test]
fn parse_kernel_version_works() {
assert_eq!(
parse_kernel_version("5.15.0-91-generic\n"),
Some((5, 15, 0))
);
assert_eq!(parse_kernel_version("6.8.12"), Some((6, 8, 12)));
assert_eq!(parse_kernel_version("4.19.0"), Some((4, 19, 0)));
}
}