feat(measurement): add configurable IRTT reflector measurements
Support optional IRTT measurements alongside ICMP with CSV and inline reflector definitions, repeated UCI registration, authentication, and endpoint-specific stream tracking. Keep the managed driver running during target acknowledgements and stop and join it on event-loop exits. Carry stream identity through baselines, selection, and rate control so endpoints sharing an IP remain distinct.
This commit is contained in:
1 parent
76a7e14835
commit
de77e5a281
12 files changed
+1022
-194
No files matched your search
+147
-62
@@ -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-echo"),
|
||||
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<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"icmp" => Ok(MeasurementType::Icmp),
|
||||
"icmp-timestamps" | "icmp_timestamps" => Ok(MeasurementType::IcmpTimestamps),
|
||||
"icmp-echo" => Ok(ReflectorProtocol::IcmpEcho),
|
||||
"icmp-timestamps" => Ok(ReflectorProtocol::IcmpTimestamps),
|
||||
"irtt" => Ok(ReflectorProtocol::Irtt),
|
||||
_ => Err(ConfigError::Message(format!(
|
||||
"invalid measurement type: {s}"
|
||||
))),
|
||||
@@ -127,12 +131,23 @@ impl FromStr for ObservabilityProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReflectorRecord {
|
||||
reflector_ip: IpAddr,
|
||||
ip_version: u8,
|
||||
#[derive(Debug, Clone, Deserialize, UciSection)]
|
||||
pub(crate) struct ReflectorDefinition {
|
||||
pub(crate) host: String,
|
||||
pub(crate) country: Option<String>,
|
||||
pub(crate) protocols: Vec<ReflectorProtocol>,
|
||||
pub(crate) irtt_port: Option<u16>,
|
||||
#[allow(dead_code)]
|
||||
description: String,
|
||||
pub(crate) irtt_hmac: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ReflectorRow {
|
||||
host: String,
|
||||
country: Option<String>,
|
||||
protocols: String,
|
||||
irtt_port: Option<u16>,
|
||||
irtt_hmac: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, UciConfig)]
|
||||
@@ -141,6 +156,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,
|
||||
@@ -159,7 +178,22 @@ impl Settings {
|
||||
let builder = Config::builder();
|
||||
|
||||
#[cfg(all(feature = "uci", unix))]
|
||||
let builder = builder.add_source(UciSource::from_schema::<Self>().required(false));
|
||||
let builder = builder.add_source(
|
||||
UciSource::from_schema::<Self>()
|
||||
.with_repeated_schema(&[
|
||||
uci_schema::UciRepeatedSectionMapping {
|
||||
uci_section: "reflector",
|
||||
config_path: "reflectors.entries",
|
||||
options: <ReflectorDefinition as uci_schema::UciSectionSchema>::UCI_OPTIONS,
|
||||
},
|
||||
uci_schema::UciRepeatedSectionMapping {
|
||||
uci_section: "bootstrap_reflector",
|
||||
config_path: "reflectors.bootstrap",
|
||||
options: <ReflectorDefinition as uci_schema::UciSectionSchema>::UCI_OPTIONS,
|
||||
},
|
||||
])
|
||||
.required(false),
|
||||
);
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
let builder = builder.add_source(
|
||||
@@ -167,47 +201,11 @@ impl Settings {
|
||||
);
|
||||
|
||||
let builder = builder.add_source(Environment::with_prefix("SQMA").separator("__"));
|
||||
let built_config = builder.build()?;
|
||||
let mut settings: Self = builder.build()?.try_deserialize()?;
|
||||
|
||||
built_config.try_deserialize()
|
||||
}
|
||||
settings.reflectors.load_files()?;
|
||||
|
||||
pub fn load_reflectors(&self) -> anyhow::Result<Vec<IpAddr>> {
|
||||
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::<ReflectorRecord>() {
|
||||
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)
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +238,99 @@ impl NetworkSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct ReflectorSettings {
|
||||
pub(crate) files: Vec<String>,
|
||||
pub(crate) countries: Vec<String>,
|
||||
|
||||
#[uci(skip)]
|
||||
pub(crate) entries: Vec<ReflectorDefinition>,
|
||||
|
||||
#[uci(skip)]
|
||||
pub(crate) bootstrap: Vec<ReflectorDefinition>,
|
||||
|
||||
pub(crate) num_reflectors: u8,
|
||||
pub(crate) peer_reselection_time: u64,
|
||||
}
|
||||
|
||||
impl ReflectorSettings {
|
||||
fn load_files(&mut self) -> Result<(), ConfigError> {
|
||||
for path in &self.files {
|
||||
let mut reader = csv::Reader::from_path(path)
|
||||
.map_err(|error| ConfigError::Message(error.to_string()))?;
|
||||
for row in reader.deserialize::<ReflectorRow>() {
|
||||
let row = row.map_err(|error| ConfigError::Message(error.to_string()))?;
|
||||
let protocols = row
|
||||
.protocols
|
||||
.split(['|', ';'])
|
||||
.map(str::trim)
|
||||
.filter(|protocol| !protocol.is_empty())
|
||||
.map(ReflectorProtocol::from_str)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if protocols.is_empty() {
|
||||
return Err(ConfigError::Message(format!(
|
||||
"reflector row in {path} has no protocols"
|
||||
)));
|
||||
}
|
||||
self.entries.push(ReflectorDefinition {
|
||||
host: row.host,
|
||||
country: row.country,
|
||||
protocols,
|
||||
irtt_port: row.irtt_port,
|
||||
irtt_hmac: row.irtt_hmac,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReflectorSettings {
|
||||
fn default() -> Self {
|
||||
let icmp = |host: &str| ReflectorDefinition {
|
||||
host: host.to_owned(),
|
||||
country: None,
|
||||
protocols: vec![
|
||||
ReflectorProtocol::IcmpEcho,
|
||||
ReflectorProtocol::IcmpTimestamps,
|
||||
],
|
||||
irtt_port: None,
|
||||
irtt_hmac: None,
|
||||
};
|
||||
|
||||
let irtt = |host: &str, country: Option<&str>| ReflectorDefinition {
|
||||
host: host.to_owned(),
|
||||
country: country.map(str::to_owned),
|
||||
protocols: vec![ReflectorProtocol::Irtt],
|
||||
irtt_port: Some(2112),
|
||||
irtt_hmac: None,
|
||||
};
|
||||
|
||||
Self {
|
||||
files: vec!["/etc/sqm-autorate-rust/reflectors.csv".into()],
|
||||
countries: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
bootstrap: vec![
|
||||
icmp("9.9.9.9"),
|
||||
icmp("8.238.120.14"),
|
||||
icmp("74.82.42.42"),
|
||||
icmp("194.242.2.2"),
|
||||
icmp("208.67.222.222"),
|
||||
icmp("94.140.14.14"),
|
||||
irtt("global.irtt.lochnair.net", None),
|
||||
irtt("uk-cov1.irtt.lochnair.net", Some("GB")),
|
||||
irtt("uk-cov2.irtt.lochnair.net", Some("GB")),
|
||||
irtt("de-fra.irtt.lochnair.net", Some("DE")),
|
||||
irtt("nl-ams1.irtt.lochnair.net", Some("NL")),
|
||||
],
|
||||
num_reflectors: 5,
|
||||
peer_reselection_time: 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct OutputSettings {
|
||||
@@ -309,10 +400,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 +414,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,
|
||||
|
||||
+42
-18
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user