use std::ffi::CString;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::{io, mem, ptr};
use anyhow::{Context, Result, bail};
use log::info;
use super::packet::{Sampler, parse_ethernet, parse_ip};
use super::{Capture, PacketInfo};
const BIOCSRTIMEOUT: libc::c_ulong = 0x8010_426d;
const DLT_NULL: u32 = 0;
const DLT_EN10MB: u32 = 1;
const DLT_RAW: u32 = 12;
const DLT_LOOP: u32 = 108;
const ALIGNMENT: usize = 4;
const CAPLEN_OFFSET: usize = 8;
const HDRLEN_OFFSET: usize = 16;
const MIN_HEADER_LEN: usize = 18;
const BUFFER_SIZE: libc::c_uint = 512 * 1024;
const MAX_DEVICES: u32 = 256;
enum LinkLayer {
Ethernet,
Loopback,
RawIp,
}
pub struct Bpf {
device: File,
link_layer: LinkLayer,
buf: Vec<u8>,
len: usize,
pos: usize,
sampler: Sampler,
}
impl Bpf {
pub fn new(interface: &str, promiscuous: bool, sampling_interval: u32) -> Result<Self> {
info!("Opening BPF capture on interface: {}", interface);
let device = open_device()?;
let fd = device.as_raw_fd();
let mut ifr: libc::ifreq = unsafe { mem::zeroed() };
let name = CString::new(interface)?;
let name = name.as_bytes_with_nul();
if name.len() > libc::IFNAMSIZ {
bail!("Interface name '{interface}' is too long");
}
unsafe {
ptr::copy_nonoverlapping(
name.as_ptr(),
ifr.ifr_name.as_mut_ptr() as *mut u8,
name.len(),
);
}
let mut size = BUFFER_SIZE;
loop {
unsafe { libc::ioctl(fd, libc::BIOCSBLEN, &mut size) };
if unsafe { libc::ioctl(fd, libc::BIOCSETIF, &ifr) } == 0 {
break;
}
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOBUFS) && size > 4096 {
size /= 2;
continue;
}
return Err(err)
.with_context(|| format!("Failed to attach to interface '{interface}'"));
}
let mut size: libc::c_uint = 0;
ioctl(fd, libc::BIOCGBLEN, &mut size, "read the buffer size")?;
let mut immediate: libc::c_uint = 1;
ioctl(
fd,
libc::BIOCIMMEDIATE,
&mut immediate,
"enable immediate mode",
)?;
let mut timeout = libc::timeval {
tv_sec: 1,
tv_usec: 0,
};
ioctl(fd, BIOCSRTIMEOUT, &mut timeout, "set the read timeout")?;
if promiscuous {
if unsafe { libc::ioctl(fd, libc::BIOCPROMISC as libc::c_ulong) } < 0 {
return Err(io::Error::last_os_error()).with_context(|| {
format!("Failed to enable promiscuous mode on '{interface}'")
});
}
info!("Promiscuous mode enabled on {}", interface);
}
let mut dlt: u32 = 0;
ioctl(fd, libc::BIOCGDLT, &mut dlt, "read the link type")?;
let link_layer = match dlt {
DLT_EN10MB => LinkLayer::Ethernet,
DLT_NULL | DLT_LOOP => LinkLayer::Loopback,
DLT_RAW => LinkLayer::RawIp,
other => bail!(
"Unsupported link type {other} on '{interface}'; only Ethernet, loopback and raw IP are supported"
),
};
info!("BPF buffer ready: {} bytes", size);
Ok(Self {
device,
link_layer,
buf: vec![0; size as usize],
len: 0,
pos: 0,
sampler: Sampler::new(sampling_interval),
})
}
fn fill(&mut self) -> bool {
self.len = 0;
self.pos = 0;
let n = unsafe {
libc::read(
self.device.as_raw_fd(),
self.buf.as_mut_ptr() as *mut libc::c_void,
self.buf.len(),
)
};
if n <= 0 {
return false;
}
self.len = n as usize;
true
}
}
impl Capture for Bpf {
fn next_packet(&mut self) -> Option<PacketInfo> {
loop {
if self.pos >= self.len && !self.fill() {
return None;
}
let Some((data, next)) = record(&self.buf[..self.len], self.pos) else {
self.len = 0;
return None;
};
self.pos = next;
if !self.sampler.select() {
continue;
}
return match self.link_layer {
LinkLayer::Ethernet => parse_ethernet(data),
LinkLayer::Loopback => parse_ip(data.get(4..)?),
LinkLayer::RawIp => parse_ip(data),
};
}
}
}
fn record(batch: &[u8], pos: usize) -> Option<(&[u8], usize)> {
let rec = batch.get(pos..)?;
if rec.len() < MIN_HEADER_LEN {
return None;
}
let field = |at: usize| u32::from_ne_bytes(rec[at..at + 4].try_into().unwrap()) as usize;
let caplen = field(CAPLEN_OFFSET);
let hdrlen =
u16::from_ne_bytes(rec[HDRLEN_OFFSET..HDRLEN_OFFSET + 2].try_into().unwrap()) as usize;
let data = rec.get(hdrlen..hdrlen + caplen)?;
let next = pos + (hdrlen + caplen).next_multiple_of(ALIGNMENT);
Some((data, next))
}
fn open_device() -> Result<File> {
for n in 0..MAX_DEVICES {
let path = format!("/dev/bpf{n}");
match File::open(&path) {
Ok(file) => return Ok(file),
Err(e) if e.raw_os_error() == Some(libc::EBUSY) => continue,
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
bail!(
"Cannot open {path}: {e}; run as root, or grant your user access to the BPF devices (Wireshark's ChmodBPF does this)"
)
}
Err(e) => return Err(e).with_context(|| format!("Cannot open {path}")),
}
}
bail!("All {MAX_DEVICES} BPF devices are busy")
}
fn ioctl<T>(fd: libc::c_int, request: libc::c_ulong, arg: *mut T, what: &str) -> Result<()> {
if unsafe { libc::ioctl(fd, request, arg) } < 0 {
return Err(io::Error::last_os_error()).with_context(|| format!("Failed to {what}"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn bpf_record(hdrlen: usize, data: &[u8]) -> Vec<u8> {
let mut rec = vec![0u8; hdrlen];
rec[CAPLEN_OFFSET..CAPLEN_OFFSET + 4].copy_from_slice(&(data.len() as u32).to_ne_bytes());
rec[12..16].copy_from_slice(&(data.len() as u32 + 100).to_ne_bytes());
rec[HDRLEN_OFFSET..HDRLEN_OFFSET + 2].copy_from_slice(&(hdrlen as u16).to_ne_bytes());
rec.extend_from_slice(data);
rec.resize(rec.len().next_multiple_of(ALIGNMENT), 0xee);
rec
}
#[test]
fn walks_records_with_their_own_header_length_and_padding() {
let mut batch = bpf_record(18, b"hello");
batch.extend(bpf_record(20, b"loop"));
batch.extend(bpf_record(18, &[7; 40]));
let (data, next) = record(&batch, 0).unwrap();
assert_eq!(data, b"hello");
assert_eq!(next, 24);
let (data, next) = record(&batch, next).unwrap();
assert_eq!(data, b"loop");
assert_eq!(next, 48);
let (data, next) = record(&batch, next).unwrap();
assert_eq!(data, &[7; 40]);
assert_eq!(next, batch.len());
assert!(record(&batch, next).is_none());
}
#[test]
fn truncated_tail_is_rejected() {
let rec = bpf_record(18, b"hello");
assert!(record(&rec[..10], 0).is_none());
assert!(record(&rec[..20], 0).is_none());
assert!(record(&rec[..23], 0).is_some());
}
}