//! 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]) } } pub(crate) struct LinkFd(RawFd); 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, 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 { for ni in pnet_datalink::interfaces() { if ni.name == iface { if let Some(mac) = ni.mac { return Ok(MacAddr(mac.octets())); } } } Err(io::Error::new( ErrorKind::NotFound, format!("could not determine MAC for interface {iface}"), )) } impl Link { pub fn open(iface: &str) -> io::Result { unsafe { let fd = libc::socket( libc::AF_PACKET, libc::SOCK_RAW, htons(libc::ETH_P_ALL as u16) as libc::c_int, ); if fd < 0 { return Err(io::Error::last_os_error()); } // Non-blocking for AsyncFd. let flags = libc::fcntl(fd, libc::F_GETFL); if flags < 0 { let e = io::Error::last_os_error(); libc::close(fd); return Err(e); } if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 { let e = io::Error::last_os_error(); 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 { let e = io::Error::last_os_error(); 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::() as libc::socklen_t, ); if r < 0 { let e = io::Error::last_os_error(); 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::() as libc::socklen_t, ); if r < 0 { let e = io::Error::last_os_error(); 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> { self.fd.readable().await } pub async fn writable(&self) -> io::Result> { 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> { let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() }; let mut slen = std::mem::size_of::() 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 { return Err(io::Error::last_os_error()); } // 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::() as libc::socklen_t, ) }; if r < 0 { return Err(io::Error::last_os_error()); } Ok(()) } }