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
@@ -12,5 +12,15 @@ fn main() {
|
||||
);
|
||||
}
|
||||
|
||||
match target_os.as_str() {
|
||||
"linux" => {}
|
||||
"macos" => println!(
|
||||
"cargo::warning=traffic control is unavailable on macOS; requested rate changes will be calculated but not applied"
|
||||
),
|
||||
_ => println!(
|
||||
"cargo::warning=target `{target}` has no native interface-statistics or traffic-control backend; synthetic interface load will be used and requested rate changes will not be applied"
|
||||
),
|
||||
}
|
||||
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
}
|
||||
+2
-8
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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