use std::os::unix::io::AsRawFd;
use aya::{
Ebpf,
maps::{Array, MapData, RingBuf},
programs::SocketFilter,
};
use libc::htons;
use log::{debug, warn};
use socket2::{Domain, Protocol, Socket, Type};
use tokio::io::{Interest, unix::AsyncFd};
use crate::{
EBPF_BYTES, ETH_P_ALL, build_sll_header, detect_ethertype, device::Device, ns_to_ts,
packet::Packet, parse_ring_entry,
};
fn bind_to_interface(socket: &socket2::Socket, ifindex: u32) -> std::io::Result<()> {
let addr = libc::sockaddr_ll {
sll_family: libc::AF_PACKET as u16,
sll_protocol: htons(ETH_P_ALL),
sll_ifindex: ifindex as i32,
sll_hatype: 0,
sll_pkttype: 0,
sll_halen: 0,
sll_addr: [0; 8],
};
let ret = unsafe {
libc::bind(
socket.as_raw_fd(),
&addr as *const _ as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
)
};
if ret < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
pub struct Stats {
pub forwarded: u64,
pub dropped: u64,
}
#[allow(async_fn_in_trait)]
pub trait PacketSink {
async fn handle(&mut self, packet: Packet) -> anyhow::Result<()>;
}
pub struct Active {
ebpf: Ebpf,
stat_array: Array<MapData, u64>,
packet_buffer: AsyncFd<RingBuf<MapData>>,
_sockets: Vec<Socket>,
}
pub struct Inactive {
pub devices: Vec<Device>,
}
pub struct Capture<State> {
state: State,
}
impl Capture<Inactive> {
pub fn from_device(device: Device) -> Self {
Self {
state: Inactive {
devices: vec![device],
},
}
}
pub async fn start(self) -> anyhow::Result<Capture<Active>> {
let sockets: Vec<_> = self
.state
.devices
.iter()
.map(|device| {
let socket = socket2::Socket::new(
Domain::PACKET,
Type::DGRAM,
Some(Protocol::from(htons(ETH_P_ALL) as i32)),
)
.map_err(|e| anyhow::anyhow!("failed to create socket: {e}"))?;
if let Some(ifindex) = device.ifindex {
bind_to_interface(&socket, ifindex)
.map_err(|e| anyhow::anyhow!("failed to bind to {}: {e}", device.name))?;
}
Ok(socket)
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(Capture {
state: Active::initialize(sockets)?,
})
}
}
impl Active {
pub fn initialize(sockets: Vec<Socket>) -> anyhow::Result<Self> {
let rlim = libc::rlimit {
rlim_cur: libc::RLIM_INFINITY,
rlim_max: libc::RLIM_INFINITY,
};
let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
if ret != 0 {
debug!("remove limit on locked memory failed, ret is: {ret}");
}
let mut ebpf = aya::Ebpf::load(EBPF_BYTES).unwrap();
let program: &mut SocketFilter = ebpf.program_mut("rust_pcap").unwrap().try_into()?;
program.load()?;
for socket in &sockets {
program.attach(socket)?;
}
let ring_buf = RingBuf::try_from(ebpf.take_map("DATA").unwrap()).unwrap();
let packet_buffer = AsyncFd::with_interest(ring_buf, Interest::READABLE).unwrap();
let stat_array = Array::try_from(ebpf.take_map("COUNTERS").unwrap())?;
Ok(Active {
ebpf,
stat_array,
packet_buffer,
_sockets: sockets,
})
}
}
impl Capture<Active> {
pub async fn next_packet(&mut self) -> anyhow::Result<Packet> {
loop {
let mut guard = self.state.packet_buffer.readable_mut().await?;
match guard.try_io(|inner| {
let ringbuf_entry = inner
.get_mut()
.next()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::WouldBlock))?;
let (timestamp_ns, packet_data) = parse_ring_entry(&ringbuf_entry);
let protocol = detect_ethertype(packet_data);
let mut data = build_sll_header(protocol).to_vec();
data.extend_from_slice(packet_data);
Ok((timestamp_ns, data))
}) {
Ok(Ok((timestamp_ns, data))) => {
let (ts_sec, ts_usec) = ns_to_ts(timestamp_ns);
return Ok(Packet {
ts_sec,
ts_usec,
incl_len: data.len() as u32,
orig_len: data.len() as u32,
data,
});
}
Ok(Err(e)) => {
if e.kind() != std::io::ErrorKind::WouldBlock {
warn!("Unexpected try_io error: {e:?}");
return Err(anyhow::anyhow!(e));
}
}
Err(_e) => {
}
}
}
}
pub fn stop(mut self) -> anyhow::Result<()> {
let program: &mut SocketFilter = self
.state
.ebpf
.program_mut("rust_pcap")
.unwrap()
.try_into()?;
program.unload()?;
Ok(())
}
pub fn fetch_stats(&mut self) -> anyhow::Result<Stats> {
let stats = Stats {
forwarded: self.state.stat_array.get(&0, 0).unwrap_or(0),
dropped: self.state.stat_array.get(&1, 0).unwrap_or(0),
};
Ok(stats)
}
}