2026-08-12 18:20:51 +00:00
|
|
|
//! Raw Ethernet I/O over `AF_PACKET` with a classic BPF filter on ethertype
|
|
|
|
|
//! `0x6969`. No eBPF authoring: the filter is a hand-assembled cBPF program
|
|
|
|
|
//! installed via `SO_ATTACH_FILTER`. The kernel may translate it to eBPF
|
|
|
|
|
//! internally at attach time; that is transparent and not our concern.
|
|
|
|
|
|
|
|
|
|
use std::io::{self, ErrorKind};
|
|
|
|
|
use std::os::unix::io::{AsRawFd, RawFd};
|
|
|
|
|
use tokio::io::unix::{AsyncFd, AsyncFdReadyGuard};
|
|
|
|
|
|
|
|
|
|
use crate::frame::{ETHERTYPE, ETH_HEADER_LEN};
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
pub struct MacAddr(pub [u8; 6]);
|
|
|
|
|
|
|
|
|
|
impl MacAddr {
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
pub fn broadcast() -> Self {
|
|
|
|
|
MacAddr([0xff; 6])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:44:02 +00:00
|
|
|
pub(crate) struct LinkFd(RawFd);
|
2026-08-12 18:20:51 +00:00
|
|
|
impl AsRawFd for LinkFd {
|
|
|
|
|
fn as_raw_fd(&self) -> RawFd {
|
|
|
|
|
self.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl Drop for LinkFd {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
unsafe {
|
|
|
|
|
libc::close(self.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Link {
|
|
|
|
|
fd: AsyncFd<LinkFd>,
|
|
|
|
|
ifindex: libc::c_int,
|
|
|
|
|
pub our_mac: MacAddr,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cBPF program for `ether proto 0x6969`:
|
|
|
|
|
// 0: ldh [12] load ethertype (host-order u16)
|
|
|
|
|
// 1: jeq #host(ETHERTYPE), 1, 0 match -> skip to accept; else fall to drop
|
|
|
|
|
// 2: ret #0 drop
|
|
|
|
|
// 3: ret #0xFFFF accept (return whole packet)
|
|
|
|
|
//
|
|
|
|
|
// The ethertype is carried big-endian on the wire; `ldh` loads it in host
|
|
|
|
|
// byte order, so the comparison constant must be `u16::from_be(ETHERTYPE)`.
|
|
|
|
|
fn bpf_program() -> [libc::sock_filter; 4] {
|
|
|
|
|
const BPF_LD_H_ABS: u16 = 0x28;
|
|
|
|
|
const BPF_JMP_JEQ_K: u16 = 0x15;
|
|
|
|
|
const BPF_RET_K: u16 = 0x06;
|
|
|
|
|
let k = u16::from_be(ETHERTYPE) as u32;
|
|
|
|
|
[
|
|
|
|
|
libc::sock_filter { code: BPF_LD_H_ABS, jt: 0, jf: 0, k: 12 },
|
|
|
|
|
libc::sock_filter { code: BPF_JMP_JEQ_K, jt: 1, jf: 0, k },
|
|
|
|
|
libc::sock_filter { code: BPF_RET_K, jt: 0, jf: 0, k: 0 },
|
|
|
|
|
libc::sock_filter { code: BPF_RET_K, jt: 0, jf: 0, k: 0xFFFF },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn htons(v: u16) -> u16 {
|
|
|
|
|
v.to_be()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn lookup_mac(iface: &str) -> io::Result<MacAddr> {
|
|
|
|
|
for ni in pnet_datalink::interfaces() {
|
|
|
|
|
if ni.name == iface {
|
|
|
|
|
if let Some(mac) = ni.mac {
|
2026-08-13 07:41:49 +00:00
|
|
|
return Ok(MacAddr(mac.octets()));
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(io::Error::new(
|
|
|
|
|
ErrorKind::NotFound,
|
|
|
|
|
format!("could not determine MAC for interface {iface}"),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Link {
|
|
|
|
|
pub fn open(iface: &str) -> io::Result<Link> {
|
|
|
|
|
unsafe {
|
2026-08-13 07:41:49 +00:00
|
|
|
let fd = libc::socket(
|
|
|
|
|
libc::AF_PACKET,
|
|
|
|
|
libc::SOCK_RAW,
|
|
|
|
|
htons(libc::ETH_P_ALL as u16) as libc::c_int,
|
|
|
|
|
);
|
2026-08-12 18:20:51 +00:00
|
|
|
if fd < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
return Err(io::Error::last_os_error());
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Non-blocking for AsyncFd.
|
|
|
|
|
let flags = libc::fcntl(fd, libc::F_GETFL);
|
|
|
|
|
if flags < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
let e = io::Error::last_os_error();
|
2026-08-12 18:20:51 +00:00
|
|
|
libc::close(fd);
|
|
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
let e = io::Error::last_os_error();
|
2026-08-12 18:20:51 +00:00
|
|
|
libc::close(fd);
|
|
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ciface = match std::ffi::CString::new(iface) {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(_) => {
|
|
|
|
|
libc::close(fd);
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
ErrorKind::InvalidInput,
|
|
|
|
|
"interface name contains an interior NUL",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let ifindex = libc::if_nametoindex(ciface.as_ptr());
|
|
|
|
|
if ifindex == 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
let e = io::Error::last_os_error();
|
2026-08-12 18:20:51 +00:00
|
|
|
libc::close(fd);
|
|
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bind to the interface so we only receive frames on it.
|
|
|
|
|
let mut sll: libc::sockaddr_ll = std::mem::zeroed();
|
|
|
|
|
sll.sll_family = libc::AF_PACKET as u16;
|
|
|
|
|
sll.sll_protocol = htons(libc::ETH_P_ALL as u16);
|
|
|
|
|
sll.sll_ifindex = ifindex as libc::c_int;
|
|
|
|
|
let r = libc::bind(
|
|
|
|
|
fd,
|
|
|
|
|
&sll as *const _ as *const libc::sockaddr,
|
|
|
|
|
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
|
|
|
|
);
|
|
|
|
|
if r < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
let e = io::Error::last_os_error();
|
2026-08-12 18:20:51 +00:00
|
|
|
libc::close(fd);
|
|
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Classic BPF: only our ethertype reaches userspace.
|
|
|
|
|
let filt = bpf_program();
|
|
|
|
|
let prog = libc::sock_fprog {
|
|
|
|
|
len: filt.len() as u16,
|
|
|
|
|
filter: filt.as_ptr() as *mut libc::sock_filter,
|
|
|
|
|
};
|
|
|
|
|
let r = libc::setsockopt(
|
|
|
|
|
fd,
|
|
|
|
|
libc::SOL_SOCKET,
|
|
|
|
|
libc::SO_ATTACH_FILTER,
|
|
|
|
|
&prog as *const _ as *const libc::c_void,
|
|
|
|
|
std::mem::size_of::<libc::sock_fprog>() as libc::socklen_t,
|
|
|
|
|
);
|
|
|
|
|
if r < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
let e = io::Error::last_os_error();
|
2026-08-12 18:20:51 +00:00
|
|
|
libc::close(fd);
|
|
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let our_mac = lookup_mac(iface)?;
|
|
|
|
|
|
|
|
|
|
let async_fd = AsyncFd::new(LinkFd(fd))?;
|
|
|
|
|
Ok(Link {
|
|
|
|
|
fd: async_fd,
|
|
|
|
|
ifindex: ifindex as libc::c_int,
|
|
|
|
|
our_mac,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn readable(&self) -> io::Result<AsyncFdReadyGuard<'_, LinkFd>> {
|
|
|
|
|
self.fd.readable().await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn writable(&self) -> io::Result<AsyncFdReadyGuard<'_, LinkFd>> {
|
|
|
|
|
self.fd.writable().await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Receive one frame. Returns:
|
|
|
|
|
/// - `Ok(Some((src, payload)))` for a frame we should process.
|
|
|
|
|
/// - `Ok(None)` for an ignorable frame (our own outgoing frame, short
|
|
|
|
|
/// frame, ethertype mismatch) — the caller should keep draining without
|
|
|
|
|
/// clearing readiness.
|
|
|
|
|
/// - `Err(WouldBlock)` when no more frames are available — the caller
|
|
|
|
|
/// should clear readiness and await again.
|
|
|
|
|
pub fn recv<'a>(&self, buf: &'a mut [u8]) -> io::Result<Option<(MacAddr, &'a [u8])>> {
|
|
|
|
|
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
|
|
|
|
let mut slen = std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
|
|
|
|
|
let n = unsafe {
|
|
|
|
|
libc::recvfrom(
|
|
|
|
|
self.fd.as_raw_fd(),
|
|
|
|
|
buf.as_mut_ptr() as *mut libc::c_void,
|
|
|
|
|
buf.len(),
|
|
|
|
|
0,
|
|
|
|
|
&mut sll as *mut _ as *mut libc::sockaddr,
|
|
|
|
|
&mut slen,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
if n < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
return Err(io::Error::last_os_error());
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
// Skip frames we transmitted (AF_PACKET with ETH_P_ALL loops them back).
|
|
|
|
|
if sll.sll_pkttype == libc::PACKET_OUTGOING {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
let pkt = &buf[..n as usize];
|
|
|
|
|
if pkt.len() < ETH_HEADER_LEN {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
let src = MacAddr([pkt[6], pkt[7], pkt[8], pkt[9], pkt[10], pkt[11]]);
|
|
|
|
|
// BPF already filtered; double-check for safety against the ethertype.
|
|
|
|
|
let et = u16::from_be_bytes([pkt[12], pkt[13]]);
|
|
|
|
|
if et != ETHERTYPE {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
Ok(Some((src, &pkt[ETH_HEADER_LEN..])))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Send `payload` (our protocol frame) wrapped in an Ethernet header to
|
|
|
|
|
/// `dst`. The ethertype is `0x6969`. Returns `WouldBlock` if the kernel
|
|
|
|
|
/// buffer is full; the caller is expected to await writability and retry.
|
|
|
|
|
pub fn send(&self, dst: MacAddr, payload: &[u8]) -> io::Result<()> {
|
|
|
|
|
let mut frame = Vec::with_capacity(ETH_HEADER_LEN + payload.len());
|
|
|
|
|
frame.extend_from_slice(&dst.0);
|
|
|
|
|
frame.extend_from_slice(&self.our_mac.0);
|
|
|
|
|
frame.extend_from_slice(ÐERTYPE.to_be_bytes());
|
|
|
|
|
frame.extend_from_slice(payload);
|
|
|
|
|
|
|
|
|
|
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
|
|
|
|
sll.sll_family = libc::AF_PACKET as u16;
|
|
|
|
|
sll.sll_protocol = htons(libc::ETH_P_ALL as u16);
|
|
|
|
|
sll.sll_ifindex = self.ifindex;
|
|
|
|
|
sll.sll_hatype = 1; // ARPHRD_ETHER
|
|
|
|
|
sll.sll_halen = 6;
|
|
|
|
|
sll.sll_addr[..6].copy_from_slice(&dst.0);
|
|
|
|
|
|
|
|
|
|
let r = unsafe {
|
|
|
|
|
libc::sendto(
|
|
|
|
|
self.fd.as_raw_fd(),
|
|
|
|
|
frame.as_ptr() as *const libc::c_void,
|
|
|
|
|
frame.len(),
|
|
|
|
|
0,
|
|
|
|
|
&sll as *const _ as *const libc::sockaddr,
|
|
|
|
|
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
if r < 0 {
|
2026-08-13 07:41:49 +00:00
|
|
|
return Err(io::Error::last_os_error());
|
2026-08-12 18:20:51 +00:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|