diff --git a/Cargo.lock b/Cargo.lock index 6095311..f9cddc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,18 @@ dependencies = [ "cc", ] +[[package]] +name = "config" +version = "0.15.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" +dependencies = [ + "pathdiff", + "serde_core", + "toml", + "winnow", +] + [[package]] name = "either" version = "1.16.0" @@ -284,6 +296,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "prettyplease" version = "0.2.37" @@ -389,6 +407,45 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "shlex" version = "1.3.0" @@ -425,6 +482,7 @@ name = "sqm-autorate-rust" version = "0.4.1" dependencies = [ "anyhow", + "config", "fastrand", "flume", "icmp-socket2", @@ -434,6 +492,7 @@ dependencies = [ "netlink-socket2", "rust-uci", "rustix", + "serde", "thiserror", ] @@ -479,6 +538,37 @@ dependencies = [ "syn 3.0.2", ] +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -544,3 +634,12 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index fcf3c73..3dcf053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] } thiserror = "2.0" libc = "0.2" flume = { version = "0.12.0", default-features = false, features = ["eventual-fairness", "select"] } +config = { version = "0.15.25", default-features = false } +serde = { version = "1", features = ["derive"] } [dependencies.log] features = ["std"] @@ -37,4 +39,6 @@ netlink-socket2 = { version = "0.3.3" } rust-uci = { git = "https://github.com/namib-project/rust-uci.git", rev = "3e7b6153b6df92b45428c32bb6461756563deecb", optional = true } [features] +default = ["toml"] +toml = ["config/toml"] uci = ["dep:rust-uci"] diff --git a/src/main.rs b/src/main.rs index a8a0b51..88b73fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod pinger_icmp_ts; mod platform; mod ratecontroller; mod reflector_selector; +mod settings; mod time; mod util; diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..fecb8c4 --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2026-Present Nils Andreas Svee mailto:contact@lochnair.net (github @Lochnair) +// +// SPDX-License-Identifier: MPL-2.0 + +use anyhow::{Context, bail}; +use config::{Config, ConfigError, Environment}; +use log::Level; +use serde::{Deserialize, Deserializer, de}; +use std::fmt::{self, Display}; +use std::str::FromStr; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MeasurementType { + Icmp = 1, + IcmpTimestamps, +} + +impl Display for MeasurementType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MeasurementType::Icmp => write!(f, "icmp"), + MeasurementType::IcmpTimestamps => write!(f, "icmp-timestamps"), + } + } +} + +impl FromStr for MeasurementType { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "icmp" => Ok(MeasurementType::Icmp), + "icmp-timestamps" | "icmp_timestamps" => Ok(MeasurementType::IcmpTimestamps), + _ => Err(ConfigError::Message(format!( + "invalid measurement type: {s}" + ))), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ObservabilityProtocol { + Udp, + Tcp, +} + +impl FromStr for ObservabilityProtocol { + type Err = ConfigError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "udp" => Ok(ObservabilityProtocol::Udp), + "tcp" => Ok(ObservabilityProtocol::Tcp), + _ => Err(ConfigError::Message(format!( + "invalid observability protocol: {s}", + ))), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct Settings { + pub(crate) network: NetworkSettings, + + #[serde(default)] + pub(crate) output: OutputSettings, + + #[serde(default)] + pub(crate) observability: ObservabilitySettings, + + #[serde(default)] + pub(crate) advanced_settings: AdvancedSettings, +} + +impl Settings { + pub(crate) fn load() -> Result { + let builder = Config::builder(); + + #[cfg(feature = "toml")] + let builder = builder.add_source( + config::File::with_name("/etc/sqm-autorate-rust/config.toml").required(false), + ); + + let builder = builder.add_source(Environment::with_prefix("SQMA").separator("__")); + let built_config = builder.build()?; + + built_config.try_deserialize() + } +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct NetworkSettings { + pub(crate) download_interface: String, + pub(crate) upload_interface: String, + pub(crate) download_base_kbits: f64, + #[serde(default = "default_min_percent")] + pub(crate) download_min_percent: f64, + pub(crate) upload_base_kbits: f64, + #[serde(default = "default_min_percent")] + pub(crate) upload_min_percent: f64, +} + +impl NetworkSettings { + pub(crate) fn download_min_kbits(&self) -> f64 { + (self.download_base_kbits * self.download_min_percent / 100.0).floor() + } + + pub(crate) fn upload_min_kbits(&self) -> f64 { + (self.upload_base_kbits * self.upload_min_percent / 100.0).floor() + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +pub(crate) struct OutputSettings { + #[serde(deserialize_with = "deserialize_from_str")] + pub(crate) log_level: Level, + pub(crate) speed_hist_file: String, + pub(crate) stats_file: String, + pub(crate) suppress_statistics: bool, +} + +impl Default for OutputSettings { + fn default() -> Self { + Self { + log_level: Level::Error, + speed_hist_file: "/tmp/sqm-speedhist.csv".to_owned(), + stats_file: "/tmp/sqm-autorate-rust.csv".to_owned(), + suppress_statistics: false, + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +pub(crate) struct ObservabilitySettings { + pub(crate) enabled: bool, + #[serde(deserialize_with = "deserialize_from_str")] + pub(crate) protocol: ObservabilityProtocol, + pub(crate) host: Option, + pub(crate) port: u16, + pub(crate) batch_size: usize, + pub(crate) batch_timeout_ms: u64, + pub(crate) export_ping_metrics: bool, + pub(crate) export_rate_metrics: bool, + pub(crate) export_baseline_metrics: bool, + pub(crate) export_events: bool, + pub(crate) host_tag: String, +} + +impl Default for ObservabilitySettings { + fn default() -> Self { + Self { + enabled: false, + protocol: ObservabilityProtocol::Udp, + host: None, + port: 8089, + batch_size: 25, + batch_timeout_ms: 100, + export_ping_metrics: false, + export_rate_metrics: true, + export_baseline_metrics: false, + export_events: true, + host_tag: default_host_tag(), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(default)] +pub(crate) struct AdvancedSettings { + pub(crate) download_delay_ms: f64, + pub(crate) high_load_level: f64, + pub(crate) min_change_interval: f64, + #[serde(deserialize_with = "deserialize_from_str")] + pub(crate) measurement_type: MeasurementType, + pub(crate) num_reflectors: u8, + pub(crate) peer_reselection_time: u64, + pub(crate) reflector_list_file: String, + pub(crate) speed_hist_size: u32, + pub(crate) tick_interval: f64, + pub(crate) upload_delay_ms: f64, + pub(crate) dry_run: bool, +} + +impl Default for AdvancedSettings { + fn default() -> Self { + Self { + download_delay_ms: 15.0, + high_load_level: 0.8, + min_change_interval: 0.5, + measurement_type: MeasurementType::IcmpTimestamps, + num_reflectors: 5, + peer_reselection_time: 15, + reflector_list_file: "/etc/sqm-autorate/reflectors-icmp.csv".to_owned(), + speed_hist_size: 100, + tick_interval: 0.5, + upload_delay_ms: 15.0, + dry_run: false, + } + } +} + +fn default_min_percent() -> f64 { + 20.0 +} + +fn default_host_tag() -> String { + rustix::system::uname() + .nodename() + .to_string_lossy() + .into_owned() +} + +fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromStr, + T::Err: Display, +{ + let value = String::deserialize(deserializer)?; + value.parse().map_err(de::Error::custom) +}