use std::{ ffi::CString, io, os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, }; const ETH_P_ALL: u16 = libc::ETH_P_ALL as u16; #[derive(Debug)] pub struct PacketSocket { fd: OwnedFd, interface: String, interface_index: u32, } impl PacketSocket { pub fn open(interface: &str) -> io::Result { let interface_index = interface_index(interface)?; let protocol = i32::from(ETH_P_ALL.to_be()); let raw_fd = unsafe { // SAFETY: socket is called with constant domain/type/protocol values and returns // a new file descriptor or -1 without aliasing Rust-owned memory. libc::socket( libc::AF_PACKET, libc::SOCK_RAW | libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK, protocol, ) }; if raw_fd < 0 { return Err(io::Error::last_os_error()); } let fd = unsafe { // SAFETY: raw_fd was just returned by socket and is owned by this function. OwnedFd::from_raw_fd(raw_fd) }; let mut address = unsafe { // SAFETY: sockaddr_ll is a plain old data kernel ABI struct; zero is a valid base // before filling the fields required by bind. std::mem::zeroed::() }; address.sll_family = libc::AF_PACKET as u16; address.sll_protocol = ETH_P_ALL.to_be(); address.sll_ifindex = interface_index as i32; let result = unsafe { // SAFETY: address points to a properly initialized sockaddr_ll and the length // matches that struct. fd remains owned by this function across the call. libc::bind( fd.as_raw_fd(), (&address as *const libc::sockaddr_ll).cast::(), std::mem::size_of::() as libc::socklen_t, ) }; if result < 0 { return Err(io::Error::last_os_error()); } Ok(Self { fd, interface: interface.to_owned(), interface_index, }) } #[must_use] pub fn interface(&self) -> &str { &self.interface } #[must_use] pub const fn interface_index(&self) -> u32 { self.interface_index } pub fn send_frame(&self, frame: &[u8]) -> io::Result { let sent = unsafe { // SAFETY: frame.as_ptr() is valid for frame.len() bytes for the duration of send, // and send does not retain the pointer after returning. libc::send( self.fd.as_raw_fd(), frame.as_ptr().cast::(), frame.len(), 0, ) }; if sent < 0 { return Err(io::Error::last_os_error()); } Ok(sent as usize) } pub fn recv_frame(&self, buffer: &mut [u8]) -> io::Result { let received = unsafe { // SAFETY: buffer.as_mut_ptr() is valid for buffer.len() bytes for the duration of // recv, and recv initializes at most that many bytes. libc::recv( self.fd.as_raw_fd(), buffer.as_mut_ptr().cast::(), buffer.len(), 0, ) }; if received < 0 { return Err(io::Error::last_os_error()); } Ok(received as usize) } } impl AsRawFd for PacketSocket { fn as_raw_fd(&self) -> RawFd { self.fd.as_raw_fd() } } pub fn interface_index(interface: &str) -> io::Result { if interface.trim().is_empty() { return Err(io::Error::new( io::ErrorKind::InvalidInput, "interface name cannot be empty", )); } let name = CString::new(interface).map_err(|_| { io::Error::new( io::ErrorKind::InvalidInput, "interface name cannot contain NUL bytes", ) })?; let index = unsafe { // SAFETY: name is a valid NUL-terminated C string and if_nametoindex does not retain it. libc::if_nametoindex(name.as_ptr()) }; if index == 0 { return Err(io::Error::last_os_error()); } Ok(index) } #[cfg(test)] mod tests { use super::*; #[test] fn rejects_invalid_interface_names() { assert_eq!( interface_index("").unwrap_err().kind(), io::ErrorKind::InvalidInput ); assert_eq!( interface_index("eth0\0bad").unwrap_err().kind(), io::ErrorKind::InvalidInput ); } #[test] fn reports_missing_interface() { let error = interface_index("lanparty-definitely-missing0").unwrap_err(); assert_ne!(error.kind(), io::ErrorKind::InvalidInput); } }