feat(platform): introduce abstractions for cross-platform interface statistics and traffic control

Decouple the rate controller from Linux-specific Netlink calls to support other operating systems. This adds a macOS backend for interface statistics and an "observe-only" mode for platforms where traffic shaping is not yet implemented.
This commit is contained in:
Nils Andreas Svee committed 2026-07-17 11:35:43 +07:00
1 parent c5d673fcb8
commit 9811592f13
10 files changed
+570 -102

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
//
// SPDX-License-Identifier: MPL-2.0
use std::ffi::{CString, NulError};
use std::io;
use std::mem::size_of;
use std::ptr;
use thiserror::Error;
use super::{InterfaceStats, InterfaceStatsProvider};
const ROUTE_MESSAGE_PREFIX_LEN: usize = size_of::<libc::c_ushort>() + 2;
const SYSCTL_FETCH_ATTEMPTS: usize = 3;
#[derive(Debug, Error)]
pub enum MacOsInterfaceStatsError {
#[error("interface name contains a NUL byte")]
InterfaceNameContainsNul(#[source] NulError),
#[error("interface `{interface}` was not found")]
InterfaceNotFound {
interface: String,
#[source]
source: io::Error,
},
#[error("sysctl failed while {operation}")]
Sysctl {
operation: &'static str,
#[source]
source: io::Error,
},
#[error("malformed route-message response: {0}")]
MalformedResponse(String),
#[error("no RTM_IFINFO2 statistics returned for interface index {interface_index}")]
NoInterfaceStats { interface_index: libc::c_uint },
}
#[derive(Debug, Default)]
pub struct MacOsInterfaceStats;
impl InterfaceStatsProvider for MacOsInterfaceStats {
type Error = MacOsInterfaceStatsError;
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
let interface_name =
CString::new(interface).map_err(MacOsInterfaceStatsError::InterfaceNameContainsNul)?;
// SAFETY: `interface_name` is a live, NUL-terminated C string for the duration of the call.
let interface_index = unsafe { libc::if_nametoindex(interface_name.as_ptr()) };
if interface_index == 0 {
return Err(MacOsInterfaceStatsError::InterfaceNotFound {
interface: interface.to_string(),
source: io::Error::last_os_error(),
});
}
let response = route_interface_list(interface_index)?;
parse_route_messages(&response, interface_index)
}
}
fn route_interface_list(
interface_index: libc::c_uint,
) -> Result<Vec<u8>, MacOsInterfaceStatsError> {
let mut mib = [
libc::CTL_NET,
libc::PF_ROUTE,
0,
0,
libc::NET_RT_IFLIST2,
interface_index as libc::c_int,
];
for attempt in 0..SYSCTL_FETCH_ATTEMPTS {
let mut required_len = 0;
// SAFETY: `mib` has the declared six elements, `required_len` is writable, and the
// null data pointers request only the response size without reading or writing data.
let size_result = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as libc::c_uint,
ptr::null_mut(),
&mut required_len,
ptr::null_mut(),
0,
)
};
if size_result != 0 {
return Err(MacOsInterfaceStatsError::Sysctl {
operation: "querying the interface-list buffer size",
source: io::Error::last_os_error(),
});
}
let mut response = vec![0_u8; required_len];
let mut returned_len = required_len;
// SAFETY: `response` owns `required_len` initialized bytes, `returned_len` describes
// that allocation, and all other pointers remain valid for the duration of the call.
let fetch_result = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as libc::c_uint,
response.as_mut_ptr().cast(),
&mut returned_len,
ptr::null_mut(),
0,
)
};
if fetch_result == 0 {
if returned_len > response.len() {
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
"sysctl returned length {returned_len} for a {}-byte buffer",
response.len()
)));
}
response.truncate(returned_len);
return Ok(response);
}
let source = io::Error::last_os_error();
let buffer_too_small = matches!(source.raw_os_error(), Some(libc::ENOMEM | libc::ENOBUFS));
if !buffer_too_small || attempt + 1 == SYSCTL_FETCH_ATTEMPTS {
return Err(MacOsInterfaceStatsError::Sysctl {
operation: "fetching the interface list",
source,
});
}
}
unreachable!("the bounded sysctl retry loop always returns")
}
fn parse_route_messages(
response: &[u8],
requested_interface_index: libc::c_uint,
) -> Result<InterfaceStats, MacOsInterfaceStatsError> {
let mut offset = 0;
while offset < response.len() {
let remaining = response.len() - offset;
if remaining < ROUTE_MESSAGE_PREFIX_LEN {
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
"truncated route-message prefix at offset {offset}: {remaining} bytes remain"
)));
}
let message_len = usize::from(u16::from_ne_bytes([response[offset], response[offset + 1]]));
if message_len < ROUTE_MESSAGE_PREFIX_LEN {
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
"route message at offset {offset} has implausible length {message_len}"
)));
}
let message_end = offset.checked_add(message_len).ok_or_else(|| {
MacOsInterfaceStatsError::MalformedResponse(format!(
"route message length overflows at offset {offset}"
))
})?;
if message_end > response.len() {
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
"route message at offset {offset} declares length {message_len}, exceeding the {}-byte response",
response.len()
)));
}
let message_type = response[offset + 3];
if message_type == libc::RTM_IFINFO2 as u8 {
if message_len < size_of::<libc::if_msghdr2>() {
return Err(MacOsInterfaceStatsError::MalformedResponse(format!(
"RTM_IFINFO2 message at offset {offset} has length {message_len}, smaller than {}",
size_of::<libc::if_msghdr2>()
)));
}
// SAFETY: the bounds checks above prove that a complete `if_msghdr2` lies within
// `response`; route messages are not guaranteed to have Rust alignment, so the value
// is copied with `read_unaligned` instead of creating a reference into the byte slice.
let header = unsafe {
ptr::read_unaligned(response.as_ptr().add(offset).cast::<libc::if_msghdr2>())
};
if libc::c_uint::from(header.ifm_index) == requested_interface_index {
return Ok(InterfaceStats {
rx_bytes: header.ifm_data.ifi_ibytes,
tx_bytes: header.ifm_data.ifi_obytes,
});
}
}
offset = message_end;
}
Err(MacOsInterfaceStatsError::NoInterfaceStats {
interface_index: requested_interface_index,
})
}