use anyhow::Result; use log::Level; #[cfg(feature = "uci")] use log::warn; #[cfg(feature = "uci")] use rust_uci::Uci; use std::fmt; use std::fmt::Display; use std::fs::File; use std::io::BufRead; use std::net::IpAddr; use std::path::Path; use std::str::FromStr; use std::{env, io}; use thiserror::Error; #[derive(Error, Debug)] pub enum ConfigError { #[error("Invalid measurement type")] InvalidMeasurementType(String), #[error("Couldn't parse value for key: `{0}`: invalid value")] ParseError(String), #[error("No config value found for key: `{0}`")] MissingValue(String), #[error("Reflector list not found at: {0}")] ReflectorListNotFound(String), } fn read_lines

(filename: P) -> io::Result>> where P: AsRef, { let file = File::open(filename)?; Ok(io::BufReader::new(file).lines()) } struct FlexiBool(bool); impl FromStr for FlexiBool { type Err = ConfigError; fn from_str(s: &str) -> Result { match s.trim().to_lowercase().as_str() { "1" | "true" | "yes" | "y" | "on" | "enabled" => Ok(FlexiBool(true)), "0" | "false" | "no" | "n" | "off" | "disabled" => Ok(FlexiBool(false)), _ => Err(ConfigError::ParseError(s.to_string())), } } } impl From for bool { fn from(f: FlexiBool) -> bool { f.0 } } #[derive(Clone, Copy, Debug)] pub enum MeasurementType { Icmp = 1, IcmpTimestamps, Ntp, TcpTimestamps, } 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"), MeasurementType::Ntp => write!(f, "ntp"), MeasurementType::TcpTimestamps => write!(f, "tcp-timestamps"), } } } impl FromStr for MeasurementType { type Err = ConfigError; fn from_str(s: &str) -> Result { return match s.to_lowercase().as_str() { "icmp" => Ok(MeasurementType::Icmp), "icmp-timestamps" => Ok(MeasurementType::IcmpTimestamps), "ntp" => Ok(MeasurementType::Ntp), "tcp-timestamps" => Ok(MeasurementType::TcpTimestamps), &_ => Err(ConfigError::InvalidMeasurementType(s.to_string())), }; } } #[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::InvalidMeasurementType(s.to_string())), } } } #[derive(Clone, Debug)] pub struct Config { // Network section pub download_interface: String, pub upload_interface: String, pub download_base_kbits: f64, pub download_min_percent: f64, pub download_min_kbits: f64, pub upload_base_kbits: f64, pub upload_min_percent: f64, pub upload_min_kbits: f64, // Output section pub log_level: Level, pub speed_hist_file: String, pub stats_file: String, pub suppress_statistics: bool, // Observability section pub observability_enabled: bool, pub observability_protocol: ObservabilityProtocol, pub observability_host: Option, pub observability_port: u16, pub observability_batch_size: usize, pub observability_batch_timeout_ms: u64, pub observability_export_ping_metrics: bool, pub observability_export_rate_metrics: bool, pub observability_export_baseline_metrics: bool, pub observability_export_events: bool, pub observability_host_tag: String, // Advanced section pub download_delay_ms: f64, pub high_load_level: f64, pub min_change_interval: f64, pub measurement_type: MeasurementType, pub num_reflectors: u8, pub peer_reselection_time: u64, pub reflector_list_file: String, pub speed_hist_size: u32, pub tick_interval: f64, pub upload_delay_ms: f64, // Monitoring / dry-run pub dry_run: bool, } impl Config { pub fn new() -> Result { let config = Self { // Network section download_base_kbits: Self::get::( "SQMA_DOWNLOAD_BASE_KBITS", "sqm-autorate.@network[0].download_base_kbits", None, )?, download_interface: Self::get::( "SQMA_DOWNLOAD_INTERFACE", "sqm-autorate.@network[0].download_interface", None, )?, download_min_percent: 0.0, // placeholder, computed below download_min_kbits: 0.0, // placeholder, computed below upload_base_kbits: Self::get::( "SQMA_UPLOAD_BASE_KBITS", "sqm-autorate.@network[0].upload_base_kbits", None, )?, upload_interface: Self::get::( "SQMA_UPLOAD_INTERFACE", "sqm-autorate.@network[0].upload_interface", None, )?, upload_min_percent: 0.0, // placeholder, computed below upload_min_kbits: 0.0, // placeholder, computed below // Output section log_level: Self::get::( "SQMA_LOG_LEVEL", "sqm-autorate.@output[0].log_level", Some(Level::Error), )?, speed_hist_file: Self::get::( "SQMA_SPEED_HIST_FILE", "sqm-autorate.@output[0].speed_hist_file", Some("/tmp/sqm-speedhist.csv".parse()?), )?, stats_file: Self::get::( "SQMA_STATS_FILE", "sqm-autorate.@output[0].stats_file", Some("/tmp/sqm-autorate.csv".parse()?), )?, suppress_statistics: Self::get::( "SQMA_SUPPRESS_STATISTICS", "sqm-autorate.@output[0].suppress_statistics", Some(false), )?, // Observability section observability_enabled: Self::get::( "SQMA_OBSERVABILITY_ENABLED", "sqm-autorate.@observability[0].enabled", Some(false), )?, observability_protocol: Self::get::( "SQMA_OBSERVABILITY_PROTOCOL", "sqm-autorate.@observability[0].protocol", Some(ObservabilityProtocol::Udp), )?, observability_host: Self::get_optional::( "SQMA_OBSERVABILITY_HOST", "sqm-autorate.@observability[0].host", ), observability_port: Self::get::( "SQMA_OBSERVABILITY_PORT", "sqm-autorate.@observability[0].port", Some(8089), )?, observability_batch_size: Self::get::( "SQMA_OBSERVABILITY_BATCH_SIZE", "sqm-autorate.@observability[0].batch_size", Some(25), )?, observability_batch_timeout_ms: Self::get::( "SQMA_OBSERVABILITY_BATCH_TIMEOUT_MS", "sqm-autorate.@observability[0].batch_timeout_ms", Some(100), )?, observability_export_ping_metrics: Self::get::( "SQMA_OBSERVABILITY_EXPORT_PING_METRICS", "sqm-autorate.@observability[0].export_ping_metrics", Some(false), )?, observability_export_rate_metrics: Self::get::( "SQMA_OBSERVABILITY_EXPORT_RATE_METRICS", "sqm-autorate.@observability[0].export_rate_metrics", Some(true), )?, observability_export_baseline_metrics: Self::get::( "SQMA_OBSERVABILITY_EXPORT_BASELINE_METRICS", "sqm-autorate.@observability[0].export_baseline_metrics", Some(false), )?, observability_export_events: Self::get::( "SQMA_OBSERVABILITY_EXPORT_EVENTS", "sqm-autorate.@observability[0].export_events", Some(true), )?, observability_host_tag: Self::get_host_tag(), // Advanced section download_delay_ms: Self::get::( "SQMA_DOWNLOAD_DELAY_MS", "sqm-autorate.@advanced_settings[0].download_delay_ms", Some(15.0), )?, high_load_level: Self::get::( "SQMA_HIGH_LOAD_LEVEL", "sqm-autorate.@advanced_settings[0].high_load_level", Some(0.8), )?, measurement_type: Self::get::( "SQMA_MEASUREMENT_TYPE", "sqm-autorate.@advanced_settings[0].measurement_type", Some(MeasurementType::IcmpTimestamps), )?, min_change_interval: Self::get::( "SQMA_MIN_CHANGE_INTERVAL", "sqm-autorate.@advanced_settings[0].min_change_interval", Some(0.5), )?, num_reflectors: Self::get::( "SQMA_NUM_REFLECTORS", "sqm-autorate.@advanced_settings[0].num_reflectors", Some(5), )?, peer_reselection_time: Self::get::( "SQMA_PEER_RESELECTION_TIME", "sqm-autorate.@advanced_settings[0].peer_reselection_time", Some(15), )?, reflector_list_file: Self::get::( "SQMA_REFLECTOR_LIST_FILE", "sqm-autorate.@advanced_settings[0].reflector_list_file", Some("/etc/sqm-autorate/reflectors-icmp.csv".parse()?), )?, speed_hist_size: Self::get::( "SQMA_SPEED_HIST_SIZE", "sqm-autorate.@advanced_settings[0].speed_hist_size", Some(100), )?, tick_interval: Self::get::( "SQMA_TICK_INTERVAL", "sqm-autorate.@advanced_settings[0].tick_interval", Some(0.5), )?, upload_delay_ms: Self::get::( "SQMA_UPLOAD_DELAY_MS", "sqm-autorate.@advanced_settings[0].upload_delay_ms", Some(15.0), )?, dry_run: Self::get::( "SQMA_DRY_RUN", "sqm-autorate.@advanced_settings[0].dry_run", Some(false), )?, }; let mut config = config; config.download_min_percent = Self::get::( "SQMA_DOWNLOAD_MIN_PERCENT", "sqm-autorate.@network[0].download_min_percent", Some(20.0), )? .clamp(1.0, 80.0); config.upload_min_percent = Self::get::( "SQMA_UPLOAD_MIN_PERCENT", "sqm-autorate.@network[0].upload_min_percent", Some(20.0), )? .clamp(1.0, 80.0); config.download_min_kbits = (config.download_base_kbits * config.download_min_percent / 100.0).floor(); config.upload_min_kbits = (config.upload_base_kbits * config.upload_min_percent / 100.0).floor(); Ok(config) } fn get(env_key: &str, uci_key: &str, default: Option) -> Result { match Self::get_value(env_key, uci_key) { Some(val) => match val.parse::() { Ok(parsed_val) => Ok(parsed_val), // Ran into an compilation error while trying to return the // error as-is, so using my own error type to indicate something went wrong while parsing Err(_) => Err(ConfigError::ParseError(env_key.to_string())), }, None => match default { Some(val) => Ok(val), None => Err(ConfigError::MissingValue(env_key.to_string())), }, } } fn get_bool(env_key: &str, uci_key: &str, default: Option) -> Result { Self::get::(env_key, uci_key, default.map(FlexiBool)).map(|f| f.0) } fn get_host_tag() -> String { // rustix can give us the hostname without shelling out rustix::system::uname() .nodename() .to_string_lossy() .into_owned() } fn get_optional(env_key: &str, uci_key: &str) -> Option { Self::get_value(env_key, uci_key).and_then(|val| val.parse::().ok()) } fn get_value(env_key: &str, uci_key: &str) -> Option { if let Ok(val) = env::var(env_key) { return Some(val); } if let Some(val) = Self::get_from_uci(uci_key) { return Some(val); } None } #[cfg(feature = "uci")] fn get_from_uci(key: &str) -> Option { let mut uci = match Uci::new() { Ok(val) => val, Err(e) => { warn!("Error opening UCI instance: {}", e); return None; } }; return match uci.get(key) { Ok(val) => Some(val), Err(e) => { warn!("Problem getting config from UCI: {}", e); None } }; } #[cfg(not(feature = "uci"))] fn get_from_uci(_: &str) -> Option { None } pub fn load_reflectors(&self) -> Result> { let lines = read_lines(self.reflector_list_file.clone()).map_err(|e| { ConfigError::ReflectorListNotFound( self.reflector_list_file.clone() + ": " + e.to_string().as_str(), ) })?; let mut reflectors: Vec = Vec::with_capacity(50); let mut first = true; for line in lines { if first { first = false; continue; } let line = line?; let columns: Vec<&str> = line.split(',').collect(); reflectors.push(IpAddr::from_str(columns[0])?); } Ok(reflectors) } }