Files
softlan-vpn/crates/lanparty-gateway/src/packet.rs
T
ddidderr 63c829183f feat(gateway): bridge relay and LAN frames
The gateway now runs the actual frame bridge after relay admission. It registers
the AF_PACKET socket with Tokio using AsyncFd, reads valid LAN Ethernet frames
and forwards them as relay datagrams, and writes valid relay Ethernet datagrams
back to the LAN socket.

The packet socket is opened nonblocking so the bridge can shut down cleanly on
Ctrl-C without leaving a blocking recv thread behind. Existing send_ethernet and
recv_ethernet helpers now share the same validation and encoding helpers used by
the bridge.

This still needs a privileged LAN-host smoke test with a real wired interface,
but the compile-time and loopback coverage now include the gateway relay side of
the bridge and the non-root-safe packet-socket validation.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings

Refs: PLAN.md gateway AF_PACKET to relay bridge loop
2026-05-21 18:16:04 +02:00

168 lines
4.7 KiB
Rust

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<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 | 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::<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)
}
}
impl AsRawFd for PacketSocket {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
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);
}
}