refactor(platform): restructure platform-specific abstractions and add synthetic backends
This commit is contained in:
8 files changed
+228
-172
No files matched your search
@@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
mod netlink;
|
||||
|
||||
pub(crate) type PlatformInterfaceStats = netlink::Netlink;
|
||||
pub(crate) type PlatformTrafficControl = netlink::Netlink;
|
||||
|
||||
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||
netlink::Netlink::default()
|
||||
}
|
||||
|
||||
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||
netlink::Netlink::default()
|
||||
}
|
||||
|
||||
pub(crate) fn warn_platform_limitations() {}
|
||||
@@ -0,0 +1,172 @@
|
||||
// SPDX-FileCopyrightText: 2022-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::io;
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use log::info;
|
||||
use netlink_bindings::{rt_link, tc};
|
||||
use netlink_socket2::NetlinkSocket;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::platform::{InterfaceStats, InterfaceStatsProvider, TrafficControlBackend};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum NetlinkError {
|
||||
#[error("Couldn't find interface `{0}`")]
|
||||
InterfaceNotFound(String),
|
||||
|
||||
#[error("Netlink error: {0}")]
|
||||
Netlink(#[from] io::Error),
|
||||
|
||||
#[error("Netlink reply error: {0}")]
|
||||
Reply(#[from] netlink_socket2::ReplyError),
|
||||
|
||||
#[error("Something went wrong while finding qdisc")]
|
||||
NlQdiscError(String),
|
||||
|
||||
#[error("Couldn't find CAKE qdisc on interface `{0}`")]
|
||||
NoQdiscFound(String),
|
||||
|
||||
#[error("Couldn't find interface statistics: `{0}`")]
|
||||
NoInterfaceStatsFound(String),
|
||||
|
||||
#[error("Error happened while parsing UTF-8 string")]
|
||||
Utf8Error(#[from] Utf8Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Qdisc {
|
||||
ifindex: i32,
|
||||
parent: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Netlink {}
|
||||
|
||||
impl Netlink {
|
||||
fn find_interface(ifname: &str) -> Result<i32, NetlinkError> {
|
||||
let mut socket = NetlinkSocket::new();
|
||||
|
||||
let mut request = rt_link::Request::new().op_getlink_do(&Default::default());
|
||||
request.encode().push_ifname_bytes(ifname.as_bytes());
|
||||
|
||||
let mut iter = socket.request(&request)?;
|
||||
let (header, _) = iter
|
||||
.recv_one()
|
||||
.map_err(|_| NetlinkError::InterfaceNotFound(ifname.to_string()))?;
|
||||
Ok(header.ifi_index)
|
||||
}
|
||||
|
||||
fn get_interface_stats(ifname: &str) -> Result<(u64, u64), NetlinkError> {
|
||||
let mut socket = NetlinkSocket::new();
|
||||
|
||||
let mut request = rt_link::Request::new().op_getlink_do(&Default::default());
|
||||
request
|
||||
.encode()
|
||||
.push_ifname_bytes(ifname.as_bytes())
|
||||
.push_ext_mask(1 /* RTEXT_FILTER_VF */);
|
||||
|
||||
let mut iter = socket.request(&request)?;
|
||||
while let Some(reply) = iter.recv() {
|
||||
let (_, attrs) = reply?;
|
||||
if let Ok(stats) = attrs.get_stats64() {
|
||||
return Ok((stats.rx_bytes, stats.tx_bytes));
|
||||
}
|
||||
}
|
||||
|
||||
Err(NetlinkError::NoInterfaceStatsFound(ifname.to_string()))
|
||||
}
|
||||
|
||||
fn qdisc_from_ifindex(ifindex: i32, ifname: &str) -> Result<Qdisc, NetlinkError> {
|
||||
let mut socket = NetlinkSocket::new();
|
||||
let header = tc::Tcmsg::new();
|
||||
let request = tc::Request::new().op_getqdisc_dump(&header);
|
||||
|
||||
let mut iter = socket.request(&request)?;
|
||||
while let Some(reply) = iter.recv() {
|
||||
let (header, attrs) = reply?;
|
||||
|
||||
if header.ifindex == ifindex
|
||||
&& let Ok(kind) = attrs.get_kind()
|
||||
&& kind
|
||||
.to_str()
|
||||
.map_err(|_| NetlinkError::NlQdiscError("Invalid UTF-8".to_string()))?
|
||||
== "cake"
|
||||
{
|
||||
return Ok(Qdisc {
|
||||
ifindex,
|
||||
parent: header.parent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(NetlinkError::NoQdiscFound(ifname.to_string()))
|
||||
}
|
||||
|
||||
fn qdisc_from_ifname(ifname: &str) -> Result<Qdisc, NetlinkError> {
|
||||
let ifindex = Netlink::find_interface(ifname)?;
|
||||
Netlink::qdisc_from_ifindex(ifindex, ifname)
|
||||
}
|
||||
|
||||
fn set_qdisc_rate(
|
||||
qdisc: Qdisc,
|
||||
bandwidth_kbit: u64,
|
||||
dry_run: bool,
|
||||
) -> Result<(), NetlinkError> {
|
||||
if dry_run {
|
||||
info!(
|
||||
"dry-run: would set qdisc ifindex={} to {} kbit/s",
|
||||
qdisc.ifindex, bandwidth_kbit
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut socket = NetlinkSocket::new();
|
||||
let bandwidth = bandwidth_kbit * 1000 / 8;
|
||||
|
||||
let mut header = tc::Tcmsg::new();
|
||||
header.ifindex = qdisc.ifindex;
|
||||
header.parent = qdisc.parent;
|
||||
let mut request = tc::Request::new().set_change().op_newqdisc_do(&header);
|
||||
request
|
||||
.encode()
|
||||
.push_kind(c"cake")
|
||||
.nested_options_cake()
|
||||
.push_base_rate64(bandwidth)
|
||||
.end_nested();
|
||||
|
||||
let mut iter = socket.request(&request)?;
|
||||
iter.recv_ack()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceStatsProvider for Netlink {
|
||||
type Error = NetlinkError;
|
||||
|
||||
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||
let (rx_bytes, tx_bytes) = Self::get_interface_stats(interface)?;
|
||||
Ok(InterfaceStats { rx_bytes, tx_bytes })
|
||||
}
|
||||
}
|
||||
|
||||
impl TrafficControlBackend for Netlink {
|
||||
type Error = NetlinkError;
|
||||
type Handle = Qdisc;
|
||||
|
||||
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error> {
|
||||
Self::qdisc_from_ifname(interface)
|
||||
}
|
||||
|
||||
fn set_rate(
|
||||
&mut self,
|
||||
shaper: &Self::Handle,
|
||||
bandwidth_kbit: u64,
|
||||
dry_run: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
Self::set_qdisc_rate(*shaper, bandwidth_kbit, dry_run)
|
||||
}
|
||||
}
|
||||
+22
-3
@@ -7,15 +7,17 @@ use std::io;
|
||||
use std::mem::size_of;
|
||||
use std::ptr;
|
||||
|
||||
use log::warn;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{InterfaceStats, InterfaceStatsProvider};
|
||||
use super::noop::NoopTrafficControl;
|
||||
use crate::platform::{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 {
|
||||
pub(crate) enum MacOsInterfaceStatsError {
|
||||
#[error("interface name contains a NUL byte")]
|
||||
InterfaceNameContainsNul(#[source] NulError),
|
||||
|
||||
@@ -41,7 +43,7 @@ pub enum MacOsInterfaceStatsError {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MacOsInterfaceStats;
|
||||
pub(crate) struct MacOsInterfaceStats;
|
||||
|
||||
impl InterfaceStatsProvider for MacOsInterfaceStats {
|
||||
type Error = MacOsInterfaceStatsError;
|
||||
@@ -197,3 +199,20 @@ fn parse_route_messages(
|
||||
interface_index: requested_interface_index,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) type PlatformInterfaceStats = MacOsInterfaceStats;
|
||||
pub(crate) type PlatformTrafficControl = NoopTrafficControl;
|
||||
|
||||
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||
MacOsInterfaceStats
|
||||
}
|
||||
|
||||
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||
NoopTrafficControl
|
||||
}
|
||||
|
||||
pub(crate) fn warn_platform_limitations() {
|
||||
warn!(
|
||||
"Traffic control is unavailable on macOS; requested rate changes will be calculated but not applied."
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use crate::platform::TrafficControlBackend;
|
||||
use log::debug;
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct NoopShaper {
|
||||
interface: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct NoopTrafficControl;
|
||||
|
||||
impl TrafficControlBackend for NoopTrafficControl {
|
||||
type Error = Infallible;
|
||||
type Handle = NoopShaper;
|
||||
|
||||
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error> {
|
||||
Ok(NoopShaper {
|
||||
interface: interface.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_rate(
|
||||
&mut self,
|
||||
shaper: &Self::Handle,
|
||||
bandwidth_kbit: u64,
|
||||
dry_run: bool,
|
||||
) -> Result<(), Self::Error> {
|
||||
debug!(
|
||||
"Ignoring traffic-control rate request of {} kbit/s for interface {} (dry_run={})",
|
||||
bandwidth_kbit, shaper.interface, dry_run
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::platform::noop::NoopTrafficControl;
|
||||
use crate::platform::{InterfaceStats, InterfaceStatsProvider};
|
||||
|
||||
// Synthetic rates are fixed per interface and intentionally conservative. Counter increments are
|
||||
// derived from elapsed monotonic time, so changing the polling frequency does not change the
|
||||
// apparent traffic rate.
|
||||
const SYNTHETIC_RX_KBIT_RANGE: std::ops::RangeInclusive<u64> = 500..=25_000;
|
||||
const SYNTHETIC_TX_KBIT_RANGE: std::ops::RangeInclusive<u64> = 250..=10_000;
|
||||
const INITIAL_COUNTER_BYTES: u64 = 1;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SyntheticInterfaceState {
|
||||
rx_bytes: u64,
|
||||
tx_bytes: u64,
|
||||
rx_kbit: u64,
|
||||
tx_kbit: u64,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
impl SyntheticInterfaceState {
|
||||
fn new(now: Instant) -> Self {
|
||||
Self {
|
||||
rx_bytes: INITIAL_COUNTER_BYTES,
|
||||
tx_bytes: INITIAL_COUNTER_BYTES,
|
||||
rx_kbit: fastrand::u64(SYNTHETIC_RX_KBIT_RANGE),
|
||||
tx_kbit: fastrand::u64(SYNTHETIC_TX_KBIT_RANGE),
|
||||
last_update: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, now: Instant) {
|
||||
let elapsed = now.saturating_duration_since(self.last_update);
|
||||
self.rx_bytes = self
|
||||
.rx_bytes
|
||||
.saturating_add(bytes_for_elapsed(elapsed, self.rx_kbit));
|
||||
self.tx_bytes = self
|
||||
.tx_bytes
|
||||
.saturating_add(bytes_for_elapsed(elapsed, self.tx_kbit));
|
||||
self.last_update = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_for_elapsed(elapsed: Duration, bitrate_kbit: u64) -> u64 {
|
||||
let bytes = elapsed
|
||||
.as_nanos()
|
||||
.saturating_mul(u128::from(bitrate_kbit))
|
||||
.saturating_mul(1_000)
|
||||
/ 8_000_000_000;
|
||||
u64::try_from(bytes).unwrap_or(u64::MAX)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct SyntheticInterfaceStats {
|
||||
interfaces: HashMap<String, SyntheticInterfaceState>,
|
||||
}
|
||||
|
||||
impl InterfaceStatsProvider for SyntheticInterfaceStats {
|
||||
type Error = Infallible;
|
||||
|
||||
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||
let now = Instant::now();
|
||||
let state = self
|
||||
.interfaces
|
||||
.entry(interface.to_string())
|
||||
.or_insert_with(|| SyntheticInterfaceState::new(now));
|
||||
state.update(now);
|
||||
|
||||
Ok(InterfaceStats {
|
||||
rx_bytes: state.rx_bytes,
|
||||
tx_bytes: state.tx_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type PlatformInterfaceStats = SyntheticInterfaceStats;
|
||||
pub(crate) type PlatformTrafficControl = NoopTrafficControl;
|
||||
|
||||
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||
SyntheticInterfaceStats::default()
|
||||
}
|
||||
|
||||
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||
NoopTrafficControl
|
||||
}
|
||||
|
||||
pub(crate) fn warn_platform_limitations() {
|
||||
warn!(
|
||||
"This operating system has no native interface-statistics or traffic-control backend; synthetic interface load will be used and requested rate changes will not be applied."
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user