refactor(platform): restructure platform-specific abstractions and add synthetic backends

This commit is contained in:
Nils Andreas Svee committed 2026-07-17 13:02:17 +07:00
1 parent 50f0dcb4dd
commit ae9193580c
8 files changed
+228 -172

No files matched your search

+2 -8
View File
@@ -11,8 +11,6 @@ mod baseliner;
mod config;
mod log;
mod metrics;
#[cfg(target_os = "linux")]
mod netlink;
mod pinger;
mod pinger_icmp;
mod pinger_icmp_ts;
@@ -39,7 +37,7 @@ use crate::config::{Config, MeasurementType};
use crate::pinger::{InFlightProbeCache, PingListener, PingSender};
use crate::pinger_icmp::{PingerICMPEchoListener, PingerICMPEchoSender};
use crate::pinger_icmp_ts::{PingerICMPTimestampListener, PingerICMPTimestampSender};
use crate::platform::{TrafficControlBackend, traffic_control_backend};
use crate::platform::{TrafficControlBackend, traffic_control_backend, warn_platform_limitations};
use crate::ratecontroller::{Ratecontroller, StatsDirection};
use crate::reflector_selector::ReflectorSelector;
@@ -337,11 +335,6 @@ fn run(config: Config) -> anyhow::Result<()> {
};
let mut main_traffic_control = traffic_control_backend();
if main_traffic_control.is_observe_only() {
warn!(
"Traffic control is observe-only on this platform; requested rate changes will not be applied."
);
}
let (down_shaper, up_shaper) = initialize_shaper(&config, &mut main_traffic_control)?;
let err_tx = error_tx.clone();
@@ -483,6 +476,7 @@ fn main() -> anyhow::Result<()> {
let config = Config::new()?;
log::init(config.log_level)?;
warn_platform_limitations();
run(config)
}
+27 -151
View File
@@ -2,37 +2,21 @@
//
// SPDX-License-Identifier: MPL-2.0
#[cfg(any(not(target_os = "linux"), test))]
use log::debug;
#[cfg(any(not(target_os = "linux"), test))]
use std::collections::HashMap;
#[cfg(any(not(target_os = "linux"), test))]
use std::convert::Infallible;
use std::error::Error;
#[cfg(any(not(any(target_os = "linux", target_os = "macos")), test))]
use thiserror::Error;
#[cfg(target_os = "linux")]
use crate::netlink::Netlink;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::MacOsInterfaceStats;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InterfaceStats {
pub rx_bytes: u64,
pub tx_bytes: u64,
pub(crate) struct InterfaceStats {
pub(crate) rx_bytes: u64,
pub(crate) tx_bytes: u64,
}
pub trait InterfaceStatsProvider: Send {
pub(crate) trait InterfaceStatsProvider: Send {
type Error: Error + Send + Sync + 'static;
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error>;
}
pub trait TrafficControlBackend: Send {
pub(crate) trait TrafficControlBackend: Send {
type Error: Error + Send + Sync + 'static;
type Handle: Send + 'static;
@@ -44,138 +28,30 @@ pub trait TrafficControlBackend: Send {
bandwidth_kbit: u64,
dry_run: bool,
) -> Result<(), Self::Error>;
fn is_observe_only(&self) -> bool {
false
}
}
#[cfg(any(not(any(target_os = "linux", target_os = "macos")), test))]
#[derive(Debug, Default)]
pub struct UnsupportedInterfaceStats;
#[cfg(any(not(any(target_os = "linux", target_os = "macos")), test))]
#[derive(Debug, Error, PartialEq, Eq)]
#[error("interface statistics are unsupported on target OS `{target_os}`")]
pub struct UnsupportedInterfaceStatsError {
target_os: &'static str,
}
#[cfg(any(not(any(target_os = "linux", target_os = "macos")), test))]
impl InterfaceStatsProvider for UnsupportedInterfaceStats {
type Error = UnsupportedInterfaceStatsError;
fn read_stats(&mut self, _: &str) -> Result<InterfaceStats, Self::Error> {
Err(UnsupportedInterfaceStatsError {
target_os: std::env::consts::OS,
})
}
}
#[cfg(any(not(target_os = "linux"), test))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObserveOnlyShaper {
interface: String,
}
#[cfg(any(not(target_os = "linux"), test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ObserveOnlyRequest {
bandwidth_kbit: u64,
dry_run: bool,
}
#[cfg(any(not(target_os = "linux"), test))]
#[derive(Debug, Default)]
pub struct ObserveOnlyTrafficControl {
last_requests: HashMap<String, ObserveOnlyRequest>,
}
#[cfg(any(not(target_os = "linux"), test))]
impl TrafficControlBackend for ObserveOnlyTrafficControl {
type Error = Infallible;
type Handle = ObserveOnlyShaper;
fn find_shaper(&mut self, interface: &str) -> Result<Self::Handle, Self::Error> {
Ok(ObserveOnlyShaper {
interface: interface.to_string(),
})
}
fn set_rate(
&mut self,
shaper: &Self::Handle,
bandwidth_kbit: u64,
dry_run: bool,
) -> Result<(), Self::Error> {
debug!(
"Observe-only traffic control requested rate {} kbit/s for interface {} (dry_run={})",
bandwidth_kbit, shaper.interface, dry_run
);
self.last_requests.insert(
shaper.interface.clone(),
ObserveOnlyRequest {
bandwidth_kbit,
dry_run,
},
);
Ok(())
}
fn is_observe_only(&self) -> bool {
true
}
}
#[cfg(test)]
#[derive(Debug, Default)]
pub struct UnsupportedTrafficControl;
#[cfg(test)]
#[derive(Debug, PartialEq, Eq)]
pub struct UnsupportedShaper;
#[cfg(test)]
#[derive(Debug, Error, PartialEq, Eq)]
#[error("traffic control is unsupported on target OS `{target_os}`")]
pub struct UnsupportedTrafficControlError {
target_os: &'static str,
}
#[cfg(test)]
impl TrafficControlBackend for UnsupportedTrafficControl {
type Error = UnsupportedTrafficControlError;
type Handle = UnsupportedShaper;
fn find_shaper(&mut self, _: &str) -> Result<Self::Handle, Self::Error> {
Err(UnsupportedTrafficControlError {
target_os: std::env::consts::OS,
})
}
fn set_rate(&mut self, _: &UnsupportedShaper, _: u64, _: bool) -> Result<(), Self::Error> {
Err(UnsupportedTrafficControlError {
target_os: std::env::consts::OS,
})
}
}
#[cfg(target_os = "linux")]
pub type PlatformInterfaceStats = Netlink;
#[cfg(target_os = "macos")]
pub type PlatformInterfaceStats = MacOsInterfaceStats;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub type PlatformInterfaceStats = UnsupportedInterfaceStats;
#[cfg(target_os = "linux")]
pub type PlatformTrafficControl = Netlink;
#[cfg(not(target_os = "linux"))]
pub type PlatformTrafficControl = ObserveOnlyTrafficControl;
mod noop;
pub fn interface_stats_provider() -> PlatformInterfaceStats {
PlatformInterfaceStats::default()
}
#[cfg(target_os = "linux")]
mod linux;
pub fn traffic_control_backend() -> PlatformTrafficControl {
PlatformTrafficControl::default()
}
#[cfg(target_os = "macos")]
mod macos;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
mod unsupported;
#[cfg(target_os = "linux")]
use linux as imp;
#[cfg(target_os = "macos")]
use macos as imp;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
use unsupported as imp;
pub(crate) use imp::{
PlatformInterfaceStats, PlatformTrafficControl, interface_stats_provider,
traffic_control_backend, warn_platform_limitations,
};
+18
View File
@@ -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() {}
@@ -13,7 +13,7 @@ use thiserror::Error;
use crate::platform::{InterfaceStats, InterfaceStatsProvider, TrafficControlBackend};
#[derive(Debug, Error)]
pub enum NetlinkError {
pub(crate) enum NetlinkError {
#[error("Couldn't find interface `{0}`")]
InterfaceNotFound(String),
@@ -37,16 +37,16 @@ pub enum NetlinkError {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Qdisc {
pub ifindex: i32,
pub parent: u32,
pub(crate) struct Qdisc {
ifindex: i32,
parent: u32,
}
#[derive(Debug, Default)]
pub struct Netlink {}
pub(crate) struct Netlink {}
impl Netlink {
pub fn find_interface(ifname: &str) -> Result<i32, NetlinkError> {
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());
@@ -59,7 +59,7 @@ impl Netlink {
Ok(header.ifi_index)
}
pub fn get_interface_stats(ifname: &str) -> Result<(u64, u64), NetlinkError> {
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());
@@ -79,7 +79,7 @@ impl Netlink {
Err(NetlinkError::NoInterfaceStatsFound(ifname.to_string()))
}
pub fn qdisc_from_ifindex(ifindex: i32, ifname: &str) -> Result<Qdisc, NetlinkError> {
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);
@@ -105,12 +105,12 @@ impl Netlink {
Err(NetlinkError::NoQdiscFound(ifname.to_string()))
}
pub fn qdisc_from_ifname(ifname: &str) -> Result<Qdisc, NetlinkError> {
fn qdisc_from_ifname(ifname: &str) -> Result<Qdisc, NetlinkError> {
let ifindex = Netlink::find_interface(ifname)?;
Netlink::qdisc_from_ifindex(ifindex, ifname)
}
pub fn set_qdisc_rate(
fn set_qdisc_rate(
qdisc: Qdisc,
bandwidth_kbit: u64,
dry_run: bool,
+22 -3
View File
@@ -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."
);
}
+39
View File
@@ -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(())
}
}
+100
View File
@@ -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."
);
}