feat(gateway): open AF_PACKET sockets
The gateway now has a small Linux PacketSocket wrapper for raw Ethernet frame I/O. It resolves the configured interface with if_nametoindex, opens an AF_PACKET/SOCK_RAW socket for ETH_P_ALL, binds it to the interface, and exposes thin send_frame and recv_frame helpers around the owned file descriptor. The gateway binary opens this socket after completing the relay control handshake. The frame bridge loop is still intentionally left for a later slice, but the process now proves the two required resources are available: relay admission and raw L2 access on the LAN interface. Tests cover interface-name validation and missing-interface lookup without requiring root or CAP_NET_RAW. Test Plan: - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings Refs: PLAN.md Linux AF_PACKET gateway socket
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
use std::{
|
||||
ffi::CString,
|
||||
io,
|
||||
os::fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
};
|
||||
|
||||
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<Self> {
|
||||
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,
|
||||
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::<libc::sockaddr_ll>()
|
||||
};
|
||||
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::<libc::sockaddr>(),
|
||||
std::mem::size_of::<libc::sockaddr_ll>() 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<usize> {
|
||||
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::<libc::c_void>(),
|
||||
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<usize> {
|
||||
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::<libc::c_void>(),
|
||||
buffer.len(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if received < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(received as usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interface_index(interface: &str) -> io::Result<u32> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user