use crate::transport::TransportError;
use std::os::unix::io::{AsRawFd, RawFd};
use std::sync::Mutex;
const ETH_HDRLEN: usize = 14;
const SIOCGIFMTU: libc::c_ulong = 0xC0206933;
struct BpfReadState {
buf: Vec<u8>,
offset: usize,
len: usize,
}
struct FdGuard(RawFd);
impl FdGuard {
fn into_raw(self) -> RawFd {
let fd = self.0;
std::mem::forget(self);
fd
}
}
impl Drop for FdGuard {
fn drop(&mut self) {
unsafe {
libc::close(self.0);
}
}
}
pub struct PacketSocket {
fd: RawFd,
if_index: i32,
ethertype: u16,
local_mac: [u8; 6],
bpf_buflen: usize,
read_state: Mutex<BpfReadState>,
shutdown_write_fd: RawFd,
shutdown_read_fd: RawFd,
}
impl PacketSocket {
pub fn open(interface: &str, ethertype: u16) -> Result<Self, TransportError> {
let guard = FdGuard(open_bpf_device()?);
let fd = guard.0;
let if_index = get_if_index(interface)?;
bind_to_interface(fd, interface)?;
set_bpf_immediate(fd)?;
set_bpf_hdrcmplt(fd)?;
install_ethertype_filter(fd, ethertype)?;
let bpf_buflen = get_bpf_buflen(fd)?;
let local_mac = get_mac_addr(interface)?;
let mut pipe_fds = [0i32; 2];
if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } < 0 {
unsafe { libc::close(guard.0) };
std::mem::forget(guard); return Err(TransportError::StartFailed(format!(
"pipe() failed: {}",
std::io::Error::last_os_error()
)));
}
guard.into_raw();
Ok(Self {
fd,
if_index,
ethertype,
local_mac,
bpf_buflen,
read_state: Mutex::new(BpfReadState {
buf: vec![0u8; bpf_buflen],
offset: 0,
len: 0,
}),
shutdown_read_fd: pipe_fds[0],
shutdown_write_fd: pipe_fds[1],
})
}
pub fn if_index(&self) -> i32 {
self.if_index
}
pub fn local_mac(&self) -> Result<[u8; 6], TransportError> {
Ok(self.local_mac)
}
pub fn interface_mtu(&self) -> Result<u16, TransportError> {
get_if_mtu(self.if_index)
}
pub fn set_recv_buffer_size(&self, _size: usize) -> Result<(), TransportError> {
Ok(())
}
pub fn bpf_buflen(&self) -> usize {
self.bpf_buflen
}
pub fn shutdown_read_fd(&self) -> RawFd {
self.shutdown_read_fd
}
pub fn request_shutdown(&self) {
unsafe {
libc::write(
self.shutdown_write_fd,
b"x".as_ptr() as *const libc::c_void,
1,
);
}
}
pub fn set_send_buffer_size(&self, _size: usize) -> Result<(), TransportError> {
Ok(())
}
pub fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> std::io::Result<usize> {
let mut hdr = [0u8; ETH_HDRLEN];
hdr[..6].copy_from_slice(dest_mac);
hdr[6..12].copy_from_slice(&self.local_mac);
hdr[12..14].copy_from_slice(&self.ethertype.to_be_bytes());
let iov = [
libc::iovec {
iov_base: hdr.as_ptr() as *mut libc::c_void,
iov_len: ETH_HDRLEN,
},
libc::iovec {
iov_base: data.as_ptr() as *mut libc::c_void,
iov_len: data.len(),
},
];
let ret = unsafe { libc::writev(self.fd, iov.as_ptr(), 2) };
if ret < 0 {
Err(std::io::Error::last_os_error())
} else {
let sent = (ret as usize).saturating_sub(ETH_HDRLEN);
Ok(sent)
}
}
pub fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, [u8; 6])> {
let mut state = self.read_state.lock().unwrap();
let state = &mut *state;
loop {
if let Some(result) = parse_next_frame(&state.buf, &mut state.offset, state.len, buf) {
return result;
}
let ret = unsafe {
libc::read(
self.fd,
state.buf.as_mut_ptr() as *mut libc::c_void,
self.bpf_buflen,
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
state.len = ret as usize;
state.offset = 0;
}
}
}
pub fn parse_next_frame(
read_buf: &[u8],
read_offset: &mut usize,
read_len: usize,
out_buf: &mut [u8],
) -> Option<std::io::Result<(usize, [u8; 6])>> {
const BPF_HDR_SIZE: usize = std::mem::size_of::<BpfHeader>();
if *read_offset >= read_len {
return None;
}
let remaining = read_len - *read_offset;
if remaining < BPF_HDR_SIZE {
*read_offset = read_len;
return None;
}
let hdr_ptr = read_buf[*read_offset..].as_ptr() as *const BpfHeader;
let hdr = unsafe { std::ptr::read_unaligned(hdr_ptr) };
let cap_len = hdr.bh_caplen as usize;
let data_offset = hdr.bh_hdrlen as usize;
let total_len = bpf_wordalign(data_offset + cap_len);
let frame_start = *read_offset + data_offset;
*read_offset += total_len;
if cap_len < ETH_HDRLEN {
return None; }
if frame_start + cap_len > read_len {
return None; }
let frame = &read_buf[frame_start..frame_start + cap_len];
let mut src_mac = [0u8; 6];
src_mac.copy_from_slice(&frame[6..12]);
let payload = &frame[ETH_HDRLEN..];
let copy_len = payload.len().min(out_buf.len());
out_buf[..copy_len].copy_from_slice(&payload[..copy_len]);
Some(Ok((copy_len, src_mac)))
}
impl AsRawFd for PacketSocket {
fn as_raw_fd(&self) -> RawFd {
self.fd
}
}
impl Drop for PacketSocket {
fn drop(&mut self) {
unsafe {
libc::close(self.fd);
libc::close(self.shutdown_read_fd);
libc::close(self.shutdown_write_fd);
}
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct BpfHeader {
bh_tstamp_sec: u32,
bh_tstamp_usec: u32,
bh_caplen: u32,
bh_datalen: u32,
bh_hdrlen: u16,
_pad: u16,
}
const _: () = assert!(std::mem::size_of::<BpfHeader>() == 20);
fn bpf_wordalign(x: usize) -> usize {
(x + 3) & !3
}
const BIOCSETIF: libc::c_ulong = 0x8020426C;
const BIOCIMMEDIATE: libc::c_ulong = 0x80044270;
const BIOCSHDRCMPLT: libc::c_ulong = 0x80044275;
const BIOCSETF: libc::c_ulong = 0x80104267;
const BIOCGBLEN: libc::c_ulong = 0x40044266;
#[repr(C)]
#[derive(Clone, Copy)]
struct BpfInsn {
code: u16,
jt: u8,
jf: u8,
k: u32,
}
#[repr(C)]
struct BpfProgram {
bf_len: u32,
bf_insns: *const BpfInsn,
}
fn open_bpf_device() -> Result<RawFd, TransportError> {
for i in 0..256 {
let path = format!("/dev/bpf{}", i);
let c_path = std::ffi::CString::new(path.as_str()).unwrap();
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR) };
if fd >= 0 {
return Ok(fd);
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EACCES) {
return Err(TransportError::StartFailed(
"BPF requires root (run with sudo)".into(),
));
}
}
Err(TransportError::StartFailed(
"no available /dev/bpf* device".into(),
))
}
fn bind_to_interface(fd: RawFd, interface: &str) -> Result<(), TransportError> {
let mut ifreq: [u8; 32] = [0; 32]; let name_bytes = interface.as_bytes();
let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1);
ifreq[..copy_len].copy_from_slice(&name_bytes[..copy_len]);
let ret = unsafe { libc::ioctl(fd, BIOCSETIF, ifreq.as_ptr()) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCSETIF({}) failed: {}",
interface,
std::io::Error::last_os_error()
)));
}
Ok(())
}
fn set_bpf_immediate(fd: RawFd) -> Result<(), TransportError> {
let enable: libc::c_uint = 1;
let ret = unsafe { libc::ioctl(fd, BIOCIMMEDIATE, &enable) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCIMMEDIATE failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
fn set_bpf_hdrcmplt(fd: RawFd) -> Result<(), TransportError> {
let enable: libc::c_uint = 1;
let ret = unsafe { libc::ioctl(fd, BIOCSHDRCMPLT, &enable) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCSHDRCMPLT failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
fn install_ethertype_filter(fd: RawFd, ethertype: u16) -> Result<(), TransportError> {
let filter = [
BpfInsn {
code: 0x28,
jt: 0,
jf: 0,
k: 12,
}, BpfInsn {
code: 0x15,
jt: 0,
jf: 1,
k: ethertype as u32,
}, BpfInsn {
code: 0x06,
jt: 0,
jf: 0,
k: 0xFFFF,
}, BpfInsn {
code: 0x06,
jt: 0,
jf: 0,
k: 0,
}, ];
let prog = BpfProgram {
bf_len: filter.len() as u32,
bf_insns: filter.as_ptr(),
};
let ret = unsafe { libc::ioctl(fd, BIOCSETF, &prog) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCSETF failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
fn get_bpf_buflen(fd: RawFd) -> Result<usize, TransportError> {
let mut buflen: libc::c_uint = 0;
let ret = unsafe { libc::ioctl(fd, BIOCGBLEN, &mut buflen) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"BIOCGBLEN failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(buflen as usize)
}
fn get_if_index(interface: &str) -> Result<i32, TransportError> {
let c_name = std::ffi::CString::new(interface).map_err(|_| {
TransportError::StartFailed(format!("invalid interface name: {}", interface))
})?;
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
if idx == 0 {
return Err(TransportError::StartFailed(format!(
"interface not found: {} ({})",
interface,
std::io::Error::last_os_error()
)));
}
Ok(idx as i32)
}
fn get_mac_addr(interface: &str) -> Result<[u8; 6], TransportError> {
let mut addrs: *mut libc::ifaddrs = std::ptr::null_mut();
let ret = unsafe { libc::getifaddrs(&mut addrs) };
if ret != 0 {
return Err(TransportError::StartFailed(format!(
"getifaddrs() failed: {}",
std::io::Error::last_os_error()
)));
}
let result = (|| {
let mut cur = addrs;
while !cur.is_null() {
let ifa = unsafe { &*cur };
let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }
.to_str()
.unwrap_or("");
if name == interface && !ifa.ifa_addr.is_null() {
let sa = unsafe { &*ifa.ifa_addr };
if sa.sa_family as i32 == libc::AF_LINK {
let sdl = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_dl) };
let nlen = sdl.sdl_nlen as usize;
let data_ptr = sdl.sdl_data.as_ptr();
let mut mac = [0u8; 6];
for (i, byte) in mac.iter_mut().enumerate() {
*byte = unsafe { *data_ptr.add(nlen + i) } as u8;
}
return Ok(mac);
}
}
cur = unsafe { (*cur).ifa_next };
}
Err(TransportError::StartFailed(format!(
"MAC address not found for interface: {}",
interface
)))
})();
unsafe { libc::freeifaddrs(addrs) };
result
}
fn get_if_mtu(if_index: i32) -> Result<u16, TransportError> {
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if sock < 0 {
return Err(TransportError::StartFailed(format!(
"socket(AF_INET) for MTU query failed: {}",
std::io::Error::last_os_error()
)));
}
let mut name_buf = [0i8; libc::IFNAMSIZ];
let ret = unsafe { libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr()) };
if ret.is_null() {
unsafe { libc::close(sock) };
return Err(TransportError::StartFailed(format!(
"if_indextoname({}) failed: {}",
if_index,
std::io::Error::last_os_error()
)));
}
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
unsafe {
std::ptr::copy_nonoverlapping(name_buf.as_ptr(), ifr.ifr_name.as_mut_ptr(), libc::IFNAMSIZ);
}
let ret = unsafe { libc::ioctl(sock, SIOCGIFMTU, &mut ifr) };
unsafe { libc::close(sock) };
if ret < 0 {
return Err(TransportError::StartFailed(format!(
"ioctl(SIOCGIFMTU) failed: {}",
std::io::Error::last_os_error()
)));
}
let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u16;
Ok(mtu)
}
#[cfg(test)]
#[cfg(target_os = "macos")]
mod tests {
use super::*;
#[test]
fn test_bpf_wordalign_already_aligned() {
assert_eq!(bpf_wordalign(0), 0);
assert_eq!(bpf_wordalign(4), 4);
assert_eq!(bpf_wordalign(8), 8);
assert_eq!(bpf_wordalign(20), 20);
}
#[test]
fn test_bpf_wordalign_rounds_up() {
assert_eq!(bpf_wordalign(1), 4);
assert_eq!(bpf_wordalign(2), 4);
assert_eq!(bpf_wordalign(3), 4);
assert_eq!(bpf_wordalign(5), 8);
assert_eq!(bpf_wordalign(21), 24);
}
fn make_bpf_packet(src_mac: [u8; 6], payload: &[u8]) -> Vec<u8> {
let cap_len = ETH_HDRLEN + payload.len();
let hdr = BpfHeader {
bh_tstamp_sec: 0,
bh_tstamp_usec: 0,
bh_caplen: cap_len as u32,
bh_datalen: cap_len as u32,
bh_hdrlen: std::mem::size_of::<BpfHeader>() as u16,
_pad: 0,
};
let hdr_size = std::mem::size_of::<BpfHeader>();
let total = bpf_wordalign(hdr_size + cap_len);
let mut buf = vec![0u8; total];
unsafe {
std::ptr::copy_nonoverlapping(
&hdr as *const BpfHeader as *const u8,
buf.as_mut_ptr(),
hdr_size,
);
}
let frame_start = hdr_size;
buf[frame_start..frame_start + 6].copy_from_slice(&[0xff; 6]); buf[frame_start + 6..frame_start + 12].copy_from_slice(&src_mac);
buf[frame_start + 12] = 0x08;
buf[frame_start + 13] = 0x00;
buf[frame_start + ETH_HDRLEN..frame_start + ETH_HDRLEN + payload.len()]
.copy_from_slice(payload);
buf
}
#[test]
fn test_parse_next_frame_single() {
let src_mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
let payload = b"hello world";
let buf = make_bpf_packet(src_mac, payload);
let mut out_buf = vec![0u8; 1500];
let mut offset = 0usize;
let len = buf.len();
let result = parse_next_frame(&buf, &mut offset, len, &mut out_buf)
.expect("should return Some")
.expect("should be Ok");
assert_eq!(result.0, payload.len());
assert_eq!(result.1, src_mac);
assert_eq!(&out_buf[..result.0], payload);
assert!(offset > 0);
}
#[test]
fn test_parse_next_frame_two_frames() {
let src1 = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
let src2 = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
let payload1 = b"first";
let payload2 = b"second";
let mut buf = make_bpf_packet(src1, payload1);
buf.extend_from_slice(&make_bpf_packet(src2, payload2));
let len = buf.len();
let mut out_buf = vec![0u8; 1500];
let mut offset = 0usize;
let (n1, mac1) = parse_next_frame(&buf, &mut offset, len, &mut out_buf)
.expect("Some")
.expect("Ok");
assert_eq!(mac1, src1);
assert_eq!(&out_buf[..n1], payload1);
let (n2, mac2) = parse_next_frame(&buf, &mut offset, len, &mut out_buf)
.expect("Some")
.expect("Ok");
assert_eq!(mac2, src2);
assert_eq!(&out_buf[..n2], payload2);
assert!(parse_next_frame(&buf, &mut offset, len, &mut out_buf).is_none());
}
#[test]
fn test_parse_next_frame_empty_buffer() {
let buf = vec![0u8; 0];
let mut out_buf = vec![0u8; 1500];
let mut offset = 0usize;
assert!(parse_next_frame(&buf, &mut offset, 0, &mut out_buf).is_none());
}
#[test]
fn test_parse_next_frame_offset_at_end() {
let buf = vec![0u8; 64];
let mut out_buf = vec![0u8; 1500];
let mut offset = 64usize;
assert!(parse_next_frame(&buf, &mut offset, 64, &mut out_buf).is_none());
}
#[test]
fn test_parse_next_frame_runt_skipped() {
let hdr_size = std::mem::size_of::<BpfHeader>();
let cap_len: usize = 13; let hdr = BpfHeader {
bh_tstamp_sec: 0,
bh_tstamp_usec: 0,
bh_caplen: cap_len as u32,
bh_datalen: cap_len as u32,
bh_hdrlen: hdr_size as u16,
_pad: 0,
};
let total = bpf_wordalign(hdr_size + cap_len);
let mut buf = vec![0u8; total];
unsafe {
std::ptr::copy_nonoverlapping(
&hdr as *const BpfHeader as *const u8,
buf.as_mut_ptr(),
hdr_size,
);
}
let mut out_buf = vec![0u8; 1500];
let mut offset = 0usize;
assert!(parse_next_frame(&buf, &mut offset, buf.len(), &mut out_buf).is_none());
}
#[test]
fn test_parse_next_frame_output_buf_truncation() {
let src_mac = [0xde, 0xad, 0xbe, 0xef, 0x00, 0x01];
let payload = b"this payload is longer than the output buffer";
let bpf_buf = make_bpf_packet(src_mac, payload);
let len = bpf_buf.len();
let mut out_buf = vec![0u8; 10]; let mut offset = 0usize;
let (n, mac) = parse_next_frame(&bpf_buf, &mut offset, len, &mut out_buf)
.expect("Some")
.expect("Ok");
assert_eq!(n, 10);
assert_eq!(mac, src_mac);
assert_eq!(&out_buf[..n], &payload[..10]);
}
fn fd_is_readable(fd: RawFd, timeout_ms: i64) -> bool {
unsafe {
let mut tv = libc::timeval {
tv_sec: timeout_ms / 1000,
tv_usec: ((timeout_ms % 1000) * 1000) as i32,
};
let mut read_fds: libc::fd_set = std::mem::zeroed();
libc::FD_ZERO(&mut read_fds);
libc::FD_SET(fd, &mut read_fds);
let ret = libc::select(
fd + 1,
&mut read_fds,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut tv,
);
ret > 0 && libc::FD_ISSET(fd, &read_fds)
}
}
#[test]
fn test_shutdown_pipe_initially_not_readable() {
let mut fds = [0i32; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let (read_fd, write_fd) = (fds[0], fds[1]);
let readable = fd_is_readable(read_fd, 0);
unsafe {
libc::close(read_fd);
libc::close(write_fd);
}
assert!(
!readable,
"pipe read end should not be readable before write"
);
}
#[test]
fn test_shutdown_pipe_readable_after_write() {
let mut fds = [0i32; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let (read_fd, write_fd) = (fds[0], fds[1]);
unsafe {
libc::write(write_fd, b"x".as_ptr() as *const libc::c_void, 1);
}
let readable = fd_is_readable(read_fd, 0);
unsafe {
libc::close(read_fd);
libc::close(write_fd);
}
assert!(readable, "pipe read end should be readable after write");
}
#[test]
fn test_bpf_header_layout_matches_kernel() {
assert_eq!(std::mem::size_of::<BpfHeader>(), 20);
let hdr = BpfHeader {
bh_tstamp_sec: 0,
bh_tstamp_usec: 0,
bh_caplen: 0,
bh_datalen: 0,
bh_hdrlen: 0xABCD,
_pad: 0,
};
let bytes: &[u8] =
unsafe { std::slice::from_raw_parts(&hdr as *const BpfHeader as *const u8, 20) };
assert_eq!(&bytes[16..18], &0xABCDu16.to_ne_bytes());
}
#[test]
fn test_parse_next_frame_caplen_exceeds_remaining_buffer() {
let hdr_size = std::mem::size_of::<BpfHeader>();
let claimed_cap_len: usize = 200; let hdr = BpfHeader {
bh_tstamp_sec: 0,
bh_tstamp_usec: 0,
bh_caplen: claimed_cap_len as u32,
bh_datalen: claimed_cap_len as u32,
bh_hdrlen: hdr_size as u16,
_pad: 0,
};
let mut buf = vec![0u8; hdr_size + 32];
unsafe {
std::ptr::copy_nonoverlapping(
&hdr as *const BpfHeader as *const u8,
buf.as_mut_ptr(),
hdr_size,
);
}
let mut out_buf = vec![0u8; 1500];
let mut offset = 0usize;
assert!(parse_next_frame(&buf, &mut offset, buf.len(), &mut out_buf).is_none());
}
#[test]
fn test_ethernet_header_round_trip_via_parse() {
let dst_mac: [u8; 6] = [0xff; 6];
let src_mac: [u8; 6] = [0x02, 0x00, 0x00, 0x12, 0x34, 0x56];
let ethertype: u16 = 0x88B5; let payload: &[u8] = b"FIPS-frame-payload";
let cap_len = ETH_HDRLEN + payload.len();
let hdr = BpfHeader {
bh_tstamp_sec: 0,
bh_tstamp_usec: 0,
bh_caplen: cap_len as u32,
bh_datalen: cap_len as u32,
bh_hdrlen: std::mem::size_of::<BpfHeader>() as u16,
_pad: 0,
};
let hdr_size = std::mem::size_of::<BpfHeader>();
let total = bpf_wordalign(hdr_size + cap_len);
let mut buf = vec![0u8; total];
unsafe {
std::ptr::copy_nonoverlapping(
&hdr as *const BpfHeader as *const u8,
buf.as_mut_ptr(),
hdr_size,
);
}
let frame_start = hdr_size;
buf[frame_start..frame_start + 6].copy_from_slice(&dst_mac);
buf[frame_start + 6..frame_start + 12].copy_from_slice(&src_mac);
buf[frame_start + 12..frame_start + 14].copy_from_slice(ðertype.to_be_bytes());
buf[frame_start + ETH_HDRLEN..frame_start + ETH_HDRLEN + payload.len()]
.copy_from_slice(payload);
let mut out_buf = vec![0u8; 1500];
let mut offset = 0usize;
let (n, parsed_src) = parse_next_frame(&buf, &mut offset, buf.len(), &mut out_buf)
.expect("Some")
.expect("Ok");
assert_eq!(n, payload.len());
assert_eq!(&out_buf[..n], payload);
assert_eq!(parsed_src, src_mac);
}
}