mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 09:33:23 +00:00
Add macOS support, fix bloom filter routing and MMP intervals
macOS platform: - Platform-native TUN interface management with shutdown pipe - Raw Ethernet transport with macOS socket backend (socket_macos.rs) - EthernetTransport and TransportHandle::Ethernet ungated from Linux-only - macOS .pkg packaging (build-pkg.sh, launchd plist, uninstall script) - CI: macOS build and unit test jobs; x86_64 cross-compiled from macos-latest via rustup target add x86_64-apple-darwin Gateway feature flag: - New opt-in `gateway` Cargo feature activates optional `rustables` dep - `pub mod gateway` and `Config.gateway` gated behind the feature so macOS builds never pull in Linux-only nftables bindings - `fips-gateway` bin has `required-features = ["gateway"]` - All Linux/OpenWrt/AUR packaging passes `--features gateway` CI / packaging: - package-linux, package-macos, package-openwrt now trigger on push to master/maint/next and on pull requests; release uploads remain tag-gated - Bloom filter routing fix: fall through to tree routing when no candidate is strictly closer - MMP intervals: raise MIN to 1s / MAX to 5s with 5-sample cold-start phase
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
//! DNS-allocated virtual IPs and kernel nftables NAT.
|
||||
|
||||
use clap::Parser;
|
||||
#[cfg(target_os = "linux")]
|
||||
use fips::gateway::{control, dns, nat, net, pool};
|
||||
use fips::version;
|
||||
use fips::Config;
|
||||
|
||||
+14
-2
@@ -112,8 +112,20 @@ async fn main() {
|
||||
|
||||
info!("FIPS running, press Ctrl+C to exit");
|
||||
|
||||
// Run the RX event loop until shutdown signal.
|
||||
// Run the RX event loop until shutdown signal (SIGINT or SIGTERM).
|
||||
// stop() drops the packet channel, causing run_rx_loop to exit.
|
||||
#[cfg(unix)]
|
||||
let shutdown = async {
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler");
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {},
|
||||
_ = sigterm.recv() => {},
|
||||
}
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let shutdown = tokio::signal::ctrl_c();
|
||||
|
||||
tokio::select! {
|
||||
result = node.run_rx_loop() => {
|
||||
match result {
|
||||
@@ -121,7 +133,7 @@ async fn main() {
|
||||
Err(e) => error!("RX loop error: {}", e),
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
_ = shutdown => {
|
||||
info!("Shutdown signal received");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//! nsec: "nsec1..."
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "gateway")]
|
||||
mod gateway;
|
||||
mod node;
|
||||
mod peer;
|
||||
@@ -34,6 +35,7 @@ pub use node::{
|
||||
NodeConfig, RateLimitConfig, RekeyConfig, RetryConfig, SessionConfig, SessionMmpConfig,
|
||||
TreeConfig,
|
||||
};
|
||||
#[cfg(feature = "gateway")]
|
||||
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig};
|
||||
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||
pub use transport::{BleConfig, DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances, TransportsConfig, UdpConfig};
|
||||
@@ -320,6 +322,7 @@ pub struct Config {
|
||||
pub peers: Vec<PeerConfig>,
|
||||
|
||||
/// Gateway configuration (`gateway`).
|
||||
#[cfg(feature = "gateway")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gateway: Option<GatewayConfig>,
|
||||
}
|
||||
@@ -441,6 +444,7 @@ impl Config {
|
||||
self.peers = other.peers;
|
||||
}
|
||||
// Merge gateway section — higher-priority config replaces entirely
|
||||
#[cfg(feature = "gateway")]
|
||||
if other.gateway.is_some() {
|
||||
self.gateway = other.gateway;
|
||||
}
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ pub mod bloom;
|
||||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod control;
|
||||
#[cfg(feature = "gateway")]
|
||||
pub mod gateway;
|
||||
pub mod identity;
|
||||
pub mod mmp;
|
||||
@@ -27,7 +28,7 @@ pub use identity::{
|
||||
};
|
||||
|
||||
// Re-export config types
|
||||
pub use config::{Config, ConfigError, GatewayConfig, IdentityConfig, TorConfig, UdpConfig};
|
||||
pub use config::{Config, ConfigError, IdentityConfig, TorConfig, UdpConfig};
|
||||
pub use upper::config::{DnsConfig, TunConfig};
|
||||
|
||||
// Re-export tree types
|
||||
|
||||
+34
-1
@@ -587,6 +587,22 @@ impl Node {
|
||||
info!("effective MTU: {} bytes", effective_mtu);
|
||||
debug!(" max TCP MSS: {} bytes", max_mss);
|
||||
|
||||
// On macOS, create a shutdown pipe. Writing to it unblocks the
|
||||
// reader thread's select() loop without closing the TUN fd
|
||||
// (which would cause a double-close when TunDevice drops).
|
||||
#[cfg(target_os = "macos")]
|
||||
let (shutdown_read_fd, shutdown_write_fd) = {
|
||||
let mut fds = [0i32; 2];
|
||||
if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
|
||||
return Err(NodeError::Tun(
|
||||
crate::upper::tun::TunError::Configure(
|
||||
"failed to create shutdown pipe".into(),
|
||||
),
|
||||
));
|
||||
}
|
||||
(fds[0], fds[1])
|
||||
};
|
||||
|
||||
// Create writer (dups the fd for independent write access)
|
||||
let (writer, tun_tx) = device.create_writer(max_mss)?;
|
||||
|
||||
@@ -604,6 +620,11 @@ impl Node {
|
||||
|
||||
// Spawn reader thread
|
||||
let transport_mtu = self.transport_mtu();
|
||||
#[cfg(target_os = "macos")]
|
||||
let reader_handle = thread::spawn(move || {
|
||||
run_tun_reader(device, mtu, our_addr, reader_tun_tx, outbound_tx, transport_mtu, shutdown_read_fd);
|
||||
});
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let reader_handle = thread::spawn(move || {
|
||||
run_tun_reader(device, mtu, our_addr, reader_tun_tx, outbound_tx, transport_mtu);
|
||||
});
|
||||
@@ -614,6 +635,8 @@ impl Node {
|
||||
self.tun_outbound_rx = Some(outbound_rx);
|
||||
self.tun_reader_handle = Some(reader_handle);
|
||||
self.tun_writer_handle = Some(writer_handle);
|
||||
#[cfg(target_os = "macos")]
|
||||
{ self.tun_shutdown_fd = Some(shutdown_write_fd); }
|
||||
}
|
||||
Err(e) => {
|
||||
self.tun_state = TunState::Failed;
|
||||
@@ -704,11 +727,21 @@ impl Node {
|
||||
// Drop the tun_tx to signal the writer to stop
|
||||
self.tun_tx.take();
|
||||
|
||||
// Delete the interface (causes reader to get EFAULT)
|
||||
// Delete the interface (on Linux, causes reader to get EFAULT)
|
||||
if let Err(e) = shutdown_tun_interface(&name).await {
|
||||
warn!(name = %name, error = %e, "Failed to shutdown TUN interface");
|
||||
}
|
||||
|
||||
// On macOS, signal the reader thread to exit by writing to the
|
||||
// shutdown pipe. The reader's select() will wake up and break.
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(fd) = self.tun_shutdown_fd.take() {
|
||||
unsafe {
|
||||
libc::write(fd, b"x".as_ptr() as *const libc::c_void, 1);
|
||||
libc::close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for threads to finish
|
||||
if let Some(handle) = self.tun_reader_handle.take() {
|
||||
let _ = handle.join();
|
||||
|
||||
+21
-27
@@ -33,7 +33,6 @@ use crate::transport::{
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::transport::tcp::TcpTransport;
|
||||
use crate::transport::tor::TorTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::transport::ethernet::EthernetTransport;
|
||||
use crate::tree::TreeState;
|
||||
use crate::upper::hosts::HostMap;
|
||||
@@ -364,6 +363,10 @@ pub struct Node {
|
||||
tun_reader_handle: Option<JoinHandle<()>>,
|
||||
/// TUN writer thread handle.
|
||||
tun_writer_handle: Option<JoinHandle<()>>,
|
||||
/// Shutdown pipe: writing to this fd unblocks the TUN reader thread on macOS.
|
||||
/// On Linux, deleting the interface via netlink serves the same purpose.
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: Option<std::os::unix::io::RawFd>,
|
||||
|
||||
// === DNS Responder ===
|
||||
/// Receiver for resolved identities from the DNS responder.
|
||||
@@ -530,6 +533,8 @@ impl Node {
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
@@ -640,6 +645,8 @@ impl Node {
|
||||
tun_outbound_rx: None,
|
||||
tun_reader_handle: None,
|
||||
tun_writer_handle: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
tun_shutdown_fd: None,
|
||||
dns_identity_rx: None,
|
||||
dns_task: None,
|
||||
index_allocator: IndexAllocator::new(),
|
||||
@@ -695,23 +702,19 @@ impl Node {
|
||||
}
|
||||
|
||||
// Create Ethernet transport instances
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let eth_instances: Vec<_> = self
|
||||
.config
|
||||
.transports
|
||||
.ethernet
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
|
||||
let xonly = self.identity.pubkey();
|
||||
for (name, eth_config) in eth_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let mut eth = EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
|
||||
eth.set_local_pubkey(xonly);
|
||||
transports.push(TransportHandle::Ethernet(eth));
|
||||
}
|
||||
let eth_instances: Vec<_> = self
|
||||
.config
|
||||
.transports
|
||||
.ethernet
|
||||
.iter()
|
||||
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
|
||||
.collect();
|
||||
let xonly = self.identity.pubkey();
|
||||
for (name, eth_config) in eth_instances {
|
||||
let transport_id = self.allocate_transport_id();
|
||||
let mut eth = EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
|
||||
eth.set_local_pubkey(xonly);
|
||||
transports.push(TransportHandle::Ethernet(eth));
|
||||
}
|
||||
|
||||
// Create TCP transport instances
|
||||
@@ -831,18 +834,9 @@ impl Node {
|
||||
))
|
||||
})?;
|
||||
|
||||
// Parse the MAC address
|
||||
#[cfg(target_os = "linux")]
|
||||
let mac = crate::transport::ethernet::parse_mac_string(mac_str).map_err(|e| {
|
||||
NodeError::NoTransportForType(format!("invalid MAC in '{}': {}", addr_str, e))
|
||||
})?;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mac: [u8; 6] = {
|
||||
let _ = mac_str;
|
||||
return Err(NodeError::NoTransportForType(
|
||||
"Ethernet transport not available on this platform".into(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok((transport_id, TransportAddr::from_bytes(&mac)))
|
||||
}
|
||||
|
||||
+12
-4
@@ -272,12 +272,20 @@ mod tests {
|
||||
}
|
||||
assert!(!bucket.available());
|
||||
|
||||
// Wait for refill
|
||||
thread::sleep(Duration::from_millis(50)); // Should refill ~5 tokens
|
||||
// Wait for refill, measuring actual elapsed time to avoid sensitivity
|
||||
// to OS scheduler variance (sleep can overshoot by a large margin).
|
||||
let before = Instant::now();
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
let elapsed_secs = before.elapsed().as_secs_f64();
|
||||
|
||||
// Expected tokens = elapsed * rate, capped at capacity.
|
||||
// Allow ±20% tolerance around the actual elapsed time.
|
||||
let expected = (elapsed_secs * 100.0).min(10.0);
|
||||
let lo = (expected * 0.8).min(expected - 0.5).max(0.0);
|
||||
let hi = (expected * 1.2).max(expected + 0.5).min(10.0);
|
||||
|
||||
// Should have tokens now
|
||||
let tokens = bucket.tokens();
|
||||
assert!((4.0..=6.0).contains(&tokens), "tokens: {}", tokens);
|
||||
assert!((lo..=hi).contains(&tokens), "tokens: {}, expected ~{:.2} (range {:.2}..={:.2})", tokens, expected, lo, hi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,7 +9,6 @@ mod bloom;
|
||||
mod ble;
|
||||
mod disconnect;
|
||||
mod discovery;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod ethernet;
|
||||
mod forwarding;
|
||||
mod handshake;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! Ethernet Transport Implementation
|
||||
//!
|
||||
//! Provides raw Ethernet transport for FIPS peer communication using
|
||||
//! AF_PACKET sockets with SOCK_DGRAM. Works on wired Ethernet and WiFi
|
||||
//! interfaces (kernel mac80211 abstracts 802.11 transparently).
|
||||
//! Provides raw Ethernet transport for FIPS peer communication. On Linux,
|
||||
//! uses AF_PACKET/SOCK_DGRAM sockets; on macOS, uses BPF devices (`/dev/bpf*`).
|
||||
//! Works on wired Ethernet and WiFi interfaces (kernel mac80211 abstracts
|
||||
//! 802.11 transparently on Linux).
|
||||
|
||||
pub mod discovery;
|
||||
pub mod socket;
|
||||
@@ -240,16 +241,26 @@ impl EthernetTransport {
|
||||
return Err(TransportError::NotStarted);
|
||||
}
|
||||
|
||||
// Abort beacon task
|
||||
if let Some(task) = self.beacon_task.take() {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
// Signal the socket to shut down. On macOS this writes to the
|
||||
// shutdown pipe, waking the reader thread's select() immediately.
|
||||
// On Linux this is a no-op (AsyncFd cancellation handles it).
|
||||
if let Some(ref socket) = self.socket {
|
||||
socket.shutdown();
|
||||
}
|
||||
|
||||
// Abort receive task
|
||||
// Abort tasks. On Linux, safe to await since all I/O is
|
||||
// AsyncFd-based and cancellation-safe. On macOS, do NOT await —
|
||||
// on a current_thread runtime the aborted task can't be polled
|
||||
// while we're blocked on the JoinHandle, causing a deadlock.
|
||||
if let Some(task) = self.beacon_task.take() {
|
||||
task.abort();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{ let _ = task.await; }
|
||||
}
|
||||
if let Some(task) = self.recv_task.take() {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{ let _ = task.await; }
|
||||
}
|
||||
|
||||
// Drop socket
|
||||
|
||||
+255
-384
@@ -1,396 +1,267 @@
|
||||
//! AF_PACKET socket creation, binding, and ioctl helpers.
|
||||
//! Raw Ethernet socket abstraction.
|
||||
//!
|
||||
//! Platform-specific implementations live in `socket_linux.rs` (AF_PACKET)
|
||||
//! and `socket_macos.rs` (BPF). This module re-exports `PacketSocket` and
|
||||
//! provides `AsyncPacketSocket`.
|
||||
|
||||
use crate::transport::TransportError;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
/// Broadcast MAC address.
|
||||
pub const ETHERNET_BROADCAST: [u8; 6] = [0xff; 6];
|
||||
|
||||
/// Wrapper around an AF_PACKET SOCK_DGRAM file descriptor.
|
||||
///
|
||||
/// Owns the fd and closes it on drop. Provides synchronous send/recv
|
||||
/// methods used by the async wrappers via `AsyncFd`.
|
||||
pub struct PacketSocket {
|
||||
fd: RawFd,
|
||||
if_index: i32,
|
||||
ethertype: u16,
|
||||
// Platform-specific PacketSocket implementation.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "socket_linux.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "socket_macos.rs"]
|
||||
mod platform;
|
||||
|
||||
pub use platform::PacketSocket;
|
||||
|
||||
// =============================================================================
|
||||
// Linux: AsyncFd-based async wrapper
|
||||
// =============================================================================
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod async_impl {
|
||||
use super::PacketSocket;
|
||||
use crate::transport::TransportError;
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
pub struct AsyncPacketSocket {
|
||||
inner: AsyncFd<PacketSocket>,
|
||||
}
|
||||
|
||||
impl AsyncPacketSocket {
|
||||
pub fn new(socket: PacketSocket) -> Result<Self, TransportError> {
|
||||
let async_fd = AsyncFd::new(socket)
|
||||
.map_err(|e| TransportError::StartFailed(format!("AsyncFd::new failed: {}", e)))?;
|
||||
Ok(Self { inner: async_fd })
|
||||
}
|
||||
|
||||
pub async fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> Result<usize, TransportError> {
|
||||
loop {
|
||||
let mut guard = self
|
||||
.inner
|
||||
.writable()
|
||||
.await
|
||||
.map_err(|e| TransportError::SendFailed(format!("writable wait: {}", e)))?;
|
||||
|
||||
match guard.try_io(|inner| inner.get_ref().send_to(data, dest_mac)) {
|
||||
Ok(Ok(n)) => return Ok(n),
|
||||
Ok(Err(e)) => return Err(TransportError::SendFailed(format!("{}", e))),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn recv_from(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
) -> Result<(usize, [u8; 6]), TransportError> {
|
||||
loop {
|
||||
let mut guard = self
|
||||
.inner
|
||||
.readable()
|
||||
.await
|
||||
.map_err(|e| TransportError::RecvFailed(format!("readable wait: {}", e)))?;
|
||||
|
||||
match guard.try_io(|inner| inner.get_ref().recv_from(buf)) {
|
||||
Ok(Ok(result)) => return Ok(result),
|
||||
Ok(Err(e)) => return Err(TransportError::RecvFailed(format!("{}", e))),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ref(&self) -> &PacketSocket {
|
||||
self.inner.get_ref()
|
||||
}
|
||||
|
||||
/// Shut down the socket, unblocking any pending recv.
|
||||
///
|
||||
/// On Linux this is a no-op — aborting the tokio task suffices
|
||||
/// since AsyncFd is cancellation-aware.
|
||||
pub fn shutdown(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// macOS: dedicated reader thread with async channel
|
||||
//
|
||||
// BPF fds don't support kqueue, so we can't use AsyncFd. Instead of
|
||||
// spawn_blocking per packet (which was the bottleneck causing 84 Mbps),
|
||||
// we spawn a single dedicated reader thread that loops on blocking
|
||||
// read() and feeds frames through a tokio mpsc channel.
|
||||
// =============================================================================
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod async_impl {
|
||||
use super::PacketSocket;
|
||||
use crate::transport::TransportError;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A received frame: (payload, source_mac).
|
||||
type Frame = (Vec<u8>, [u8; 6]);
|
||||
|
||||
pub struct AsyncPacketSocket {
|
||||
inner: Arc<PacketSocket>,
|
||||
rx: tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Frame>>,
|
||||
reader_thread: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl AsyncPacketSocket {
|
||||
pub fn new(socket: PacketSocket) -> Result<Self, TransportError> {
|
||||
// Channel capacity: buffer up to 1024 frames to decouple
|
||||
// the blocking reader from the async consumer.
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Frame>(1024);
|
||||
let inner = Arc::new(socket);
|
||||
let reader_socket = Arc::clone(&inner);
|
||||
|
||||
let reader_thread = std::thread::Builder::new()
|
||||
.name("bpf-reader".into())
|
||||
.spawn(move || {
|
||||
let bpf_fd = reader_socket.as_raw_fd();
|
||||
let shutdown_fd = reader_socket.shutdown_read_fd();
|
||||
let bpf_buflen = reader_socket.bpf_buflen();
|
||||
let mut read_buf = vec![0u8; bpf_buflen];
|
||||
let mut parse_buf = vec![0u8; bpf_buflen];
|
||||
let mut parse_offset: usize = 0;
|
||||
let mut parse_len: usize = 0;
|
||||
let nfds = bpf_fd.max(shutdown_fd) + 1;
|
||||
|
||||
loop {
|
||||
// Drain any buffered frames from the previous read
|
||||
while let Some(result) = super::platform::parse_next_frame(
|
||||
&parse_buf, &mut parse_offset, parse_len, &mut read_buf,
|
||||
) {
|
||||
match result {
|
||||
Ok((n, mac)) => {
|
||||
let data = read_buf[..n].to_vec();
|
||||
if tx.blocking_send((data, mac)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for BPF data or shutdown signal via select()
|
||||
unsafe {
|
||||
let mut read_fds: libc::fd_set = std::mem::zeroed();
|
||||
libc::FD_ZERO(&mut read_fds);
|
||||
libc::FD_SET(bpf_fd, &mut read_fds);
|
||||
libc::FD_SET(shutdown_fd, &mut read_fds);
|
||||
|
||||
let ret = libc::select(
|
||||
nfds,
|
||||
&mut read_fds,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() == std::io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if libc::FD_ISSET(shutdown_fd, &read_fds) {
|
||||
break; // shutdown signal
|
||||
}
|
||||
}
|
||||
|
||||
// BPF fd is readable
|
||||
let ret = unsafe {
|
||||
libc::read(
|
||||
bpf_fd,
|
||||
parse_buf.as_mut_ptr() as *mut libc::c_void,
|
||||
bpf_buflen,
|
||||
)
|
||||
};
|
||||
if ret <= 0 {
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EBADF) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
parse_len = 0;
|
||||
parse_offset = 0;
|
||||
continue;
|
||||
}
|
||||
parse_len = ret as usize;
|
||||
parse_offset = 0;
|
||||
}
|
||||
})
|
||||
.map_err(|e| TransportError::StartFailed(format!("reader thread: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
rx: tokio::sync::Mutex::new(rx),
|
||||
reader_thread: Some(reader_thread),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> Result<usize, TransportError> {
|
||||
let socket = Arc::clone(&self.inner);
|
||||
let data = data.to_vec();
|
||||
let dest = *dest_mac;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
socket.send_to(&data, &dest)
|
||||
.map_err(|e| TransportError::SendFailed(format!("{}", e)))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| TransportError::SendFailed(format!("spawn_blocking: {}", e)))?
|
||||
}
|
||||
|
||||
pub async fn recv_from(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
) -> Result<(usize, [u8; 6]), TransportError> {
|
||||
let mut rx = self.rx.lock().await;
|
||||
match rx.recv().await {
|
||||
Some((data, mac)) => {
|
||||
let n = data.len().min(buf.len());
|
||||
buf[..n].copy_from_slice(&data[..n]);
|
||||
Ok((n, mac))
|
||||
}
|
||||
None => Err(TransportError::RecvFailed("reader thread stopped".into())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ref(&self) -> &PacketSocket {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Signal the reader thread to stop.
|
||||
///
|
||||
/// Sets the shutdown flag; the reader thread checks it after
|
||||
/// each BPF read timeout (~250ms) and exits.
|
||||
pub fn shutdown(&self) {
|
||||
self.inner.request_shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AsyncPacketSocket {
|
||||
fn drop(&mut self) {
|
||||
self.inner.request_shutdown();
|
||||
if let Some(handle) = self.reader_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use async_impl::AsyncPacketSocket;
|
||||
|
||||
impl PacketSocket {
|
||||
/// Create and bind an AF_PACKET SOCK_DGRAM socket.
|
||||
///
|
||||
/// Returns an error with a clear message if CAP_NET_RAW is missing.
|
||||
pub fn open(interface: &str, ethertype: u16) -> Result<Self, TransportError> {
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_PACKET,
|
||||
libc::SOCK_DGRAM,
|
||||
(ethertype).to_be() as i32,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EPERM) {
|
||||
return Err(TransportError::StartFailed(
|
||||
"AF_PACKET requires CAP_NET_RAW capability \
|
||||
(run as root or use: setcap cap_net_raw=ep <binary>)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"socket(AF_PACKET) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
// Look up interface index
|
||||
let if_index = get_if_index(fd, interface)?;
|
||||
|
||||
// Bind to the interface
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
sll.sll_family = libc::AF_PACKET as u16;
|
||||
sll.sll_protocol = ethertype.to_be();
|
||||
sll.sll_ifindex = if_index;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::bind(
|
||||
fd,
|
||||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"bind(AF_PACKET, {}) failed: {}",
|
||||
interface, err
|
||||
)));
|
||||
}
|
||||
|
||||
// Set non-blocking for async integration
|
||||
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
||||
if flags < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"fcntl(F_GETFL) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"fcntl(F_SETFL, O_NONBLOCK) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
fd,
|
||||
if_index,
|
||||
ethertype,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the interface index.
|
||||
pub fn if_index(&self) -> i32 {
|
||||
self.if_index
|
||||
}
|
||||
|
||||
/// Get the local MAC address of the bound interface.
|
||||
pub fn local_mac(&self) -> Result<[u8; 6], TransportError> {
|
||||
get_mac_addr(self.fd, self.if_index)
|
||||
}
|
||||
|
||||
/// Get the interface MTU.
|
||||
pub fn interface_mtu(&self) -> Result<u16, TransportError> {
|
||||
get_if_mtu(self.fd, self.if_index)
|
||||
}
|
||||
|
||||
/// Set the socket receive buffer size.
|
||||
pub fn set_recv_buffer_size(&self, size: usize) -> Result<(), TransportError> {
|
||||
let size = size as libc::c_int;
|
||||
let ret = unsafe {
|
||||
libc::setsockopt(
|
||||
self.fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_RCVBUF,
|
||||
&size as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"setsockopt(SO_RCVBUF) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the socket send buffer size.
|
||||
pub fn set_send_buffer_size(&self, size: usize) -> Result<(), TransportError> {
|
||||
let size = size as libc::c_int;
|
||||
let ret = unsafe {
|
||||
libc::setsockopt(
|
||||
self.fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_SNDBUF,
|
||||
&size as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"setsockopt(SO_SNDBUF) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a payload to a destination MAC address.
|
||||
///
|
||||
/// Returns the number of bytes sent, or an io::Error.
|
||||
pub fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> std::io::Result<usize> {
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
sll.sll_family = libc::AF_PACKET as u16;
|
||||
sll.sll_protocol = self.ethertype.to_be();
|
||||
sll.sll_ifindex = self.if_index;
|
||||
sll.sll_halen = 6;
|
||||
sll.sll_addr[..6].copy_from_slice(dest_mac);
|
||||
|
||||
let ret = unsafe {
|
||||
libc::sendto(
|
||||
self.fd,
|
||||
data.as_ptr() as *const libc::c_void,
|
||||
data.len(),
|
||||
0,
|
||||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a payload and source MAC address.
|
||||
///
|
||||
/// Returns (bytes_read, source_mac), or an io::Error.
|
||||
pub fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, [u8; 6])> {
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
let mut sll_len = std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::recvfrom(
|
||||
self.fd,
|
||||
buf.as_mut_ptr() as *mut libc::c_void,
|
||||
buf.len(),
|
||||
0,
|
||||
&mut sll as *mut libc::sockaddr_ll as *mut libc::sockaddr,
|
||||
&mut sll_len,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let mut src_mac = [0u8; 6];
|
||||
src_mac.copy_from_slice(&sll.sll_addr[..6]);
|
||||
|
||||
Ok((ret as usize, src_mac))
|
||||
}
|
||||
|
||||
/// Wrap this socket in a tokio AsyncFd for async I/O.
|
||||
/// Wrap this socket in an async wrapper for tokio integration.
|
||||
pub fn into_async(self) -> Result<AsyncPacketSocket, TransportError> {
|
||||
let async_fd = AsyncFd::new(self)
|
||||
.map_err(|e| TransportError::StartFailed(format!("AsyncFd::new failed: {}", e)))?;
|
||||
Ok(AsyncPacketSocket { inner: async_fd })
|
||||
AsyncPacketSocket::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for PacketSocket {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PacketSocket {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Async wrapper around PacketSocket using tokio's AsyncFd.
|
||||
pub struct AsyncPacketSocket {
|
||||
inner: AsyncFd<PacketSocket>,
|
||||
}
|
||||
|
||||
impl AsyncPacketSocket {
|
||||
/// Send a payload to a destination MAC address.
|
||||
pub async fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> Result<usize, TransportError> {
|
||||
loop {
|
||||
let mut guard = self
|
||||
.inner
|
||||
.writable()
|
||||
.await
|
||||
.map_err(|e| TransportError::SendFailed(format!("writable wait: {}", e)))?;
|
||||
|
||||
match guard.try_io(|inner| inner.get_ref().send_to(data, dest_mac)) {
|
||||
Ok(Ok(n)) => return Ok(n),
|
||||
Ok(Err(e)) => return Err(TransportError::SendFailed(format!("{}", e))),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a payload and source MAC address.
|
||||
pub async fn recv_from(
|
||||
&self,
|
||||
buf: &mut [u8],
|
||||
) -> Result<(usize, [u8; 6]), TransportError> {
|
||||
loop {
|
||||
let mut guard = self
|
||||
.inner
|
||||
.readable()
|
||||
.await
|
||||
.map_err(|e| TransportError::RecvFailed(format!("readable wait: {}", e)))?;
|
||||
|
||||
match guard.try_io(|inner| inner.get_ref().recv_from(buf)) {
|
||||
Ok(Ok(result)) => return Ok(result),
|
||||
Ok(Err(e)) => return Err(TransportError::RecvFailed(format!("{}", e))),
|
||||
Err(_would_block) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the inner PacketSocket.
|
||||
pub fn get_ref(&self) -> &PacketSocket {
|
||||
self.inner.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ioctl helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Get the interface index by name.
|
||||
fn get_if_index(_fd: RawFd, interface: &str) -> Result<i32, TransportError> {
|
||||
let c_name = std::ffi::CString::new(interface).map_err(|_| {
|
||||
TransportError::StartFailed(format!("invalid interface name: {}", interface))
|
||||
})?;
|
||||
|
||||
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
|
||||
if idx == 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"interface not found: {} ({})",
|
||||
interface,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(idx as i32)
|
||||
}
|
||||
|
||||
/// Get the MAC address of an interface by its index.
|
||||
fn get_mac_addr(fd: RawFd, if_index: i32) -> Result<[u8; 6], TransportError> {
|
||||
// First get the interface name from the index
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
|
||||
// Use if_indextoname to get the name
|
||||
let mut name_buf = [0u8; libc::IFNAMSIZ];
|
||||
let ret = unsafe {
|
||||
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr() as *mut libc::c_char)
|
||||
};
|
||||
if ret.is_null() {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}",
|
||||
if_index,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
// Copy name into ifreq
|
||||
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
|
||||
let copy_len = name_len.min(libc::IFNAMSIZ - 1);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_env = "musl")]
|
||||
let ioctl_req = libc::SIOCGIFHWADDR as libc::c_int;
|
||||
#[cfg(not(target_env = "musl"))]
|
||||
let ioctl_req = libc::SIOCGIFHWADDR as libc::c_ulong;
|
||||
let ret = unsafe { libc::ioctl(fd, ioctl_req, &ifr) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFHWADDR) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut mac = [0u8; 6];
|
||||
unsafe {
|
||||
let sa_data = ifr.ifr_ifru.ifru_hwaddr.sa_data;
|
||||
for (i, byte) in mac.iter_mut().enumerate() {
|
||||
*byte = sa_data[i] as u8;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
/// Get the MTU of an interface by its index.
|
||||
fn get_if_mtu(fd: RawFd, if_index: i32) -> Result<u16, TransportError> {
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
|
||||
// Get the interface name from index
|
||||
let mut name_buf = [0u8; libc::IFNAMSIZ];
|
||||
let ret = unsafe {
|
||||
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr() as *mut libc::c_char)
|
||||
};
|
||||
if ret.is_null() {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}",
|
||||
if_index,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
|
||||
let copy_len = name_len.min(libc::IFNAMSIZ - 1);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_env = "musl")]
|
||||
let ioctl_req = libc::SIOCGIFMTU as libc::c_int;
|
||||
#[cfg(not(target_env = "musl"))]
|
||||
let ioctl_req = libc::SIOCGIFMTU as libc::c_ulong;
|
||||
let ret = unsafe { libc::ioctl(fd, ioctl_req, &ifr) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFMTU) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u16;
|
||||
Ok(mtu)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
//! AF_PACKET socket creation, binding, and ioctl helpers (Linux).
|
||||
|
||||
use crate::transport::TransportError;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
/// Wrapper around an AF_PACKET SOCK_DGRAM file descriptor.
|
||||
///
|
||||
/// Owns the fd and closes it on drop. Provides synchronous send/recv
|
||||
/// methods used by the async wrappers via `AsyncFd`.
|
||||
pub struct PacketSocket {
|
||||
fd: RawFd,
|
||||
if_index: i32,
|
||||
ethertype: u16,
|
||||
}
|
||||
|
||||
impl PacketSocket {
|
||||
/// Create and bind an AF_PACKET SOCK_DGRAM socket.
|
||||
///
|
||||
/// Returns an error with a clear message if CAP_NET_RAW is missing.
|
||||
pub fn open(interface: &str, ethertype: u16) -> Result<Self, TransportError> {
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_PACKET,
|
||||
libc::SOCK_DGRAM,
|
||||
(ethertype).to_be() as i32,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EPERM) {
|
||||
return Err(TransportError::StartFailed(
|
||||
"AF_PACKET requires CAP_NET_RAW capability \
|
||||
(run as root or use: setcap cap_net_raw=ep <binary>)"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"socket(AF_PACKET) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
// Look up interface index
|
||||
let if_index = get_if_index(fd, interface)?;
|
||||
|
||||
// Bind to the interface
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
sll.sll_family = libc::AF_PACKET as u16;
|
||||
sll.sll_protocol = ethertype.to_be();
|
||||
sll.sll_ifindex = if_index;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::bind(
|
||||
fd,
|
||||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"bind(AF_PACKET, {}) failed: {}",
|
||||
interface, err
|
||||
)));
|
||||
}
|
||||
|
||||
// Set non-blocking for async integration
|
||||
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
||||
if flags < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"fcntl(F_GETFL) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
let ret = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"fcntl(F_SETFL, O_NONBLOCK) failed: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
fd,
|
||||
if_index,
|
||||
ethertype,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the interface index.
|
||||
pub fn if_index(&self) -> i32 {
|
||||
self.if_index
|
||||
}
|
||||
|
||||
/// Get the local MAC address of the bound interface.
|
||||
pub fn local_mac(&self) -> Result<[u8; 6], TransportError> {
|
||||
get_mac_addr(self.fd, self.if_index)
|
||||
}
|
||||
|
||||
/// Get the interface MTU.
|
||||
pub fn interface_mtu(&self) -> Result<u16, TransportError> {
|
||||
get_if_mtu(self.fd, self.if_index)
|
||||
}
|
||||
|
||||
/// Set the socket receive buffer size.
|
||||
pub fn set_recv_buffer_size(&self, size: usize) -> Result<(), TransportError> {
|
||||
let size = size as libc::c_int;
|
||||
let ret = unsafe {
|
||||
libc::setsockopt(
|
||||
self.fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_RCVBUF,
|
||||
&size as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"setsockopt(SO_RCVBUF) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the socket send buffer size.
|
||||
pub fn set_send_buffer_size(&self, size: usize) -> Result<(), TransportError> {
|
||||
let size = size as libc::c_int;
|
||||
let ret = unsafe {
|
||||
libc::setsockopt(
|
||||
self.fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_SNDBUF,
|
||||
&size as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"setsockopt(SO_SNDBUF) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a payload to a destination MAC address.
|
||||
///
|
||||
/// Returns the number of bytes sent, or an io::Error.
|
||||
pub fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> std::io::Result<usize> {
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
sll.sll_family = libc::AF_PACKET as u16;
|
||||
sll.sll_protocol = self.ethertype.to_be();
|
||||
sll.sll_ifindex = self.if_index;
|
||||
sll.sll_halen = 6;
|
||||
sll.sll_addr[..6].copy_from_slice(dest_mac);
|
||||
|
||||
let ret = unsafe {
|
||||
libc::sendto(
|
||||
self.fd,
|
||||
data.as_ptr() as *const libc::c_void,
|
||||
data.len(),
|
||||
0,
|
||||
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
|
||||
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a payload and source MAC address.
|
||||
///
|
||||
/// Returns (bytes_read, source_mac), or an io::Error.
|
||||
pub fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, [u8; 6])> {
|
||||
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
|
||||
let mut sll_len = std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::recvfrom(
|
||||
self.fd,
|
||||
buf.as_mut_ptr() as *mut libc::c_void,
|
||||
buf.len(),
|
||||
0,
|
||||
&mut sll as *mut libc::sockaddr_ll as *mut libc::sockaddr,
|
||||
&mut sll_len,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let mut src_mac = [0u8; 6];
|
||||
src_mac.copy_from_slice(&sll.sll_addr[..6]);
|
||||
|
||||
Ok((ret as usize, src_mac))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for PacketSocket {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PacketSocket {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ioctl helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Get the interface index by name.
|
||||
fn get_if_index(_fd: RawFd, interface: &str) -> Result<i32, TransportError> {
|
||||
let c_name = std::ffi::CString::new(interface).map_err(|_| {
|
||||
TransportError::StartFailed(format!("invalid interface name: {}", interface))
|
||||
})?;
|
||||
|
||||
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
|
||||
if idx == 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"interface not found: {} ({})",
|
||||
interface,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(idx as i32)
|
||||
}
|
||||
|
||||
/// Get the MAC address of an interface by its index.
|
||||
fn get_mac_addr(fd: RawFd, if_index: i32) -> Result<[u8; 6], TransportError> {
|
||||
// First get the interface name from the index
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
|
||||
// Use if_indextoname to get the name
|
||||
let mut name_buf = [0u8; libc::IFNAMSIZ];
|
||||
let ret = unsafe {
|
||||
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr() as *mut libc::c_char)
|
||||
};
|
||||
if ret.is_null() {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}",
|
||||
if_index,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
// Copy name into ifreq
|
||||
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
|
||||
let copy_len = name_len.min(libc::IFNAMSIZ - 1);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_env = "musl")]
|
||||
let ioctl_req = libc::SIOCGIFHWADDR as libc::c_int;
|
||||
#[cfg(not(target_env = "musl"))]
|
||||
let ioctl_req = libc::SIOCGIFHWADDR as libc::c_ulong;
|
||||
let ret = unsafe { libc::ioctl(fd, ioctl_req, &ifr) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFHWADDR) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut mac = [0u8; 6];
|
||||
unsafe {
|
||||
let sa_data = ifr.ifr_ifru.ifru_hwaddr.sa_data;
|
||||
for (i, byte) in mac.iter_mut().enumerate() {
|
||||
*byte = sa_data[i] as u8;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
/// Get the MTU of an interface by its index.
|
||||
fn get_if_mtu(fd: RawFd, if_index: i32) -> Result<u16, TransportError> {
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
|
||||
// Get the interface name from index
|
||||
let mut name_buf = [0u8; libc::IFNAMSIZ];
|
||||
let ret = unsafe {
|
||||
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr() as *mut libc::c_char)
|
||||
};
|
||||
if ret.is_null() {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}",
|
||||
if_index,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let name_len = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
|
||||
let copy_len = name_len.min(libc::IFNAMSIZ - 1);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr() as *mut u8,
|
||||
copy_len,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_env = "musl")]
|
||||
let ioctl_req = libc::SIOCGIFMTU as libc::c_int;
|
||||
#[cfg(not(target_env = "musl"))]
|
||||
let ioctl_req = libc::SIOCGIFMTU as libc::c_ulong;
|
||||
let ret = unsafe { libc::ioctl(fd, ioctl_req, &ifr) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFMTU) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u16;
|
||||
Ok(mtu)
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
//! BPF-based raw Ethernet socket for macOS.
|
||||
//!
|
||||
//! Provides the same `PacketSocket` API as the Linux AF_PACKET implementation,
|
||||
//! using Berkeley Packet Filter (BPF) devices (`/dev/bpf*`).
|
||||
//!
|
||||
//! Key differences from Linux AF_PACKET SOCK_DGRAM:
|
||||
//! - BPF operates on raw frames including the 14-byte Ethernet header.
|
||||
//! `send_to()` prepends it; `recv_from()` strips it.
|
||||
//! - BPF reads may return multiple frames in one buffer (chained `bpf_hdr`).
|
||||
//! - MAC address is obtained via `getifaddrs()` with `AF_LINK`.
|
||||
|
||||
use crate::transport::TransportError;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Ethernet header size: dst(6) + src(6) + ethertype(2).
|
||||
const ETH_HDRLEN: usize = 14;
|
||||
|
||||
/// macOS SIOCGIFMTU ioctl constant.
|
||||
const SIOCGIFMTU: libc::c_ulong = 0xC0206933;
|
||||
|
||||
/// BPF internal read state (behind Mutex for interior mutability,
|
||||
/// since the async recv path needs `&self` access).
|
||||
struct BpfReadState {
|
||||
buf: Vec<u8>,
|
||||
offset: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
/// RAII guard that closes a raw fd on drop, used during socket setup
|
||||
/// to prevent fd leaks if an intermediate step fails.
|
||||
struct FdGuard(RawFd);
|
||||
|
||||
impl FdGuard {
|
||||
/// Disarm the guard, returning the fd without closing it.
|
||||
fn into_raw(self) -> RawFd {
|
||||
let fd = self.0;
|
||||
std::mem::forget(self);
|
||||
fd
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FdGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::close(self.0); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around a BPF file descriptor.
|
||||
///
|
||||
/// Owns the fd and closes it on drop. Provides synchronous send/recv
|
||||
/// methods matching the Linux `PacketSocket` API.
|
||||
pub struct PacketSocket {
|
||||
fd: RawFd,
|
||||
if_index: i32,
|
||||
ethertype: u16,
|
||||
local_mac: [u8; 6],
|
||||
bpf_buflen: usize,
|
||||
read_state: Mutex<BpfReadState>,
|
||||
/// Write end of the shutdown pipe. Writing a byte wakes the reader
|
||||
/// thread's `select()` loop so it can exit promptly.
|
||||
shutdown_write_fd: RawFd,
|
||||
/// Read end of the shutdown pipe (passed to the reader thread).
|
||||
shutdown_read_fd: RawFd,
|
||||
}
|
||||
|
||||
impl PacketSocket {
|
||||
/// Open a BPF device and bind it to the named interface.
|
||||
pub fn open(interface: &str, ethertype: u16) -> Result<Self, TransportError> {
|
||||
// Open the first available /dev/bpf device
|
||||
let guard = FdGuard(open_bpf_device()?);
|
||||
let fd = guard.0;
|
||||
|
||||
// Look up interface index (for API compat; BPF binds by name)
|
||||
let if_index = get_if_index(interface)?;
|
||||
|
||||
// Bind to the interface
|
||||
bind_to_interface(fd, interface)?;
|
||||
|
||||
// Set immediate mode (deliver packets as they arrive)
|
||||
set_bpf_immediate(fd)?;
|
||||
|
||||
// We supply complete Ethernet headers on writes
|
||||
set_bpf_hdrcmplt(fd)?;
|
||||
|
||||
// Install a BPF filter to only capture our EtherType
|
||||
install_ethertype_filter(fd, ethertype)?;
|
||||
|
||||
// Get the BPF read buffer length
|
||||
let bpf_buflen = get_bpf_buflen(fd)?;
|
||||
|
||||
// Get local MAC address
|
||||
let local_mac = get_mac_addr(interface)?;
|
||||
|
||||
// Create shutdown pipe — the reader thread select()s on both
|
||||
// the BPF fd and this pipe. Writing a byte signals shutdown.
|
||||
let mut pipe_fds = [0i32; 2];
|
||||
if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } < 0 {
|
||||
unsafe { libc::close(guard.0) };
|
||||
std::mem::forget(guard); // don't double-close via FdGuard
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"pipe() failed: {}", std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
// All setup succeeded — disarm the guard so fd isn't closed
|
||||
guard.into_raw();
|
||||
|
||||
Ok(Self {
|
||||
fd,
|
||||
if_index,
|
||||
ethertype,
|
||||
local_mac,
|
||||
bpf_buflen,
|
||||
read_state: Mutex::new(BpfReadState {
|
||||
buf: vec![0u8; bpf_buflen],
|
||||
offset: 0,
|
||||
len: 0,
|
||||
}),
|
||||
shutdown_read_fd: pipe_fds[0],
|
||||
shutdown_write_fd: pipe_fds[1],
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the interface index.
|
||||
pub fn if_index(&self) -> i32 {
|
||||
self.if_index
|
||||
}
|
||||
|
||||
/// Get the local MAC address of the bound interface.
|
||||
pub fn local_mac(&self) -> Result<[u8; 6], TransportError> {
|
||||
Ok(self.local_mac)
|
||||
}
|
||||
|
||||
/// Get the interface MTU.
|
||||
pub fn interface_mtu(&self) -> Result<u16, TransportError> {
|
||||
get_if_mtu(self.if_index)
|
||||
}
|
||||
|
||||
/// Set the socket receive buffer size.
|
||||
///
|
||||
/// BPF devices use a fixed kernel buffer; silently ignored.
|
||||
pub fn set_recv_buffer_size(&self, _size: usize) -> Result<(), TransportError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the BPF read buffer length.
|
||||
pub fn bpf_buflen(&self) -> usize {
|
||||
self.bpf_buflen
|
||||
}
|
||||
|
||||
/// Get the read end of the shutdown pipe (for the reader thread).
|
||||
pub fn shutdown_read_fd(&self) -> RawFd {
|
||||
self.shutdown_read_fd
|
||||
}
|
||||
|
||||
/// Signal the reader thread to stop by writing to the shutdown pipe.
|
||||
pub fn request_shutdown(&self) {
|
||||
unsafe {
|
||||
libc::write(self.shutdown_write_fd, b"x".as_ptr() as *const libc::c_void, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the socket send buffer size.
|
||||
///
|
||||
/// BPF devices use a fixed kernel buffer; silently ignored.
|
||||
pub fn set_send_buffer_size(&self, _size: usize) -> Result<(), TransportError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a payload to a destination MAC address.
|
||||
///
|
||||
/// Prepends a 14-byte Ethernet header (dst + src + ethertype) using
|
||||
/// `writev` for zero-copy scatter-gather. Returns the payload bytes
|
||||
/// sent (excluding the Ethernet header).
|
||||
pub fn send_to(&self, data: &[u8], dest_mac: &[u8; 6]) -> std::io::Result<usize> {
|
||||
// Build the 14-byte Ethernet header on the stack
|
||||
let mut hdr = [0u8; ETH_HDRLEN];
|
||||
hdr[..6].copy_from_slice(dest_mac);
|
||||
hdr[6..12].copy_from_slice(&self.local_mac);
|
||||
hdr[12..14].copy_from_slice(&self.ethertype.to_be_bytes());
|
||||
|
||||
let iov = [
|
||||
libc::iovec {
|
||||
iov_base: hdr.as_ptr() as *mut libc::c_void,
|
||||
iov_len: ETH_HDRLEN,
|
||||
},
|
||||
libc::iovec {
|
||||
iov_base: data.as_ptr() as *mut libc::c_void,
|
||||
iov_len: data.len(),
|
||||
},
|
||||
];
|
||||
|
||||
let ret = unsafe { libc::writev(self.fd, iov.as_ptr(), 2) };
|
||||
if ret < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
// Return payload bytes (subtract Ethernet header)
|
||||
let sent = (ret as usize).saturating_sub(ETH_HDRLEN);
|
||||
Ok(sent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a payload and source MAC address.
|
||||
///
|
||||
/// BPF reads return raw frames with `bpf_hdr` prefixes. This method
|
||||
/// parses the next frame from the internal buffer, stripping the
|
||||
/// Ethernet header. Returns `(payload_bytes, source_mac)`.
|
||||
pub fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, [u8; 6])> {
|
||||
let mut state = self.read_state.lock().unwrap();
|
||||
let state = &mut *state;
|
||||
loop {
|
||||
// Try to parse the next frame from the read buffer
|
||||
if let Some(result) = parse_next_frame(&state.buf, &mut state.offset, state.len, buf) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Buffer exhausted — read more from BPF
|
||||
let ret = unsafe {
|
||||
libc::read(
|
||||
self.fd,
|
||||
state.buf.as_mut_ptr() as *mut libc::c_void,
|
||||
self.bpf_buflen,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
state.len = ret as usize;
|
||||
state.offset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the next BPF frame from the read buffer.
|
||||
///
|
||||
/// Returns `None` if the buffer is exhausted and needs a new `read()`.
|
||||
pub fn parse_next_frame(
|
||||
read_buf: &[u8],
|
||||
read_offset: &mut usize,
|
||||
read_len: usize,
|
||||
out_buf: &mut [u8],
|
||||
) -> Option<std::io::Result<(usize, [u8; 6])>> {
|
||||
const BPF_HDR_SIZE: usize = std::mem::size_of::<BpfHeader>();
|
||||
|
||||
if *read_offset >= read_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
let remaining = read_len - *read_offset;
|
||||
if remaining < BPF_HDR_SIZE {
|
||||
*read_offset = read_len;
|
||||
return None;
|
||||
}
|
||||
|
||||
// Read the bpf_hdr
|
||||
let hdr_ptr = read_buf[*read_offset..].as_ptr() as *const BpfHeader;
|
||||
let hdr = unsafe { std::ptr::read_unaligned(hdr_ptr) };
|
||||
let cap_len = hdr.bh_caplen as usize;
|
||||
let data_offset = hdr.bh_hdrlen as usize;
|
||||
|
||||
// Advance past this frame (BPF_WORDALIGN)
|
||||
let total_len = bpf_wordalign(data_offset + cap_len);
|
||||
let frame_start = *read_offset + data_offset;
|
||||
*read_offset += total_len;
|
||||
|
||||
// Validate we have enough captured data for an Ethernet header
|
||||
if cap_len < ETH_HDRLEN {
|
||||
return None; // Runt frame, skip
|
||||
}
|
||||
|
||||
if frame_start + cap_len > read_len {
|
||||
return None; // Truncated, skip
|
||||
}
|
||||
|
||||
let frame = &read_buf[frame_start..frame_start + cap_len];
|
||||
|
||||
// Extract source MAC from Ethernet header bytes [6..12]
|
||||
let mut src_mac = [0u8; 6];
|
||||
src_mac.copy_from_slice(&frame[6..12]);
|
||||
|
||||
// Copy payload (after 14-byte Ethernet header) into caller's buffer
|
||||
let payload = &frame[ETH_HDRLEN..];
|
||||
let copy_len = payload.len().min(out_buf.len());
|
||||
out_buf[..copy_len].copy_from_slice(&payload[..copy_len]);
|
||||
|
||||
Some(Ok((copy_len, src_mac)))
|
||||
}
|
||||
|
||||
impl AsRawFd for PacketSocket {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PacketSocket {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.fd);
|
||||
libc::close(self.shutdown_read_fd);
|
||||
libc::close(self.shutdown_write_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BPF helpers
|
||||
// ============================================================================
|
||||
|
||||
/// BPF header (matches `struct bpf_hdr` from `<net/bpf.h>`).
|
||||
///
|
||||
/// On macOS, `bpf_hdr` uses `struct BPF_TIMEVAL` which is `struct timeval32`
|
||||
/// (two `u32` fields) regardless of architecture. Total size: 20 bytes
|
||||
/// (4+4+4+4+2 = 18, padded to 20 for alignment).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct BpfHeader {
|
||||
bh_tstamp_sec: u32,
|
||||
bh_tstamp_usec: u32,
|
||||
bh_caplen: u32,
|
||||
bh_datalen: u32,
|
||||
bh_hdrlen: u16,
|
||||
_pad: u16,
|
||||
}
|
||||
|
||||
// Compile-time check that our struct matches the kernel's bpf_hdr (20 bytes).
|
||||
const _: () = assert!(std::mem::size_of::<BpfHeader>() == 20);
|
||||
|
||||
/// BPF word alignment (round up to next 4-byte boundary).
|
||||
fn bpf_wordalign(x: usize) -> usize {
|
||||
(x + 3) & !3
|
||||
}
|
||||
|
||||
/// BPF ioctl constants (from <net/bpf.h>).
|
||||
const BIOCSETIF: libc::c_ulong = 0x8020426C;
|
||||
const BIOCIMMEDIATE: libc::c_ulong = 0x80044270;
|
||||
const BIOCSHDRCMPLT: libc::c_ulong = 0x80044275;
|
||||
const BIOCSETF: libc::c_ulong = 0x80104267;
|
||||
const BIOCGBLEN: libc::c_ulong = 0x40044266;
|
||||
|
||||
/// BPF instruction.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct BpfInsn {
|
||||
code: u16,
|
||||
jt: u8,
|
||||
jf: u8,
|
||||
k: u32,
|
||||
}
|
||||
|
||||
/// BPF program.
|
||||
#[repr(C)]
|
||||
struct BpfProgram {
|
||||
bf_len: u32,
|
||||
bf_insns: *const BpfInsn,
|
||||
}
|
||||
|
||||
/// Open the first available `/dev/bpf*` device.
|
||||
fn open_bpf_device() -> Result<RawFd, TransportError> {
|
||||
for i in 0..256 {
|
||||
let path = format!("/dev/bpf{}", i);
|
||||
let c_path = std::ffi::CString::new(path.as_str()).unwrap();
|
||||
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR) };
|
||||
if fd >= 0 {
|
||||
return Ok(fd);
|
||||
}
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.raw_os_error() == Some(libc::EACCES) {
|
||||
return Err(TransportError::StartFailed(
|
||||
"BPF requires root (run with sudo)".into(),
|
||||
));
|
||||
}
|
||||
// EBUSY: device in use, try next one
|
||||
}
|
||||
Err(TransportError::StartFailed(
|
||||
"no available /dev/bpf* device".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Bind a BPF fd to a network interface.
|
||||
fn bind_to_interface(fd: RawFd, interface: &str) -> Result<(), TransportError> {
|
||||
let mut ifreq: [u8; 32] = [0; 32]; // struct ifreq
|
||||
let name_bytes = interface.as_bytes();
|
||||
let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1);
|
||||
ifreq[..copy_len].copy_from_slice(&name_bytes[..copy_len]);
|
||||
|
||||
let ret = unsafe { libc::ioctl(fd, BIOCSETIF, ifreq.as_ptr()) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"BIOCSETIF({}) failed: {}", interface, std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable immediate mode (deliver packets without buffering).
|
||||
fn set_bpf_immediate(fd: RawFd) -> Result<(), TransportError> {
|
||||
let enable: libc::c_uint = 1;
|
||||
let ret = unsafe { libc::ioctl(fd, BIOCIMMEDIATE, &enable) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"BIOCIMMEDIATE failed: {}", std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tell BPF we supply complete Ethernet headers on writes.
|
||||
fn set_bpf_hdrcmplt(fd: RawFd) -> Result<(), TransportError> {
|
||||
let enable: libc::c_uint = 1;
|
||||
let ret = unsafe { libc::ioctl(fd, BIOCSHDRCMPLT, &enable) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"BIOCSHDRCMPLT failed: {}", std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a BPF filter to only capture frames with the given EtherType.
|
||||
fn install_ethertype_filter(fd: RawFd, ethertype: u16) -> Result<(), TransportError> {
|
||||
// BPF program: match EtherType at offset 12 in Ethernet header
|
||||
// ldh [12] ; load half-word at offset 12 (EtherType)
|
||||
// jeq #ethertype, accept, reject
|
||||
// accept: ret #65535 ; accept entire packet
|
||||
// reject: ret #0 ; drop
|
||||
let filter = [
|
||||
BpfInsn { code: 0x28, jt: 0, jf: 0, k: 12 }, // ldh [12]
|
||||
BpfInsn { code: 0x15, jt: 0, jf: 1, k: ethertype as u32 }, // jeq #ethertype
|
||||
BpfInsn { code: 0x06, jt: 0, jf: 0, k: 0xFFFF }, // ret #65535
|
||||
BpfInsn { code: 0x06, jt: 0, jf: 0, k: 0 }, // ret #0
|
||||
];
|
||||
|
||||
let prog = BpfProgram {
|
||||
bf_len: filter.len() as u32,
|
||||
bf_insns: filter.as_ptr(),
|
||||
};
|
||||
|
||||
let ret = unsafe { libc::ioctl(fd, BIOCSETF, &prog) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"BIOCSETF failed: {}", std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the BPF read buffer length.
|
||||
fn get_bpf_buflen(fd: RawFd) -> Result<usize, TransportError> {
|
||||
let mut buflen: libc::c_uint = 0;
|
||||
let ret = unsafe { libc::ioctl(fd, BIOCGBLEN, &mut buflen) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"BIOCGBLEN failed: {}", std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(buflen as usize)
|
||||
}
|
||||
|
||||
/// Get the interface index by name.
|
||||
fn get_if_index(interface: &str) -> Result<i32, TransportError> {
|
||||
let c_name = std::ffi::CString::new(interface).map_err(|_| {
|
||||
TransportError::StartFailed(format!("invalid interface name: {}", interface))
|
||||
})?;
|
||||
|
||||
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
|
||||
if idx == 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"interface not found: {} ({})",
|
||||
interface,
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
Ok(idx as i32)
|
||||
}
|
||||
|
||||
/// Get the MAC address of an interface using `getifaddrs()` with `AF_LINK`.
|
||||
fn get_mac_addr(interface: &str) -> Result<[u8; 6], TransportError> {
|
||||
let mut addrs: *mut libc::ifaddrs = std::ptr::null_mut();
|
||||
let ret = unsafe { libc::getifaddrs(&mut addrs) };
|
||||
if ret != 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"getifaddrs() failed: {}", std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let result = (|| {
|
||||
let mut cur = addrs;
|
||||
while !cur.is_null() {
|
||||
let ifa = unsafe { &*cur };
|
||||
let name = unsafe { std::ffi::CStr::from_ptr(ifa.ifa_name) }
|
||||
.to_str()
|
||||
.unwrap_or("");
|
||||
|
||||
if name == interface && !ifa.ifa_addr.is_null() {
|
||||
let sa = unsafe { &*ifa.ifa_addr };
|
||||
if sa.sa_family as i32 == libc::AF_LINK {
|
||||
let sdl = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_dl) };
|
||||
let nlen = sdl.sdl_nlen as usize;
|
||||
// MAC address starts after the interface name in sdl_data
|
||||
let data_ptr = sdl.sdl_data.as_ptr();
|
||||
let mut mac = [0u8; 6];
|
||||
for i in 0..6 {
|
||||
mac[i] = unsafe { *data_ptr.add(nlen + i) } as u8;
|
||||
}
|
||||
return Ok(mac);
|
||||
}
|
||||
}
|
||||
cur = unsafe { (*cur).ifa_next };
|
||||
}
|
||||
Err(TransportError::StartFailed(format!(
|
||||
"MAC address not found for interface: {}", interface
|
||||
)))
|
||||
})();
|
||||
|
||||
unsafe { libc::freeifaddrs(addrs) };
|
||||
result
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unit tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// bpf_wordalign
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_bpf_wordalign_already_aligned() {
|
||||
assert_eq!(bpf_wordalign(0), 0);
|
||||
assert_eq!(bpf_wordalign(4), 4);
|
||||
assert_eq!(bpf_wordalign(8), 8);
|
||||
assert_eq!(bpf_wordalign(20), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bpf_wordalign_rounds_up() {
|
||||
assert_eq!(bpf_wordalign(1), 4);
|
||||
assert_eq!(bpf_wordalign(2), 4);
|
||||
assert_eq!(bpf_wordalign(3), 4);
|
||||
assert_eq!(bpf_wordalign(5), 8);
|
||||
assert_eq!(bpf_wordalign(21), 24);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// parse_next_frame helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Build a single BPF packet in a buffer.
|
||||
///
|
||||
/// Layout:
|
||||
/// [0..20] BpfHeader (bh_hdrlen = 20)
|
||||
/// [20..26] dst_mac
|
||||
/// [26..32] src_mac
|
||||
/// [32..34] ethertype (0x0800)
|
||||
/// [34..] payload
|
||||
/// [..] zero padding to BPF_WORDALIGN boundary
|
||||
fn make_bpf_packet(src_mac: [u8; 6], payload: &[u8]) -> Vec<u8> {
|
||||
let cap_len = ETH_HDRLEN + payload.len();
|
||||
let hdr = BpfHeader {
|
||||
bh_tstamp_sec: 0,
|
||||
bh_tstamp_usec: 0,
|
||||
bh_caplen: cap_len as u32,
|
||||
bh_datalen: cap_len as u32,
|
||||
bh_hdrlen: std::mem::size_of::<BpfHeader>() as u16,
|
||||
_pad: 0,
|
||||
};
|
||||
let hdr_size = std::mem::size_of::<BpfHeader>();
|
||||
let total = bpf_wordalign(hdr_size + cap_len);
|
||||
let mut buf = vec![0u8; total];
|
||||
|
||||
// Write header
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
&hdr as *const BpfHeader as *const u8,
|
||||
buf.as_mut_ptr(),
|
||||
hdr_size,
|
||||
);
|
||||
}
|
||||
|
||||
// Write Ethernet header: dst(6) + src(6) + ethertype(2)
|
||||
let frame_start = hdr_size;
|
||||
buf[frame_start..frame_start + 6].copy_from_slice(&[0xff; 6]); // dst = broadcast
|
||||
buf[frame_start + 6..frame_start + 12].copy_from_slice(&src_mac);
|
||||
buf[frame_start + 12] = 0x08;
|
||||
buf[frame_start + 13] = 0x00;
|
||||
|
||||
// Write payload
|
||||
buf[frame_start + ETH_HDRLEN..frame_start + ETH_HDRLEN + payload.len()]
|
||||
.copy_from_slice(payload);
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// parse_next_frame
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_frame_single() {
|
||||
let src_mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06];
|
||||
let payload = b"hello world";
|
||||
let buf = make_bpf_packet(src_mac, payload);
|
||||
|
||||
let mut out_buf = vec![0u8; 1500];
|
||||
let mut offset = 0usize;
|
||||
let len = buf.len();
|
||||
|
||||
let result = parse_next_frame(&buf, &mut offset, len, &mut out_buf)
|
||||
.expect("should return Some")
|
||||
.expect("should be Ok");
|
||||
|
||||
assert_eq!(result.0, payload.len());
|
||||
assert_eq!(result.1, src_mac);
|
||||
assert_eq!(&out_buf[..result.0], payload);
|
||||
// offset should have advanced past this frame
|
||||
assert!(offset > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_frame_two_frames() {
|
||||
let src1 = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
|
||||
let src2 = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
||||
let payload1 = b"first";
|
||||
let payload2 = b"second";
|
||||
|
||||
let mut buf = make_bpf_packet(src1, payload1);
|
||||
buf.extend_from_slice(&make_bpf_packet(src2, payload2));
|
||||
let len = buf.len();
|
||||
|
||||
let mut out_buf = vec![0u8; 1500];
|
||||
let mut offset = 0usize;
|
||||
|
||||
// First frame
|
||||
let (n1, mac1) = parse_next_frame(&buf, &mut offset, len, &mut out_buf)
|
||||
.expect("Some")
|
||||
.expect("Ok");
|
||||
assert_eq!(mac1, src1);
|
||||
assert_eq!(&out_buf[..n1], payload1);
|
||||
|
||||
// Second frame
|
||||
let (n2, mac2) = parse_next_frame(&buf, &mut offset, len, &mut out_buf)
|
||||
.expect("Some")
|
||||
.expect("Ok");
|
||||
assert_eq!(mac2, src2);
|
||||
assert_eq!(&out_buf[..n2], payload2);
|
||||
|
||||
// Buffer exhausted
|
||||
assert!(parse_next_frame(&buf, &mut offset, len, &mut out_buf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_frame_empty_buffer() {
|
||||
let buf = vec![0u8; 0];
|
||||
let mut out_buf = vec![0u8; 1500];
|
||||
let mut offset = 0usize;
|
||||
assert!(parse_next_frame(&buf, &mut offset, 0, &mut out_buf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_frame_offset_at_end() {
|
||||
let buf = vec![0u8; 64];
|
||||
let mut out_buf = vec![0u8; 1500];
|
||||
let mut offset = 64usize;
|
||||
assert!(parse_next_frame(&buf, &mut offset, 64, &mut out_buf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_frame_runt_skipped() {
|
||||
// Build a packet with cap_len < ETH_HDRLEN (13 bytes — too short for
|
||||
// a valid Ethernet header). parse_next_frame should skip it and return None.
|
||||
let hdr_size = std::mem::size_of::<BpfHeader>();
|
||||
let cap_len: usize = 13; // < ETH_HDRLEN (14)
|
||||
let hdr = BpfHeader {
|
||||
bh_tstamp_sec: 0,
|
||||
bh_tstamp_usec: 0,
|
||||
bh_caplen: cap_len as u32,
|
||||
bh_datalen: cap_len as u32,
|
||||
bh_hdrlen: hdr_size as u16,
|
||||
_pad: 0,
|
||||
};
|
||||
let total = bpf_wordalign(hdr_size + cap_len);
|
||||
let mut buf = vec![0u8; total];
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
&hdr as *const BpfHeader as *const u8,
|
||||
buf.as_mut_ptr(),
|
||||
hdr_size,
|
||||
);
|
||||
}
|
||||
|
||||
let mut out_buf = vec![0u8; 1500];
|
||||
let mut offset = 0usize;
|
||||
// Runt: returns None (skipped, not an error)
|
||||
assert!(parse_next_frame(&buf, &mut offset, buf.len(), &mut out_buf).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_next_frame_output_buf_truncation() {
|
||||
// out_buf smaller than payload — result should be truncated to out_buf size.
|
||||
let src_mac = [0xde, 0xad, 0xbe, 0xef, 0x00, 0x01];
|
||||
let payload = b"this payload is longer than the output buffer";
|
||||
let bpf_buf = make_bpf_packet(src_mac, payload);
|
||||
let len = bpf_buf.len();
|
||||
|
||||
let mut out_buf = vec![0u8; 10]; // deliberately small
|
||||
let mut offset = 0usize;
|
||||
let (n, mac) = parse_next_frame(&bpf_buf, &mut offset, len, &mut out_buf)
|
||||
.expect("Some")
|
||||
.expect("Ok");
|
||||
|
||||
assert_eq!(n, 10);
|
||||
assert_eq!(mac, src_mac);
|
||||
assert_eq!(&out_buf[..n], &payload[..10]);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Shutdown pipe signaling
|
||||
//
|
||||
// Verifies the self-pipe pattern used to wake the BPF reader thread.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// Returns true if `fd` is readable within the given timeout (0 = poll).
|
||||
fn fd_is_readable(fd: RawFd, timeout_ms: i64) -> bool {
|
||||
unsafe {
|
||||
let mut tv = libc::timeval {
|
||||
tv_sec: timeout_ms / 1000,
|
||||
tv_usec: ((timeout_ms % 1000) * 1000) as i32,
|
||||
};
|
||||
let mut read_fds: libc::fd_set = std::mem::zeroed();
|
||||
libc::FD_ZERO(&mut read_fds);
|
||||
libc::FD_SET(fd, &mut read_fds);
|
||||
let ret = libc::select(
|
||||
fd + 1,
|
||||
&mut read_fds,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut tv,
|
||||
);
|
||||
ret > 0 && libc::FD_ISSET(fd, &read_fds)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shutdown_pipe_initially_not_readable() {
|
||||
let mut fds = [0i32; 2];
|
||||
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
|
||||
let (read_fd, write_fd) = (fds[0], fds[1]);
|
||||
|
||||
let readable = fd_is_readable(read_fd, 0);
|
||||
unsafe {
|
||||
libc::close(read_fd);
|
||||
libc::close(write_fd);
|
||||
}
|
||||
assert!(!readable, "pipe read end should not be readable before write");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shutdown_pipe_readable_after_write() {
|
||||
let mut fds = [0i32; 2];
|
||||
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
|
||||
let (read_fd, write_fd) = (fds[0], fds[1]);
|
||||
|
||||
unsafe {
|
||||
libc::write(write_fd, b"x".as_ptr() as *const libc::c_void, 1);
|
||||
}
|
||||
|
||||
let readable = fd_is_readable(read_fd, 0);
|
||||
unsafe {
|
||||
libc::close(read_fd);
|
||||
libc::close(write_fd);
|
||||
}
|
||||
assert!(readable, "pipe read end should be readable after write");
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the MTU of an interface by index.
|
||||
fn get_if_mtu(if_index: i32) -> Result<u16, TransportError> {
|
||||
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
|
||||
if sock < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"socket(AF_INET) for MTU query failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
// Get interface name from index
|
||||
let mut name_buf = [0i8; libc::IFNAMSIZ];
|
||||
let ret = unsafe {
|
||||
libc::if_indextoname(if_index as libc::c_uint, name_buf.as_mut_ptr())
|
||||
};
|
||||
if ret.is_null() {
|
||||
unsafe { libc::close(sock) };
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"if_indextoname({}) failed: {}", if_index, std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
name_buf.as_ptr(),
|
||||
ifr.ifr_name.as_mut_ptr(),
|
||||
libc::IFNAMSIZ,
|
||||
);
|
||||
}
|
||||
|
||||
let ret = unsafe { libc::ioctl(sock, SIOCGIFMTU, &mut ifr) };
|
||||
unsafe { libc::close(sock) };
|
||||
if ret < 0 {
|
||||
return Err(TransportError::StartFailed(format!(
|
||||
"ioctl(SIOCGIFMTU) failed: {}",
|
||||
std::io::Error::last_os_error()
|
||||
)));
|
||||
}
|
||||
|
||||
let mtu = unsafe { ifr.ifr_ifru.ifru_mtu } as u16;
|
||||
Ok(mtu)
|
||||
}
|
||||
+1
-22
@@ -8,9 +8,9 @@ pub mod udp;
|
||||
pub mod tcp;
|
||||
pub mod tor;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ethernet;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ble;
|
||||
|
||||
use secp256k1::XOnlyPublicKey;
|
||||
@@ -18,7 +18,6 @@ use udp::UdpTransport;
|
||||
use tcp::TcpTransport;
|
||||
use tor::control::TorMonitoringInfo;
|
||||
use tor::TorTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use ethernet::EthernetTransport;
|
||||
#[cfg(target_os = "linux")]
|
||||
use ble::DefaultBleTransport;
|
||||
@@ -852,7 +851,6 @@ pub enum TransportHandle {
|
||||
/// UDP/IP transport.
|
||||
Udp(UdpTransport),
|
||||
/// Raw Ethernet transport.
|
||||
#[cfg(target_os = "linux")]
|
||||
Ethernet(EthernetTransport),
|
||||
/// TCP/IP transport.
|
||||
Tcp(TcpTransport),
|
||||
@@ -868,7 +866,6 @@ impl TransportHandle {
|
||||
pub async fn start(&mut self) -> Result<(), TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.start_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.start_async().await,
|
||||
TransportHandle::Tcp(t) => t.start_async().await,
|
||||
TransportHandle::Tor(t) => t.start_async().await,
|
||||
@@ -881,7 +878,6 @@ impl TransportHandle {
|
||||
pub async fn stop(&mut self) -> Result<(), TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.stop_async().await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.stop_async().await,
|
||||
TransportHandle::Tcp(t) => t.stop_async().await,
|
||||
TransportHandle::Tor(t) => t.stop_async().await,
|
||||
@@ -894,7 +890,6 @@ impl TransportHandle {
|
||||
pub async fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<usize, TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.send_async(addr, data).await,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Tcp(t) => t.send_async(addr, data).await,
|
||||
TransportHandle::Tor(t) => t.send_async(addr, data).await,
|
||||
@@ -907,7 +902,6 @@ impl TransportHandle {
|
||||
pub fn transport_id(&self) -> TransportId {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.transport_id(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.transport_id(),
|
||||
TransportHandle::Tcp(t) => t.transport_id(),
|
||||
TransportHandle::Tor(t) => t.transport_id(),
|
||||
@@ -920,7 +914,6 @@ impl TransportHandle {
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.name(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.name(),
|
||||
TransportHandle::Tcp(t) => t.name(),
|
||||
TransportHandle::Tor(t) => t.name(),
|
||||
@@ -933,7 +926,6 @@ impl TransportHandle {
|
||||
pub fn transport_type(&self) -> &TransportType {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.transport_type(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.transport_type(),
|
||||
TransportHandle::Tcp(t) => t.transport_type(),
|
||||
TransportHandle::Tor(t) => t.transport_type(),
|
||||
@@ -946,7 +938,6 @@ impl TransportHandle {
|
||||
pub fn state(&self) -> TransportState {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.state(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.state(),
|
||||
TransportHandle::Tcp(t) => t.state(),
|
||||
TransportHandle::Tor(t) => t.state(),
|
||||
@@ -959,7 +950,6 @@ impl TransportHandle {
|
||||
pub fn mtu(&self) -> u16 {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.mtu(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.mtu(),
|
||||
TransportHandle::Tcp(t) => t.mtu(),
|
||||
TransportHandle::Tor(t) => t.mtu(),
|
||||
@@ -975,7 +965,6 @@ impl TransportHandle {
|
||||
pub fn link_mtu(&self, addr: &TransportAddr) -> u16 {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.link_mtu(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.link_mtu(addr),
|
||||
TransportHandle::Tcp(t) => t.link_mtu(addr),
|
||||
TransportHandle::Tor(t) => t.link_mtu(addr),
|
||||
@@ -988,7 +977,6 @@ impl TransportHandle {
|
||||
pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.local_addr(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(_) => None,
|
||||
TransportHandle::Tcp(t) => t.local_addr(),
|
||||
TransportHandle::Tor(_) => None,
|
||||
@@ -1001,7 +989,6 @@ impl TransportHandle {
|
||||
pub fn interface_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
TransportHandle::Udp(_) => None,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => Some(t.interface_name()),
|
||||
TransportHandle::Tcp(_) => None,
|
||||
TransportHandle::Tor(_) => None,
|
||||
@@ -1038,7 +1025,6 @@ impl TransportHandle {
|
||||
pub fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.discover(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.discover(),
|
||||
TransportHandle::Tcp(t) => t.discover(),
|
||||
TransportHandle::Tor(t) => t.discover(),
|
||||
@@ -1051,7 +1037,6 @@ impl TransportHandle {
|
||||
pub fn auto_connect(&self) -> bool {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.auto_connect(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.auto_connect(),
|
||||
TransportHandle::Tcp(t) => t.auto_connect(),
|
||||
TransportHandle::Tor(t) => t.auto_connect(),
|
||||
@@ -1064,7 +1049,6 @@ impl TransportHandle {
|
||||
pub fn accept_connections(&self) -> bool {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.accept_connections(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.accept_connections(),
|
||||
TransportHandle::Tcp(t) => t.accept_connections(),
|
||||
TransportHandle::Tor(t) => t.accept_connections(),
|
||||
@@ -1083,7 +1067,6 @@ impl TransportHandle {
|
||||
pub async fn connect(&self, addr: &TransportAddr) -> Result<(), TransportError> {
|
||||
match self {
|
||||
TransportHandle::Udp(_) => Ok(()), // connectionless
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(_) => Ok(()), // connectionless
|
||||
TransportHandle::Tcp(t) => t.connect_async(addr).await,
|
||||
TransportHandle::Tor(t) => t.connect_async(addr).await,
|
||||
@@ -1100,7 +1083,6 @@ impl TransportHandle {
|
||||
pub fn connection_state(&self, addr: &TransportAddr) -> ConnectionState {
|
||||
match self {
|
||||
TransportHandle::Udp(_) => ConnectionState::Connected,
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(_) => ConnectionState::Connected,
|
||||
TransportHandle::Tcp(t) => t.connection_state_sync(addr),
|
||||
TransportHandle::Tor(t) => t.connection_state_sync(addr),
|
||||
@@ -1116,7 +1098,6 @@ impl TransportHandle {
|
||||
pub async fn close_connection(&self, addr: &TransportAddr) {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.close_connection(addr),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => t.close_connection(addr),
|
||||
TransportHandle::Tcp(t) => t.close_connection_async(addr).await,
|
||||
TransportHandle::Tor(t) => t.close_connection_async(addr).await,
|
||||
@@ -1138,7 +1119,6 @@ impl TransportHandle {
|
||||
pub fn congestion(&self) -> TransportCongestion {
|
||||
match self {
|
||||
TransportHandle::Udp(t) => t.congestion(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(_) => TransportCongestion::default(),
|
||||
TransportHandle::Tcp(_) => TransportCongestion::default(),
|
||||
TransportHandle::Tor(_) => TransportCongestion::default(),
|
||||
@@ -1155,7 +1135,6 @@ impl TransportHandle {
|
||||
TransportHandle::Udp(t) => {
|
||||
serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
TransportHandle::Ethernet(t) => {
|
||||
let snap = t.stats().snapshot();
|
||||
serde_json::json!({
|
||||
|
||||
@@ -152,7 +152,10 @@ impl UdpRawSocket {
|
||||
|
||||
// Control message buffer sized for SO_RXQ_OVFL (u32).
|
||||
// CMSG_SPACE computes the aligned size including header.
|
||||
#[cfg(target_os = "linux")]
|
||||
const CMSG_BUF_SIZE: usize = unsafe { libc::CMSG_SPACE(4) } as usize;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const CMSG_BUF_SIZE: usize = 64;
|
||||
let mut cmsg_buf = [0u8; CMSG_BUF_SIZE];
|
||||
|
||||
let mut src_addr: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
|
||||
@@ -160,7 +163,7 @@ impl UdpRawSocket {
|
||||
msg.msg_name = &mut src_addr as *mut _ as *mut libc::c_void;
|
||||
msg.msg_namelen = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||
msg.msg_iov = &mut iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_iovlen = 1 as _;
|
||||
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
|
||||
msg.msg_controllen = cmsg_buf.len() as _;
|
||||
|
||||
@@ -173,7 +176,10 @@ impl UdpRawSocket {
|
||||
let addr = sockaddr_to_socket_addr(&src_addr)?;
|
||||
|
||||
// Walk cmsg chain for SO_RXQ_OVFL drop counter
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut drops: u32 = 0;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let drops: u32 = 0;
|
||||
#[cfg(target_os = "linux")]
|
||||
unsafe {
|
||||
let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
|
||||
|
||||
+407
-156
@@ -5,10 +5,10 @@
|
||||
//! allowing standard socket applications to communicate over the mesh.
|
||||
|
||||
use crate::{FipsAddress, TunConfig};
|
||||
use futures::TryStreamExt;
|
||||
use rtnetlink::{new_connection, Handle, LinkUnspec, RouteMessageBuilder};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Read;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use std::io::Write;
|
||||
use std::net::Ipv6Addr;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||
use std::sync::mpsc;
|
||||
@@ -33,6 +33,7 @@ pub enum TunError {
|
||||
#[error("failed to configure TUN device: {0}")]
|
||||
Configure(String),
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error("netlink error: {0}")]
|
||||
Netlink(#[from] rtnetlink::Error),
|
||||
|
||||
@@ -87,7 +88,7 @@ impl TunDevice {
|
||||
/// This requires CAP_NET_ADMIN capability (run with sudo or setcap).
|
||||
pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
|
||||
// Check if IPv6 is enabled
|
||||
if is_ipv6_disabled() {
|
||||
if platform::is_ipv6_disabled() {
|
||||
return Err(TunError::Ipv6Disabled);
|
||||
}
|
||||
|
||||
@@ -95,9 +96,9 @@ impl TunDevice {
|
||||
let mtu = config.mtu();
|
||||
|
||||
// Delete existing interface if present (TUN devices are exclusive)
|
||||
if interface_exists(name).await {
|
||||
if platform::interface_exists(name).await {
|
||||
debug!(name, "Deleting existing TUN interface");
|
||||
if let Err(e) = delete_interface(name).await {
|
||||
if let Err(e) = platform::delete_interface(name).await {
|
||||
debug!(name, error = %e, "Failed to delete existing interface");
|
||||
}
|
||||
}
|
||||
@@ -105,17 +106,32 @@ impl TunDevice {
|
||||
// Create the TUN device
|
||||
let mut tun_config = tun::Configuration::default();
|
||||
|
||||
// On macOS, utun devices get kernel-assigned names (utun0, utun1, ...),
|
||||
// so we skip setting the name and read it back after creation.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[allow(deprecated)]
|
||||
tun_config.name(name).layer(Layer::L3).mtu(mtu);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
tun_config.layer(Layer::L3).mtu(mtu);
|
||||
}
|
||||
|
||||
let device = tun::create(&tun_config)?;
|
||||
|
||||
// Configure address and bring up via netlink
|
||||
configure_interface(name, address.to_ipv6(), mtu).await?;
|
||||
// Read the actual device name (on macOS this is the kernel-assigned utun* name)
|
||||
let actual_name = {
|
||||
use tun::AbstractDevice;
|
||||
device.tun_name().map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
|
||||
};
|
||||
|
||||
// Configure address and bring up via platform-specific method
|
||||
platform::configure_interface(&actual_name, address.to_ipv6(), mtu).await?;
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
name: name.to_string(),
|
||||
name: actual_name,
|
||||
mtu,
|
||||
address,
|
||||
})
|
||||
@@ -148,10 +164,17 @@ impl TunDevice {
|
||||
|
||||
/// Read a packet from the TUN device.
|
||||
///
|
||||
/// Returns the number of bytes read into the buffer, or an error.
|
||||
/// Returns the number of bytes read into the buffer, or an `io::Error`.
|
||||
/// The buffer should be at least MTU + header size (typically 1500+ bytes).
|
||||
pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, TunError> {
|
||||
self.device.read(buf).map_err(|e| TunError::Configure(format!("read failed: {}", e)))
|
||||
///
|
||||
/// The tun crate's `Read` impl transparently strips the macOS utun
|
||||
/// packet information header, so this returns a raw IP packet on all
|
||||
/// platforms.
|
||||
///
|
||||
/// The raw `io::Error` is returned so callers can inspect `ErrorKind`
|
||||
/// (e.g. `WouldBlock`) or `raw_os_error()` without string matching.
|
||||
pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
self.device.read(buf)
|
||||
}
|
||||
|
||||
/// Shutdown and delete the TUN device.
|
||||
@@ -159,7 +182,7 @@ impl TunDevice {
|
||||
/// This deletes the interface entirely.
|
||||
pub async fn shutdown(&self) -> Result<(), TunError> {
|
||||
debug!(name = %self.name, "Deleting TUN device");
|
||||
delete_interface(&self.name).await
|
||||
platform::delete_interface(&self.name).await
|
||||
}
|
||||
|
||||
/// Create a TunWriter for this device.
|
||||
@@ -214,6 +237,7 @@ impl TunWriter {
|
||||
///
|
||||
/// Blocks forever, reading packets from the channel and writing them
|
||||
/// to the TUN device. Returns when the channel is closed (all senders dropped).
|
||||
#[cfg_attr(target_os = "macos", allow(unused_mut))]
|
||||
pub fn run(mut self) {
|
||||
use super::tcp_mss::clamp_tcp_mss;
|
||||
|
||||
@@ -229,7 +253,43 @@ impl TunWriter {
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = self.file.write_all(&packet) {
|
||||
// On macOS, utun devices require a 4-byte packet information header
|
||||
// prepended to each packet. The tun crate handles this for its own
|
||||
// Read/Write impl, but we use a dup'd fd directly. We use writev
|
||||
// to avoid allocating a buffer on every packet.
|
||||
#[cfg(target_os = "macos")]
|
||||
let write_result = {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
const AF_INET6_HEADER: [u8; 4] = [0, 0, 0, 30];
|
||||
let iov = [
|
||||
libc::iovec {
|
||||
iov_base: AF_INET6_HEADER.as_ptr() as *mut libc::c_void,
|
||||
iov_len: 4,
|
||||
},
|
||||
libc::iovec {
|
||||
iov_base: packet.as_ptr() as *mut libc::c_void,
|
||||
iov_len: packet.len(),
|
||||
},
|
||||
];
|
||||
let ret = unsafe { libc::writev(self.file.as_raw_fd(), iov.as_ptr(), 2) };
|
||||
if ret < 0 {
|
||||
Err(std::io::Error::last_os_error())
|
||||
} else {
|
||||
let expected = 4 + packet.len();
|
||||
if (ret as usize) < expected {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::WriteZero,
|
||||
format!("short writev: {} of {} bytes", ret, expected),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let write_result = self.file.write_all(&packet);
|
||||
|
||||
if let Err(e) = write_result {
|
||||
// "Bad address" is expected during shutdown when interface is deleted
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("Bad address") {
|
||||
@@ -243,7 +303,7 @@ impl TunWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// TUN packet reader loop.
|
||||
/// TUN packet reader loop (Linux).
|
||||
///
|
||||
/// Reads IPv6 packets from the TUN device. Packets destined for FIPS addresses
|
||||
/// (fd::/8) are forwarded to the Node via the outbound channel for session
|
||||
@@ -255,6 +315,7 @@ impl TunWriter {
|
||||
/// This is designed to run in a dedicated thread since TUN reads are blocking.
|
||||
/// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable
|
||||
/// error occurs.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn run_tun_reader(
|
||||
mut device: TunDevice,
|
||||
mtu: u16,
|
||||
@@ -263,13 +324,128 @@ pub fn run_tun_reader(
|
||||
outbound_tx: TunOutboundTx,
|
||||
transport_mtu: u16,
|
||||
) {
|
||||
use super::icmp::{build_dest_unreachable, effective_ipv6_mtu, should_send_icmp_error, DestUnreachableCode};
|
||||
use super::tcp_mss::clamp_tcp_mss;
|
||||
let (name, mut buf, max_mss) = tun_reader_setup(&device, mtu, transport_mtu);
|
||||
|
||||
loop {
|
||||
match device.read_packet(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
if !handle_tun_packet(&mut buf[..n], max_mss, &name, our_addr, &tun_tx, &outbound_tx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
// EFAULT ("Bad address") is expected during shutdown when the interface is deleted
|
||||
if e.raw_os_error() != Some(libc::EFAULT) {
|
||||
error!(name = %name, error = %e, "TUN read error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII wrapper that closes a raw fd on drop.
|
||||
///
|
||||
/// Used to ensure the shutdown pipe read-end is always closed when
|
||||
/// `run_tun_reader` returns, regardless of which exit path is taken.
|
||||
#[cfg(target_os = "macos")]
|
||||
struct ShutdownFd(std::os::unix::io::RawFd);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for ShutdownFd {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::close(self.0); }
|
||||
}
|
||||
}
|
||||
|
||||
/// TUN packet reader loop (macOS).
|
||||
///
|
||||
/// Uses `select()` to multiplex between the TUN fd and a shutdown pipe,
|
||||
/// avoiding the need to close the TUN fd externally (which would cause a
|
||||
/// double-close when `TunDevice` drops).
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn run_tun_reader(
|
||||
mut device: TunDevice,
|
||||
mtu: u16,
|
||||
our_addr: FipsAddress,
|
||||
tun_tx: TunTx,
|
||||
outbound_tx: TunOutboundTx,
|
||||
transport_mtu: u16,
|
||||
shutdown_fd: std::os::unix::io::RawFd,
|
||||
) {
|
||||
let _shutdown_fd = ShutdownFd(shutdown_fd);
|
||||
let tun_fd = device.device().as_raw_fd();
|
||||
let (name, mut buf, max_mss) = tun_reader_setup(&device, mtu, transport_mtu);
|
||||
|
||||
// Set TUN fd to non-blocking so we can use select + read without blocking
|
||||
// past the point where select returns readable.
|
||||
unsafe {
|
||||
let flags = libc::fcntl(tun_fd, libc::F_GETFL);
|
||||
if flags >= 0 {
|
||||
libc::fcntl(tun_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
||||
}
|
||||
}
|
||||
|
||||
let nfds = tun_fd.max(shutdown_fd) + 1;
|
||||
|
||||
loop {
|
||||
// Wait for either TUN data or shutdown signal
|
||||
unsafe {
|
||||
let mut read_fds: libc::fd_set = std::mem::zeroed();
|
||||
libc::FD_ZERO(&mut read_fds);
|
||||
libc::FD_SET(tun_fd, &mut read_fds);
|
||||
libc::FD_SET(shutdown_fd, &mut read_fds);
|
||||
|
||||
let ret = libc::select(nfds, &mut read_fds, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut());
|
||||
if ret < 0 {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() == std::io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
error!(name = %name, error = %err, "TUN select error");
|
||||
break;
|
||||
}
|
||||
|
||||
// Shutdown signal received
|
||||
if libc::FD_ISSET(shutdown_fd, &read_fds) {
|
||||
debug!(name = %name, "TUN reader received shutdown signal");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TUN fd is readable — drain all available packets
|
||||
loop {
|
||||
match device.read_packet(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
if !handle_tun_packet(&mut buf[..n], max_mss, &name, our_addr, &tun_tx, &outbound_tx) {
|
||||
return; // _shutdown_fd closes on drop
|
||||
}
|
||||
}
|
||||
Ok(_) => break, // No more data
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock {
|
||||
break; // Done for this select round
|
||||
}
|
||||
// EBADF is expected during shutdown when the fd is closed
|
||||
if e.raw_os_error() != Some(libc::EBADF) {
|
||||
error!(name = %name, error = %e, "TUN read error");
|
||||
}
|
||||
return; // _shutdown_fd closes on drop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// _shutdown_fd closes on drop
|
||||
}
|
||||
|
||||
/// Common setup for TUN reader: extracts name, allocates buffer, computes max MSS.
|
||||
fn tun_reader_setup(device: &TunDevice, mtu: u16, transport_mtu: u16) -> (String, Vec<u8>, u16) {
|
||||
use super::icmp::effective_ipv6_mtu;
|
||||
|
||||
let name = device.name().to_string();
|
||||
let mut buf = vec![0u8; mtu as usize + 100]; // Extra space for headers
|
||||
let buf = vec![0u8; mtu as usize + 100];
|
||||
|
||||
// Calculate maximum safe TCP MSS from the effective IPv6 MTU
|
||||
const IPV6_HEADER: u16 = 40;
|
||||
const TCP_HEADER: u16 = 20;
|
||||
let effective_mtu = effective_ipv6_mtu(transport_mtu);
|
||||
@@ -286,65 +462,52 @@ pub fn run_tun_reader(
|
||||
"TUN reader starting"
|
||||
);
|
||||
|
||||
loop {
|
||||
match device.read_packet(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
let packet = &mut buf[..n];
|
||||
log_ipv6_packet(packet);
|
||||
(name, buf, max_mss)
|
||||
}
|
||||
|
||||
// Must be a valid IPv6 packet
|
||||
if packet.len() < 40 || packet[0] >> 4 != 6 {
|
||||
continue;
|
||||
}
|
||||
/// Process a single TUN packet. Returns `false` if the reader should exit.
|
||||
fn handle_tun_packet(
|
||||
packet: &mut [u8],
|
||||
max_mss: u16,
|
||||
name: &str,
|
||||
our_addr: FipsAddress,
|
||||
tun_tx: &TunTx,
|
||||
outbound_tx: &TunOutboundTx,
|
||||
) -> bool {
|
||||
use super::icmp::{build_dest_unreachable, should_send_icmp_error, DestUnreachableCode};
|
||||
use super::tcp_mss::clamp_tcp_mss;
|
||||
|
||||
// Check if destination is a FIPS address (fd::/8 prefix)
|
||||
if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
|
||||
// Clamp TCP MSS if this is a SYN packet
|
||||
if clamp_tcp_mss(packet, max_mss) {
|
||||
trace!(
|
||||
name = %name,
|
||||
max_mss = max_mss,
|
||||
"Clamped TCP MSS in SYN packet"
|
||||
);
|
||||
}
|
||||
log_ipv6_packet(packet);
|
||||
|
||||
// Forward to Node for session encapsulation and routing
|
||||
if outbound_tx.blocking_send(packet.to_vec()).is_err() {
|
||||
break; // Channel closed, shutdown
|
||||
}
|
||||
} else {
|
||||
// Non-FIPS destination: send ICMPv6 Destination Unreachable
|
||||
if should_send_icmp_error(packet)
|
||||
&& let Some(response) = build_dest_unreachable(
|
||||
packet,
|
||||
DestUnreachableCode::NoRoute,
|
||||
our_addr.to_ipv6(),
|
||||
)
|
||||
{
|
||||
trace!(
|
||||
name = %name,
|
||||
len = response.len(),
|
||||
"Sending ICMPv6 Destination Unreachable (non-FIPS destination)"
|
||||
);
|
||||
if tun_tx.send(response).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// Zero-length read, continue
|
||||
}
|
||||
Err(e) => {
|
||||
// "Bad address" (EFAULT) is expected during shutdown when interface is deleted
|
||||
let err_str = format!("{}", e);
|
||||
if !err_str.contains("Bad address") {
|
||||
error!(name = %name, error = %e, "TUN read error");
|
||||
}
|
||||
break;
|
||||
// Must be a valid IPv6 packet
|
||||
if packet.len() < 40 || packet[0] >> 4 != 6 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if destination is a FIPS address (fd::/8 prefix)
|
||||
if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
|
||||
if clamp_tcp_mss(packet, max_mss) {
|
||||
trace!(name = %name, max_mss = max_mss, "Clamped TCP MSS in SYN packet");
|
||||
}
|
||||
if outbound_tx.blocking_send(packet.to_vec()).is_err() {
|
||||
return false; // Channel closed, shutdown
|
||||
}
|
||||
} else {
|
||||
// Non-FIPS destination: send ICMPv6 Destination Unreachable
|
||||
if should_send_icmp_error(packet)
|
||||
&& let Some(response) = build_dest_unreachable(
|
||||
packet,
|
||||
DestUnreachableCode::NoRoute,
|
||||
our_addr.to_ipv6(),
|
||||
)
|
||||
{
|
||||
trace!(name = %name, len = response.len(), "Sending ICMPv6 Destination Unreachable (non-FIPS destination)");
|
||||
if tun_tx.send(response).is_err() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Log basic information about an IPv6 packet at TRACE level.
|
||||
@@ -388,7 +551,7 @@ pub fn log_ipv6_packet(packet: &[u8]) {
|
||||
/// has been moved to another thread.
|
||||
pub async fn shutdown_tun_interface(name: &str) -> Result<(), TunError> {
|
||||
debug!("Shutting down TUN interface {}", name);
|
||||
delete_interface(name).await?;
|
||||
platform::delete_interface(name).await?;
|
||||
debug!("TUN interface {} stopped", name);
|
||||
Ok(())
|
||||
}
|
||||
@@ -403,98 +566,186 @@ impl std::fmt::Debug for TunDevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a network interface already exists.
|
||||
async fn interface_exists(name: &str) -> bool {
|
||||
let Ok((connection, handle, _)) = new_connection() else {
|
||||
return false;
|
||||
};
|
||||
tokio::spawn(connection);
|
||||
// =============================================================================
|
||||
// Platform-specific TUN configuration
|
||||
// =============================================================================
|
||||
|
||||
get_interface_index(&handle, name).await.is_ok()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
mod platform {
|
||||
use super::TunError;
|
||||
use futures::TryStreamExt;
|
||||
use rtnetlink::{new_connection, Handle, LinkUnspec, RouteMessageBuilder};
|
||||
use std::net::Ipv6Addr;
|
||||
use tracing::debug;
|
||||
|
||||
/// Delete a network interface by name.
|
||||
async fn delete_interface(name: &str) -> Result<(), TunError> {
|
||||
let (connection, handle, _) = new_connection()
|
||||
.map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let index = get_interface_index(&handle, name).await?;
|
||||
handle.link().del(index).execute().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure a network interface with an IPv6 address via netlink.
|
||||
async fn configure_interface(name: &str, addr: Ipv6Addr, mtu: u16) -> Result<(), TunError> {
|
||||
let (connection, handle, _) = new_connection()
|
||||
.map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
// Get interface index
|
||||
let index = get_interface_index(&handle, name).await?;
|
||||
|
||||
// Add IPv6 address with /128 prefix (point-to-point)
|
||||
handle
|
||||
.address()
|
||||
.add(index, std::net::IpAddr::V6(addr), 128)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Set MTU
|
||||
handle
|
||||
.link()
|
||||
.change(LinkUnspec::new_with_index(index).mtu(mtu as u32).build())
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Bring interface up
|
||||
handle
|
||||
.link()
|
||||
.change(LinkUnspec::new_with_index(index).up().build())
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Add route for fd00::/8 (FIPS address space) via this interface
|
||||
let fd_prefix: Ipv6Addr = "fd00::".parse().unwrap();
|
||||
let route = RouteMessageBuilder::<Ipv6Addr>::new()
|
||||
.destination_prefix(fd_prefix, 8)
|
||||
.output_interface(index)
|
||||
.build();
|
||||
handle
|
||||
.route()
|
||||
.add(route)
|
||||
.execute()
|
||||
.await
|
||||
.map_err(|e| TunError::Configure(format!("failed to add fd00::/8 route: {}", e)))?;
|
||||
|
||||
// Add ip6 rule to ensure fd00::/8 uses the main table, preventing other
|
||||
// routing software (e.g. Tailscale) from intercepting FIPS traffic via
|
||||
// catch-all rules in auxiliary routing tables.
|
||||
let mut rule_req = handle.rule().add().v6().destination_prefix(fd_prefix, 8).table_id(254).priority(5265);
|
||||
rule_req.message_mut().header.action = 1.into(); // FR_ACT_TO_TBL
|
||||
if let Err(e) = rule_req.execute().await {
|
||||
debug!("ip6 rule for fd00::/8 not added (may already exist): {e}");
|
||||
/// Check if IPv6 is disabled system-wide.
|
||||
pub fn is_ipv6_disabled() -> bool {
|
||||
std::fs::read_to_string("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
.map(|s| s.trim() == "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Check if a network interface already exists.
|
||||
pub async fn interface_exists(name: &str) -> bool {
|
||||
let Ok((connection, handle, _)) = new_connection() else {
|
||||
return false;
|
||||
};
|
||||
tokio::spawn(connection);
|
||||
|
||||
/// Get the interface index by name.
|
||||
async fn get_interface_index(handle: &Handle, name: &str) -> Result<u32, TunError> {
|
||||
let mut links = handle.link().get().match_name(name.to_string()).execute();
|
||||
get_interface_index(&handle, name).await.is_ok()
|
||||
}
|
||||
|
||||
if let Some(link) = links.try_next().await? {
|
||||
Ok(link.header.index)
|
||||
} else {
|
||||
Err(TunError::InterfaceNotFound(name.to_string()))
|
||||
/// Delete a network interface by name.
|
||||
pub async fn delete_interface(name: &str) -> Result<(), TunError> {
|
||||
let (connection, handle, _) = new_connection()
|
||||
.map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
let index = get_interface_index(&handle, name).await?;
|
||||
handle.link().del(index).execute().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure a network interface with an IPv6 address via netlink.
|
||||
pub async fn configure_interface(name: &str, addr: Ipv6Addr, mtu: u16) -> Result<(), TunError> {
|
||||
let (connection, handle, _) = new_connection()
|
||||
.map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?;
|
||||
tokio::spawn(connection);
|
||||
|
||||
// Get interface index
|
||||
let index = get_interface_index(&handle, name).await?;
|
||||
|
||||
// Add IPv6 address with /128 prefix (point-to-point)
|
||||
handle
|
||||
.address()
|
||||
.add(index, std::net::IpAddr::V6(addr), 128)
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Set MTU
|
||||
handle
|
||||
.link()
|
||||
.change(LinkUnspec::new_with_index(index).mtu(mtu as u32).build())
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Bring interface up
|
||||
handle
|
||||
.link()
|
||||
.change(LinkUnspec::new_with_index(index).up().build())
|
||||
.execute()
|
||||
.await?;
|
||||
|
||||
// Add route for fd00::/8 (FIPS address space) via this interface
|
||||
let fd_prefix: Ipv6Addr = "fd00::".parse().unwrap();
|
||||
let route = RouteMessageBuilder::<Ipv6Addr>::new()
|
||||
.destination_prefix(fd_prefix, 8)
|
||||
.output_interface(index)
|
||||
.build();
|
||||
handle
|
||||
.route()
|
||||
.add(route)
|
||||
.execute()
|
||||
.await
|
||||
.map_err(|e| TunError::Configure(format!("failed to add fd00::/8 route: {}", e)))?;
|
||||
|
||||
// Add ip6 rule to ensure fd00::/8 uses the main table, preventing other
|
||||
// routing software (e.g. Tailscale) from intercepting FIPS traffic via
|
||||
// catch-all rules in auxiliary routing tables.
|
||||
let mut rule_req = handle.rule().add().v6().destination_prefix(fd_prefix, 8).table_id(254).priority(5265);
|
||||
rule_req.message_mut().header.action = 1.into(); // FR_ACT_TO_TBL
|
||||
if let Err(e) = rule_req.execute().await {
|
||||
debug!("ip6 rule for fd00::/8 not added (may already exist): {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the interface index by name.
|
||||
async fn get_interface_index(handle: &Handle, name: &str) -> Result<u32, TunError> {
|
||||
let mut links = handle.link().get().match_name(name.to_string()).execute();
|
||||
|
||||
if let Some(link) = links.try_next().await? {
|
||||
Ok(link.header.index)
|
||||
} else {
|
||||
Err(TunError::InterfaceNotFound(name.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if IPv6 is disabled system-wide.
|
||||
fn is_ipv6_disabled() -> bool {
|
||||
std::fs::read_to_string("/proc/sys/net/ipv6/conf/all/disable_ipv6")
|
||||
.map(|s| s.trim() == "1")
|
||||
.unwrap_or(false)
|
||||
#[cfg(target_os = "macos")]
|
||||
mod platform {
|
||||
use super::TunError;
|
||||
use std::net::Ipv6Addr;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// Check if IPv6 is disabled system-wide.
|
||||
pub fn is_ipv6_disabled() -> bool {
|
||||
// macOS: check via sysctl; if the key doesn't exist, IPv6 is enabled
|
||||
std::process::Command::new("sysctl")
|
||||
.args(["-n", "net.inet6.ip6.disabled"])
|
||||
.output()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim() == "1")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if a network interface already exists.
|
||||
pub async fn interface_exists(name: &str) -> bool {
|
||||
Command::new("ifconfig")
|
||||
.arg(name)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Shut down a network interface by name.
|
||||
///
|
||||
/// On macOS, utun devices are automatically destroyed when the file
|
||||
/// descriptor is closed. Bringing the interface down causes any
|
||||
/// blocking reads to return an error, which unblocks the reader thread.
|
||||
pub async fn delete_interface(name: &str) -> Result<(), TunError> {
|
||||
run_cmd("ifconfig", &[name, "down"]).await
|
||||
}
|
||||
|
||||
/// Configure a network interface with an IPv6 address using ifconfig/route.
|
||||
pub async fn configure_interface(name: &str, addr: Ipv6Addr, mtu: u16) -> Result<(), TunError> {
|
||||
// Add IPv6 address with /128 prefix
|
||||
run_cmd("ifconfig", &[name, "inet6", &addr.to_string(), "prefixlen", "128"]).await?;
|
||||
|
||||
// Set MTU
|
||||
run_cmd("ifconfig", &[name, "mtu", &mtu.to_string()]).await?;
|
||||
|
||||
// Bring interface up
|
||||
run_cmd("ifconfig", &[name, "up"]).await?;
|
||||
|
||||
// Add route for fd00::/8 (FIPS address space) via this interface
|
||||
run_cmd("route", &["add", "-inet6", "-prefixlen", "8", "fd00::", "-interface", name]).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a command and return an error if it fails.
|
||||
async fn run_cmd(program: &str, args: &[&str]) -> Result<(), TunError> {
|
||||
let output = Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| TunError::Configure(format!("{} failed: {}", program, e)))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(TunError::Configure(format!(
|
||||
"{} {} failed: {}",
|
||||
program,
|
||||
args.join(" "),
|
||||
stderr.trim()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user