From 4b9e510ab3fd50f2874e90a4b7072696ee91a87a Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Tue, 13 Oct 2026 00:34:50 +0700 Subject: [PATCH] feat(reflectors): refactor reflector configuration --- src/main.rs | 2 +- src/reflector_selector.rs | 8 +-- src/settings/mod.rs | 110 ++++++++++++++++++------------------- src/settings/validation.rs | 60 ++++++++++++++------ 4 files changed, 101 insertions(+), 79 deletions(-) diff --git a/src/main.rs b/src/main.rs index f252695..6d9bd1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,7 +148,7 @@ fn setup_reflectors(settings: &Settings) -> anyhow::Result { IpAddr::from_str("94.140.14.14")?, ]; - let reselection_enabled = configured_count > settings.advanced_settings.num_reflectors as usize; + let reselection_enabled = configured_count > settings.reflectors.num_reflectors as usize; let pool = if reselection_enabled { configured } else { diff --git a/src/reflector_selector.rs b/src/reflector_selector.rs index 92c1227..c38b979 100644 --- a/src/reflector_selector.rs +++ b/src/reflector_selector.rs @@ -159,10 +159,8 @@ impl ReflectorSelector { // After 40 reselections, slow down to every 15 minutes if reselection_count > 40 { - selector_sleep_time = Duration::new( - self.settings.advanced_settings.peer_reselection_time * 60, - 0, - ); + selector_sleep_time = + Duration::new(self.settings.reflectors.peer_reselection_time * 60, 0); } let mut next_peers: Vec = Vec::new(); @@ -222,7 +220,7 @@ impl ReflectorSelector { candidates.sort_by(|a, b| a.1.cmp(&b.1)); // Now we will just limit the candidates down to 2 * num_reflectors - let mut num_reflectors = self.settings.advanced_settings.num_reflectors; + let mut num_reflectors = self.settings.reflectors.num_reflectors; let candidate_pool_num = (2 * num_reflectors) as usize; candidates.truncate(candidate_pool_num); diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 2cc41c6..ec757fa 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -78,28 +78,32 @@ where deserializer.deserialize_any(FlexiBoolVisitor) } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum MeasurementType { - Icmp = 1, +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum ReflectorProtocol { + IcmpEcho = 1, IcmpTimestamps, + Irtt, } -impl Display for MeasurementType { +impl Display for ReflectorProtocol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - MeasurementType::Icmp => write!(f, "icmp"), - MeasurementType::IcmpTimestamps => write!(f, "icmp-timestamps"), + ReflectorProtocol::IcmpEcho => write!(f, "icmp"), + ReflectorProtocol::IcmpTimestamps => write!(f, "icmp-timestamps"), + ReflectorProtocol::Irtt => write!(f, "irtt"), } } } -impl FromStr for MeasurementType { +impl FromStr for ReflectorProtocol { 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), + "icmp" => Ok(ReflectorProtocol::IcmpEcho), + "icmp-timestamps" | "icmp_timestamps" => Ok(ReflectorProtocol::IcmpTimestamps), + "irtt" => Ok(ReflectorProtocol::Irtt), _ => Err(ConfigError::Message(format!( "invalid measurement type: {s}" ))), @@ -127,12 +131,13 @@ impl FromStr for ObservabilityProtocol { } } -#[derive(Debug, Deserialize)] -struct ReflectorRecord { - reflector_ip: IpAddr, - ip_version: u8, - #[allow(dead_code)] - description: String, +#[derive(Debug, Clone, Deserialize, UciSection)] +pub(crate) struct ReflectorDefinition { + pub(crate) host: String, + pub(crate) country: Option, + pub(crate) protocols: Vec, + pub(crate) irtt_port: Option, + pub(crate) irtt_hmac: Option, } #[derive(Clone, Debug, Deserialize, UciConfig)] @@ -141,6 +146,10 @@ pub(crate) struct Settings { #[uci(section)] pub(crate) network: NetworkSettings, + #[serde(default)] + #[uci(section)] + pub(crate) reflectors: ReflectorSettings, + #[serde(default)] #[uci(section)] pub(crate) output: OutputSettings, @@ -173,40 +182,8 @@ impl Settings { } pub fn load_reflectors(&self) -> anyhow::Result> { - let mut reader = csv::ReaderBuilder::new() - .has_headers(true) - .trim(csv::Trim::All) - .from_path(&self.advanced_settings.reflector_list_file) - .with_context(|| { - format!( - "failed to open reflector list: {}", - self.advanced_settings.reflector_list_file - ) - })?; - let mut reflectors = Vec::with_capacity(50); - for record in reader.deserialize::() { - let record = record.with_context(|| { - format!( - "failed to parse reflector list: {}", - self.advanced_settings.reflector_list_file - ) - })?; - - match (record.ip_version, record.reflector_ip) { - (4, IpAddr::V4(_)) => {} - (6, address @ IpAddr::V6(_)) => { - bail!("reflector {address} is IPv6; only IPv4 reflectors are supported"); - } - (version, address) => { - bail!("reflector {address} has inconsistent IP version {version}"); - } - } - - reflectors.push(record.reflector_ip); - } - Ok(reflectors) } } @@ -240,6 +217,35 @@ impl NetworkSettings { } } +#[derive(Clone, Debug, Deserialize, UciSection)] +#[serde(default)] +pub(crate) struct ReflectorSettings { + pub(crate) files: Vec, + pub(crate) countries: Vec, + + #[uci(skip)] + pub(crate) entries: Vec, + + #[uci(skip)] + pub(crate) bootstrap: Vec, + + pub(crate) num_reflectors: u8, + pub(crate) peer_reselection_time: u64, +} + +impl Default for ReflectorSettings { + fn default() -> Self { + Self { + files: vec!["/etc/sqm-autorate-rust/reflectors.csv".into()], + countries: Vec::new(), + entries: Vec::new(), + bootstrap: Vec::new(), + num_reflectors: 5, + peer_reselection_time: 15, + } + } +} + #[derive(Clone, Debug, Deserialize, UciSection)] #[serde(default)] pub(crate) struct OutputSettings { @@ -309,10 +315,7 @@ pub(crate) struct AdvancedSettings { 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) measurement_type: ReflectorProtocol, pub(crate) speed_hist_size: u32, pub(crate) tick_interval: f64, pub(crate) upload_delay_ms: f64, @@ -326,10 +329,7 @@ impl Default for AdvancedSettings { 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(), + measurement_type: ReflectorProtocol::IcmpTimestamps, speed_hist_size: 100, tick_interval: 0.5, upload_delay_ms: 15.0, diff --git a/src/settings/validation.rs b/src/settings/validation.rs index 14a54cd..b48e2af 100644 --- a/src/settings/validation.rs +++ b/src/settings/validation.rs @@ -3,6 +3,8 @@ // SPDX-License-Identifier: MPL-2.0 // +use crate::settings::ReflectorSettings; + use super::{AdvancedSettings, NetworkSettings, ObservabilitySettings, OutputSettings, Settings}; use std::error::Error; use std::fmt::{self, Display, Formatter}; @@ -88,6 +90,7 @@ impl Settings { let mut report = ValidationReport::default(); self.network.validate(&mut report); + self.reflectors.validate(&mut report); self.output.validate(&mut report); self.observability.validate(&mut report); self.advanced_settings.validate(&mut report); @@ -192,6 +195,45 @@ impl NetworkSettings { } } +impl ReflectorSettings { + fn validate(&self, report: &mut ValidationReport) { + if self.num_reflectors == 0 { + report.error("reflectors.num_reflectors", "must be greater than 0"); + } + + if self.peer_reselection_time == 0 { + report.error("reflectors.peer_reselection_time", "must be greater than 0"); + } + + if self.files.is_empty() && self.entries.is_empty() { + report.error( + "reflectors", + "at least one reflector file or inline entry is required", + ); + } + + for (index, file) in self.files.iter().enumerate() { + if file.trim().is_empty() { + report.error( + "reflectors.files", + format!("entry {index} must not be empty"), + ); + } + } + + for (index, country) in self.countries.iter().enumerate() { + if country.len() != 2 || !country.bytes().all(|c| c.is_ascii_alphabetic()) { + report.error( + "reflectors.countries", + format!( + "entry {index} ({country:?}) must be an ISO 3166-1 alpha-2 country code" + ), + ); + } + } + } +} + impl OutputSettings { fn validate(&self, report: &mut ValidationReport) { if self.suppress_statistics { @@ -298,24 +340,6 @@ impl AdvancedSettings { self.tick_interval, ); - if self.num_reflectors < 5 { - report.error( - "advanced_settings.num_reflectors", - format!("must be at least 5, got {}", self.num_reflectors,), - ); - } - - if self.peer_reselection_time == 0 { - report.error( - "advanced_settings.peer_reselection_time", - "must be greater than 0", - ); - } - - if self.reflector_list_file.trim().is_empty() { - report.error("advanced_settings.reflector_list_file", "must not be empty"); - } - if self.speed_hist_size == 0 { report.error( "advanced_settings.speed_hist_size",