feat(platform): add FreeBSD support for interface statistics
Implement interface statistics for FreeBSD using sysctl and introduce a basic UCI configuration source for Linux. Include a cross-compilation configuration and a target check script to verify multi-platform support.
This commit is contained in:
6 files changed
+229
-2
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
[target.x86_64-unknown-freebsd]
|
||||||
|
image.name = "ghcr.io/cross-rs/x86_64-unknown-freebsd:local"
|
||||||
|
image.toolchain = ["linux/arm64=aarch64-unknown-linux-gnu"]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
targets=(
|
||||||
|
aarch64-apple-darwin
|
||||||
|
x86_64-unknown-linux-gnu
|
||||||
|
x86_64-unknown-linux-musl
|
||||||
|
aarch64-unknown-linux-musl
|
||||||
|
x86_64-unknown-freebsd
|
||||||
|
)
|
||||||
|
|
||||||
|
for target in "${targets[@]}"; do
|
||||||
|
echo
|
||||||
|
echo "==> Checking ${target}"
|
||||||
|
cargo check \
|
||||||
|
--workspace \
|
||||||
|
--all-targets \
|
||||||
|
--target "${target}"
|
||||||
|
done
|
||||||
+8
-2
@@ -33,22 +33,28 @@ pub(crate) trait TrafficControlBackend: Send {
|
|||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
mod noop;
|
mod noop;
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
mod freebsd;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod linux;
|
mod linux;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod macos;
|
mod macos;
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
#[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "macos")))]
|
||||||
mod unsupported;
|
mod unsupported;
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
use freebsd as imp;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use linux as imp;
|
use linux as imp;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use macos as imp;
|
use macos as imp;
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
#[cfg(not(any(target_os = "freebsd", target_os = "linux", target_os = "macos")))]
|
||||||
use unsupported as imp;
|
use unsupported as imp;
|
||||||
|
|
||||||
pub(crate) use imp::{
|
pub(crate) use imp::{
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair)
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::io;
|
||||||
|
use std::mem::{MaybeUninit, size_of};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
|
||||||
|
use super::noop::NoopTrafficControl;
|
||||||
|
use crate::platform::{InterfaceStats, InterfaceStatsProvider};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub(crate) enum FreeBsdInterfaceStatsError {
|
||||||
|
#[error("interface name contains a NUL byte")]
|
||||||
|
InterfaceNameContainsNul(#[source] std::ffi::NulError),
|
||||||
|
|
||||||
|
#[error("interface `{interface}` was not found")]
|
||||||
|
InterfaceNotFound {
|
||||||
|
interface: String,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("failed to query statistics for interface `{interface}`")]
|
||||||
|
Sysctl {
|
||||||
|
interface: String,
|
||||||
|
#[source]
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"unexpected interface-statistics response size for `{interface}`: \
|
||||||
|
expected {expected} bytes, received {actual}"
|
||||||
|
)]
|
||||||
|
UnexpectedResponseSize {
|
||||||
|
interface: String,
|
||||||
|
expected: usize,
|
||||||
|
actual: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct FreeBsdInterfaceStats;
|
||||||
|
|
||||||
|
impl InterfaceStatsProvider for FreeBsdInterfaceStats {
|
||||||
|
type Error = FreeBsdInterfaceStatsError;
|
||||||
|
|
||||||
|
fn read_stats(&mut self, interface: &str) -> Result<InterfaceStats, Self::Error> {
|
||||||
|
let interface_name = CString::new(interface)
|
||||||
|
.map_err(FreeBsdInterfaceStatsError::InterfaceNameContainsNul)?;
|
||||||
|
let interface_index = unsafe { libc::if_nametoindex(interface_name.as_ptr()) };
|
||||||
|
if interface_index == 0 {
|
||||||
|
return Err(FreeBsdInterfaceStatsError::InterfaceNotFound {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
source: io::Error::last_os_error(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mib = [
|
||||||
|
libc::CTL_NET,
|
||||||
|
libc::PF_LINK,
|
||||||
|
libc::NETLINK_GENERIC,
|
||||||
|
libc::IFMIB_IFDATA,
|
||||||
|
interface_index as libc::c_int,
|
||||||
|
libc::IFDATA_GENERAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut ifmd = MaybeUninit::<libc::ifmibdata>::uninit();
|
||||||
|
let mut returned_len = size_of::<libc::ifmibdata>();
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
libc::sysctl(
|
||||||
|
mib.as_ptr(),
|
||||||
|
mib.len() as libc::c_uint,
|
||||||
|
ifmd.as_mut_ptr().cast::<libc::c_void>(),
|
||||||
|
&mut returned_len,
|
||||||
|
ptr::null(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result == -1 {
|
||||||
|
return Err(FreeBsdInterfaceStatsError::Sysctl {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
source: io::Error::last_os_error(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_len = size_of::<libc::ifmibdata>();
|
||||||
|
if returned_len != expected_len {
|
||||||
|
return Err(FreeBsdInterfaceStatsError::UnexpectedResponseSize {
|
||||||
|
interface: interface.to_string(),
|
||||||
|
expected: expected_len,
|
||||||
|
actual: returned_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let ifmd = unsafe { ifmd.assume_init() };
|
||||||
|
|
||||||
|
Ok(InterfaceStats {
|
||||||
|
rx_bytes: ifmd.ifmd_data.ifi_ibytes,
|
||||||
|
tx_bytes: ifmd.ifmd_data.ifi_obytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type PlatformInterfaceStats = FreeBsdInterfaceStats;
|
||||||
|
pub(crate) type PlatformTrafficControl = NoopTrafficControl;
|
||||||
|
|
||||||
|
pub(crate) fn interface_stats_provider() -> PlatformInterfaceStats {
|
||||||
|
FreeBsdInterfaceStats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn traffic_control_backend() -> PlatformTrafficControl {
|
||||||
|
NoopTrafficControl
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn warn_platform_limitations() {
|
||||||
|
warn!(
|
||||||
|
"Traffic control is unavailable on FreeBSD; requested rate changes will be calculated but not applied."
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
mod netlink;
|
mod netlink;
|
||||||
|
#[cfg(all(target_os = "linux", feature = "uci"))]
|
||||||
|
mod uci;
|
||||||
|
|
||||||
pub(crate) type PlatformInterfaceStats = netlink::Netlink;
|
pub(crate) type PlatformInterfaceStats = netlink::Netlink;
|
||||||
pub(crate) type PlatformTrafficControl = netlink::Netlink;
|
pub(crate) type PlatformTrafficControl = netlink::Netlink;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use config::{ConfigError, Map, Source, Value, ValueKind};
|
||||||
|
use rust_uci::Uci;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct ConfigKey {
|
||||||
|
canonical: &'static str,
|
||||||
|
uci: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const UCI_KEYS: &[ConfigKey] = &[
|
||||||
|
ConfigKey {
|
||||||
|
canonical: "network.download_base_kbits",
|
||||||
|
uci: "sqm-autorate-rust.@network[0].download_base_kbits",
|
||||||
|
},
|
||||||
|
ConfigKey {
|
||||||
|
canonical: "network.download_interface",
|
||||||
|
uci: "sqm-autorate-rust.@network[0].download_interface",
|
||||||
|
},
|
||||||
|
ConfigKey {
|
||||||
|
canonical: "observability.enabled",
|
||||||
|
uci: "sqm-autorate-rust.@observability[0].enabled",
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct UciSource {
|
||||||
|
keys: &'static [ConfigKey],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UciSource {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { keys: UCI_KEYS }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for UciSource {
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
|
||||||
|
let mut uci =
|
||||||
|
Uci::new().map_err(|e| ConfigError::Message(format!("failed to open UCI: {e}")))?;
|
||||||
|
|
||||||
|
let origin = "UCI package sqm-autorate-rust".to_owned();
|
||||||
|
let mut values = Map::new();
|
||||||
|
|
||||||
|
for key in self.keys {
|
||||||
|
match uci.get(key.uci) {
|
||||||
|
Ok(raw) => {
|
||||||
|
values.insert(
|
||||||
|
key.canonical.to_owned(),
|
||||||
|
Value::new(Some(&origin), ValueKind::String(raw)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This needs a real check for UCI's "not found" result.
|
||||||
|
Err(e) if is_missing_uci_value(&e) => {}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ConfigError::Message(format!(
|
||||||
|
"failed reading UCI key {}: {e}",
|
||||||
|
key.uci
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(values)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user