feat(measurement): integrate IRTT and configurable reflector selection
Add protocol-aware reflector definitions, TOML/UCI lists, validation and environment overrides. Resolve and select endpoint-aware measurement streams, honor configured ICMP timing and socket routing, and retain partial-set reselection behavior from the standalone fixes. Drive the managed IRTT task throughout its lifecycle, retry terminal targets without restarting healthy sessions, normalize blank HMAC keys and use effective RTT for latency fallback. Fold the reviewed configuration, resolution and measurement corrections into their implementation.
This commit is contained in:
13 files changed
+1154
-198
No files matched your search
Generated
+117
@@ -60,6 +60,15 @@ version = "2.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@@ -123,6 +132,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmov"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "config"
|
name = "config"
|
||||||
version = "0.15.27"
|
version = "0.15.27"
|
||||||
@@ -135,6 +150,21 @@ dependencies = [
|
|||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "csv"
|
name = "csv"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -156,6 +186,27 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctutils"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||||
|
dependencies = [
|
||||||
|
"cmov",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
|
"crypto-common",
|
||||||
|
"ctutils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
@@ -242,6 +293,24 @@ version = "0.17.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hybrid-array"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icmp-socket2"
|
name = "icmp-socket2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -265,6 +334,31 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-client"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a59c24704bd94f19b84b2b0190aa9163efede499f8cf4eb02922443cfee08339"
|
||||||
|
dependencies = [
|
||||||
|
"irtt-proto",
|
||||||
|
"nix",
|
||||||
|
"socket2",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-proto"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a56ff307983b8f6477724e08cf726c5e24975b9affeb853d0830d91c60026ab2"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
"md-5",
|
||||||
|
"subtle",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -337,6 +431,16 @@ version = "0.4.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -679,6 +783,7 @@ dependencies = [
|
|||||||
"fastrand",
|
"fastrand",
|
||||||
"flume",
|
"flume",
|
||||||
"icmp-socket2",
|
"icmp-socket2",
|
||||||
|
"irtt-client",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"netlink-bindings",
|
"netlink-bindings",
|
||||||
@@ -697,6 +802,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85ec932792e62b2d3936163eacbd2e09984453bcd114f665c1e1694e36ff59c9"
|
checksum = "85ec932792e62b2d3936163eacbd2e09984453bcd114f665c1e1694e36ff59c9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.119"
|
||||||
@@ -836,6 +947,12 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uci-derive"
|
name = "uci-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
+3
-1
@@ -25,6 +25,7 @@ csv = "1.4.0"
|
|||||||
fastrand = "2"
|
fastrand = "2"
|
||||||
flume = { version = "0.12.0", default-features = false, features = ["async", "eventual-fairness", "select"] }
|
flume = { version = "0.12.0", default-features = false, features = ["async", "eventual-fairness", "select"] }
|
||||||
icmp-socket2 = { version = "0.6.4", features = ["tokio"] }
|
icmp-socket2 = { version = "0.6.4", features = ["tokio"] }
|
||||||
|
irtt-client = { version = "0.6.0", optional = true, features = ["ancillary", "tokio"] }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
rustix = { version = "1.1", features = ["fs", "system", "thread", "time"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -44,7 +45,8 @@ netlink-socket2 = { version = "0.3.5" }
|
|||||||
rust-uci = { git = "https://github.com/Lochnair/rust-uci.git", rev = "6835397f4f4ba8b69e75e727105186865c357417", optional = true, default-features = false }
|
rust-uci = { git = "https://github.com/Lochnair/rust-uci.git", rev = "6835397f4f4ba8b69e75e727105186865c357417", optional = true, default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["toml", "uci-vendored"]
|
default = ["irtt", "toml", "uci-vendored"]
|
||||||
|
irtt = ["dep:irtt-client"]
|
||||||
toml = ["config/toml"]
|
toml = ["config/toml"]
|
||||||
uci = ["dep:rust-uci"]
|
uci = ["dep:rust-uci"]
|
||||||
uci-vendored = ["uci", "rust-uci/vendored"]
|
uci-vendored = ["uci", "rust-uci/vendored"]
|
||||||
|
|||||||
+15
-12
@@ -6,13 +6,12 @@
|
|||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
use crate::measurement::model::{MeasurementEvent, MeasurementObservation};
|
use crate::measurement::model::{MeasurementEvent, MeasurementObservation, MeasurementStream};
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use flume::{Receiver, Sender};
|
use flume::{Receiver, Sender};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -72,7 +71,7 @@ impl ReflectorState {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ControlSnapshot {
|
pub struct ControlSnapshot {
|
||||||
pub generated_at: Instant,
|
pub generated_at: Instant,
|
||||||
pub reflectors: HashMap<IpAddr, ReflectorState>,
|
pub reflectors: HashMap<MeasurementStream, ReflectorState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Baseliner {
|
pub struct Baseliner {
|
||||||
@@ -121,7 +120,7 @@ fn update_reflector_state(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn process_reply(
|
fn process_reply(
|
||||||
reflectors: &mut HashMap<IpAddr, ReflectorState>,
|
reflectors: &mut HashMap<MeasurementStream, ReflectorState>,
|
||||||
reply: &MeasurementObservation,
|
reply: &MeasurementObservation,
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
slow_factor: f64,
|
slow_factor: f64,
|
||||||
@@ -129,7 +128,7 @@ fn process_reply(
|
|||||||
) -> (ReflectorState, bool) {
|
) -> (ReflectorState, bool) {
|
||||||
let sample = EwmaStats::from_reply(reply);
|
let sample = EwmaStats::from_reply(reply);
|
||||||
let state = reflectors
|
let state = reflectors
|
||||||
.entry(reply.stream.peer)
|
.entry(reply.stream)
|
||||||
.or_insert_with(|| ReflectorState::new(sample, reply.observed_at));
|
.or_insert_with(|| ReflectorState::new(sample, reply.observed_at));
|
||||||
let anomaly = update_reflector_state(
|
let anomaly = update_reflector_state(
|
||||||
state,
|
state,
|
||||||
@@ -144,7 +143,7 @@ fn process_reply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn control_snapshot(
|
fn control_snapshot(
|
||||||
reflectors: &HashMap<IpAddr, ReflectorState>,
|
reflectors: &HashMap<MeasurementStream, ReflectorState>,
|
||||||
generated_at: Instant,
|
generated_at: Instant,
|
||||||
) -> ControlSnapshot {
|
) -> ControlSnapshot {
|
||||||
ControlSnapshot {
|
ControlSnapshot {
|
||||||
@@ -155,7 +154,7 @@ fn control_snapshot(
|
|||||||
|
|
||||||
impl Baseliner {
|
impl Baseliner {
|
||||||
pub fn run(&self) -> anyhow::Result<()> {
|
pub fn run(&self) -> anyhow::Result<()> {
|
||||||
let mut reflectors = HashMap::<IpAddr, ReflectorState>::new();
|
let mut reflectors = HashMap::<MeasurementStream, ReflectorState>::new();
|
||||||
|
|
||||||
let slow_factor = ewma_factor(
|
let slow_factor = ewma_factor(
|
||||||
self.settings.advanced_settings.tick_interval,
|
self.settings.advanced_settings.tick_interval,
|
||||||
@@ -249,6 +248,7 @@ mod tests {
|
|||||||
MeasurementSource, MeasurementStream, MeasurementStreamId, OneWayClock, OneWayLatency,
|
MeasurementSource, MeasurementStream, MeasurementStreamId, OneWayClock, OneWayLatency,
|
||||||
SignedDuration,
|
SignedDuration,
|
||||||
};
|
};
|
||||||
|
use std::net::IpAddr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
fn state(
|
fn state(
|
||||||
@@ -289,6 +289,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
started_at: received_at,
|
started_at: received_at,
|
||||||
observed_at: received_at,
|
observed_at: received_at,
|
||||||
|
round_trip: None,
|
||||||
one_way: Some(OneWayLatency {
|
one_way: Some(OneWayLatency {
|
||||||
uplink: SignedDuration::from_millis(up_time as i64),
|
uplink: SignedDuration::from_millis(up_time as i64),
|
||||||
downlink: SignedDuration::from_millis(down_time as i64),
|
downlink: SignedDuration::from_millis(down_time as i64),
|
||||||
@@ -434,7 +435,8 @@ mod tests {
|
|||||||
let received_at = start + Duration::from_secs(1);
|
let received_at = start + Duration::from_secs(1);
|
||||||
let reflector = "192.0.2.1".parse().unwrap();
|
let reflector = "192.0.2.1".parse().unwrap();
|
||||||
let mut reflectors = HashMap::new();
|
let mut reflectors = HashMap::new();
|
||||||
reflectors.insert(reflector, state(100.0, 100.0, 100.0, 100.0, start));
|
let stream = reply(reflector, 0.0, 0.0, start).stream;
|
||||||
|
reflectors.insert(stream, state(100.0, 100.0, 100.0, 100.0, start));
|
||||||
|
|
||||||
let (updated, anomaly) = process_reply(
|
let (updated, anomaly) = process_reply(
|
||||||
&mut reflectors,
|
&mut reflectors,
|
||||||
@@ -450,7 +452,7 @@ mod tests {
|
|||||||
assert_close(updated.recent.down, 150.0);
|
assert_close(updated.recent.down, 150.0);
|
||||||
assert_close(updated.recent.up, 200.0);
|
assert_close(updated.recent.up, 200.0);
|
||||||
assert_eq!(updated.last_receive_at, received_at);
|
assert_eq!(updated.last_receive_at, received_at);
|
||||||
assert_close(reflectors[&reflector].recent.up, 200.0);
|
assert_close(reflectors[&stream].recent.up, 200.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -469,14 +471,15 @@ mod tests {
|
|||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
let snapshot = control_snapshot(&reflectors, generated_at);
|
let snapshot = control_snapshot(&reflectors, generated_at);
|
||||||
|
let stream = reply(reflector, 0.0, 0.0, received_at).stream;
|
||||||
|
|
||||||
assert!(!anomaly);
|
assert!(!anomaly);
|
||||||
assert_eq!(snapshot.generated_at, generated_at);
|
assert_eq!(snapshot.generated_at, generated_at);
|
||||||
assert_close(
|
assert_close(
|
||||||
snapshot.reflectors[&reflector].baseline.down,
|
snapshot.reflectors[&stream].baseline.down,
|
||||||
updated.baseline.down,
|
updated.baseline.down,
|
||||||
);
|
);
|
||||||
assert_close(snapshot.reflectors[&reflector].recent.up, updated.recent.up);
|
assert_close(snapshot.reflectors[&stream].recent.up, updated.recent.up);
|
||||||
assert_eq!(snapshot.reflectors[&reflector].last_receive_at, received_at);
|
assert_eq!(snapshot.reflectors[&stream].last_receive_at, received_at);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+89
-39
@@ -1,100 +1,150 @@
|
|||||||
use std::{collections::HashMap, net::IpAddr, time::Duration};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use flume::{Receiver, SendError, Sender};
|
use flume::{Receiver, SendError, Sender};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::measurement::{
|
#[cfg(feature = "irtt")]
|
||||||
|
use crate::measurement::irtt::IrttBinding;
|
||||||
|
use crate::{
|
||||||
|
measurement::{
|
||||||
icmp::IcmpBinding,
|
icmp::IcmpBinding,
|
||||||
model::{MeasurementSource, MeasurementStream, MeasurementStreamId, ProbeSchedule},
|
model::{MeasurementSource, MeasurementStream, MeasurementStreamId, ProbeSchedule},
|
||||||
|
},
|
||||||
|
settings::ReflectorDefinition,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ControllerError {
|
pub enum ControllerError {
|
||||||
#[error("ICMP channel error")]
|
#[error("ICMP channel error")]
|
||||||
IcmpChannelError(#[from] SendError<Vec<IcmpBinding>>),
|
IcmpChannelError(#[from] SendError<Vec<IcmpBinding>>),
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
#[error("IRTT channel error")]
|
||||||
|
IrttChannelError(#[from] SendError<Vec<IrttBinding>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
reflector_rx: Receiver<Vec<IpAddr>>,
|
reflector_rx: Receiver<Vec<SocketAddr>>,
|
||||||
icmp_binding_tx: Sender<Vec<IcmpBinding>>,
|
icmp_binding_tx: Sender<Vec<IcmpBinding>>,
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
irtt_binding_tx: Sender<Vec<IrttBinding>>,
|
||||||
|
reflectors: HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
active_streams: Arc<RwLock<Vec<MeasurementStream>>>,
|
||||||
|
endpoint_streams: Arc<RwLock<HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
source: MeasurementSource,
|
source: MeasurementSource,
|
||||||
period: Duration,
|
period: Duration,
|
||||||
|
|
||||||
next_stream_id: u64,
|
next_stream_id: u64,
|
||||||
stream_ids: HashMap<(IpAddr, MeasurementSource), MeasurementStreamId>,
|
stream_ids: HashMap<(SocketAddr, MeasurementSource), MeasurementStreamId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
reflector_rx: Receiver<Vec<IpAddr>>,
|
reflector_rx: Receiver<Vec<SocketAddr>>,
|
||||||
icmp_binding_tx: Sender<Vec<IcmpBinding>>,
|
icmp_binding_tx: Sender<Vec<IcmpBinding>>,
|
||||||
|
#[cfg(feature = "irtt")] irtt_binding_tx: Sender<Vec<IrttBinding>>,
|
||||||
|
reflectors: HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
active_streams: Arc<RwLock<Vec<MeasurementStream>>>,
|
||||||
|
endpoint_streams: Arc<RwLock<HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
|
source: MeasurementSource,
|
||||||
|
period: Duration,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
reflector_rx,
|
reflector_rx,
|
||||||
icmp_binding_tx,
|
icmp_binding_tx,
|
||||||
source: MeasurementSource::IcmpTimestamp,
|
#[cfg(feature = "irtt")]
|
||||||
period: Duration::from_millis(500),
|
irtt_binding_tx,
|
||||||
|
reflectors,
|
||||||
|
active_streams,
|
||||||
|
endpoint_streams,
|
||||||
|
source,
|
||||||
|
period,
|
||||||
next_stream_id: 0,
|
next_stream_id: 0,
|
||||||
stream_ids: HashMap::new(),
|
stream_ids: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(mut self, initial_reflectors: Vec<IpAddr>) -> Result<(), ControllerError> {
|
pub async fn run(mut self, initial_reflectors: Vec<SocketAddr>) -> Result<(), ControllerError> {
|
||||||
self.update_reflectors(initial_reflectors).await?;
|
self.update_reflectors(initial_reflectors).await?;
|
||||||
|
|
||||||
while let Ok(reflectors) = self.reflector_rx.recv_async().await {
|
while let Ok(reflectors) = self.reflector_rx.recv_async().await {
|
||||||
self.update_reflectors(reflectors).await?;
|
self.update_reflectors(reflectors).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_for(&mut self, peer: IpAddr) -> MeasurementStream {
|
fn stream_for(&mut self, endpoint: SocketAddr) -> MeasurementStream {
|
||||||
let key = (peer, self.source);
|
let key = (endpoint, self.source);
|
||||||
|
|
||||||
let id = *self.stream_ids.entry(key).or_insert_with(|| {
|
let id = *self.stream_ids.entry(key).or_insert_with(|| {
|
||||||
let id = MeasurementStreamId(self.next_stream_id);
|
let id = MeasurementStreamId(self.next_stream_id);
|
||||||
self.next_stream_id += 1;
|
self.next_stream_id += 1;
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
|
|
||||||
MeasurementStream {
|
MeasurementStream {
|
||||||
id,
|
id,
|
||||||
peer,
|
peer: endpoint.ip(),
|
||||||
source: self.source,
|
source: self.source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_reflectors(&mut self, reflectors: Vec<IpAddr>) -> Result<(), ControllerError> {
|
async fn update_reflectors(
|
||||||
let count = reflectors.len();
|
&mut self,
|
||||||
|
endpoints: Vec<SocketAddr>,
|
||||||
let bindings = reflectors
|
) -> Result<(), ControllerError> {
|
||||||
.into_iter()
|
let count = endpoints.len();
|
||||||
.enumerate()
|
let mut icmp = Vec::new();
|
||||||
.map(|(index, peer)| {
|
#[cfg(feature = "irtt")]
|
||||||
let stream = self.stream_for(peer);
|
let mut irtt = Vec::new();
|
||||||
|
let mut streams = Vec::with_capacity(count);
|
||||||
IcmpBinding::new(
|
let mut endpoint_streams = HashMap::new();
|
||||||
stream,
|
for (index, endpoint) in endpoints.into_iter().enumerate() {
|
||||||
ProbeSchedule {
|
let Some(_definition) = self.reflectors.get(&endpoint) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
let (port, hmac) = {
|
||||||
|
(
|
||||||
|
_definition.irtt_port.unwrap_or(2112),
|
||||||
|
_definition.irtt_hmac.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let stream = self.stream_for(endpoint);
|
||||||
|
let schedule = ProbeSchedule {
|
||||||
period: self.period,
|
period: self.period,
|
||||||
offset: evenly_spaced_offset(self.period, index, count),
|
offset: evenly_spaced_offset(self.period, index, count),
|
||||||
},
|
};
|
||||||
)
|
match self.source {
|
||||||
})
|
MeasurementSource::IcmpEcho | MeasurementSource::IcmpTimestamp => {
|
||||||
.collect();
|
icmp.push(IcmpBinding::new(stream, schedule))
|
||||||
|
}
|
||||||
self.icmp_binding_tx
|
#[cfg(feature = "irtt")]
|
||||||
.send_async(bindings)
|
MeasurementSource::Irtt => irtt.push(IrttBinding::new(stream, port, hmac)),
|
||||||
.await
|
#[cfg(not(feature = "irtt"))]
|
||||||
.map_err(|e| ControllerError::IcmpChannelError(e))
|
MeasurementSource::Irtt => unreachable!("IRTT feature is checked before startup"),
|
||||||
|
_ => unreachable!("unsupported reflector measurement source"),
|
||||||
|
}
|
||||||
|
streams.push(stream);
|
||||||
|
endpoint_streams.insert(endpoint, stream);
|
||||||
|
}
|
||||||
|
*self
|
||||||
|
.active_streams
|
||||||
|
.write()
|
||||||
|
.expect("active stream lock poisoned") = streams;
|
||||||
|
*self
|
||||||
|
.endpoint_streams
|
||||||
|
.write()
|
||||||
|
.expect("endpoint stream lock poisoned") = endpoint_streams;
|
||||||
|
self.icmp_binding_tx.send_async(icmp).await?;
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
self.irtt_binding_tx.send_async(irtt).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evenly_spaced_offset(period: Duration, index: usize, count: usize) -> Duration {
|
fn evenly_spaced_offset(period: Duration, index: usize, count: usize) -> Duration {
|
||||||
let nanos = period.as_nanos() * index as u128 / count as u128;
|
let nanos = period.as_nanos() * index as u128 / count as u128;
|
||||||
|
|
||||||
Duration::new(
|
Duration::new(
|
||||||
(nanos / 1_000_000_000) as u64,
|
(nanos / 1_000_000_000) as u64,
|
||||||
(nanos % 1_000_000_000) as u32,
|
(nanos % 1_000_000_000) as u32,
|
||||||
|
|||||||
+170
-28
@@ -22,16 +22,19 @@ mod util;
|
|||||||
use crate::baseliner::Baseliner;
|
use crate::baseliner::Baseliner;
|
||||||
use crate::controller::Controller;
|
use crate::controller::Controller;
|
||||||
use crate::measurement::icmp::IcmpEngine;
|
use crate::measurement::icmp::IcmpEngine;
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
use crate::measurement::irtt::IrttEngine;
|
||||||
|
use crate::measurement::model::{MeasurementSource, MeasurementStream};
|
||||||
use crate::metrics::{Metric, Metrics, MetricsSender};
|
use crate::metrics::{Metric, Metrics, MetricsSender};
|
||||||
use crate::platform::{TrafficControlBackend, traffic_control_backend, warn_platform_limitations};
|
use crate::platform::{TrafficControlBackend, traffic_control_backend, warn_platform_limitations};
|
||||||
use crate::ratecontroller::Ratecontroller;
|
use crate::ratecontroller::Ratecontroller;
|
||||||
use crate::reflector_selector::ReflectorSelector;
|
use crate::reflector_selector::ReflectorSelector;
|
||||||
use crate::settings::Settings;
|
use crate::settings::{ReflectorDefinition, ReflectorProtocol, Settings};
|
||||||
use crate::util::RwLockExt;
|
use crate::util::RwLockExt;
|
||||||
use ::log::{info, warn};
|
use ::log::{info, warn};
|
||||||
use flume::RecvTimeoutError;
|
use flume::RecvTimeoutError;
|
||||||
use std::net::IpAddr;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -39,8 +42,11 @@ use std::thread::{JoinHandle, sleep};
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
struct ReflectorSetup {
|
struct ReflectorSetup {
|
||||||
peers: Arc<RwLock<Vec<IpAddr>>>,
|
peers: Arc<RwLock<Vec<SocketAddr>>>,
|
||||||
pool: Vec<IpAddr>,
|
active_streams: Arc<RwLock<Vec<MeasurementStream>>>,
|
||||||
|
endpoint_streams: Arc<RwLock<HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
|
pool: Vec<SocketAddr>,
|
||||||
|
reflectors: HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
reselection_enabled: bool,
|
reselection_enabled: bool,
|
||||||
}
|
}
|
||||||
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
|
pub static SHUTDOWN: AtomicBool = AtomicBool::new(false);
|
||||||
@@ -134,29 +140,126 @@ fn restore_shaper<T: TrafficControlBackend>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_reflectors(settings: &Settings) -> anyhow::Result<ReflectorSetup> {
|
fn resolve_reflectors(
|
||||||
let configured = settings.load_reflectors()?;
|
definitions: &[ReflectorDefinition],
|
||||||
let configured_count = configured.len();
|
protocol: ReflectorProtocol,
|
||||||
|
countries: &[String],
|
||||||
let default_reflectors = [
|
filter_country: bool,
|
||||||
IpAddr::from_str("9.9.9.9")?,
|
) -> anyhow::Result<HashMap<SocketAddr, ReflectorDefinition>> {
|
||||||
IpAddr::from_str("8.238.120.14")?,
|
let mut resolved = HashMap::new();
|
||||||
IpAddr::from_str("74.82.42.42")?,
|
for definition in definitions {
|
||||||
IpAddr::from_str("194.242.2.2")?,
|
if !definition.protocols.contains(&protocol) {
|
||||||
IpAddr::from_str("208.67.222.222")?,
|
continue;
|
||||||
IpAddr::from_str("94.140.14.14")?,
|
}
|
||||||
];
|
if protocol != ReflectorProtocol::Irtt
|
||||||
|
&& definition
|
||||||
let reselection_enabled = configured_count > settings.advanced_settings.num_reflectors as usize;
|
.host
|
||||||
let pool = if reselection_enabled {
|
.parse::<IpAddr>()
|
||||||
configured
|
.is_ok_and(|ip| ip.is_ipv6())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if filter_country
|
||||||
|
&& !countries.is_empty()
|
||||||
|
&& definition.country.as_ref().is_some_and(|country| {
|
||||||
|
!countries
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| allowed.eq_ignore_ascii_case(country))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let port = if protocol == ReflectorProtocol::Irtt {
|
||||||
|
definition.irtt_port.unwrap_or(2112)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
0
|
||||||
};
|
};
|
||||||
|
let endpoints = match (definition.host.as_str(), port).to_socket_addrs() {
|
||||||
|
Ok(endpoints) => endpoints,
|
||||||
|
Err(error) => {
|
||||||
|
warn!("Failed to resolve reflector {}: {error}", definition.host);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for endpoint in endpoints {
|
||||||
|
if protocol != ReflectorProtocol::Irtt && endpoint.is_ipv6() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
insert_reflector(&mut resolved, endpoint, definition.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_reflector(
|
||||||
|
reflectors: &mut HashMap<SocketAddr, ReflectorDefinition>,
|
||||||
|
endpoint: SocketAddr,
|
||||||
|
definition: ReflectorDefinition,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let Some(existing) = reflectors.get_mut(&endpoint) {
|
||||||
|
let same_country = match (&existing.country, &definition.country) {
|
||||||
|
(Some(a), Some(b)) => a.eq_ignore_ascii_case(b),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
let same_protocols = existing
|
||||||
|
.protocols
|
||||||
|
.iter()
|
||||||
|
.all(|p| definition.protocols.contains(p))
|
||||||
|
&& definition
|
||||||
|
.protocols
|
||||||
|
.iter()
|
||||||
|
.all(|p| existing.protocols.contains(p));
|
||||||
|
if !same_country
|
||||||
|
|| !same_protocols
|
||||||
|
|| existing.irtt_port.unwrap_or(2112) != definition.irtt_port.unwrap_or(2112)
|
||||||
|
|| existing.irtt_hmac != definition.irtt_hmac
|
||||||
|
{
|
||||||
|
anyhow::bail!("conflicting reflector definitions for resolved endpoint {endpoint}");
|
||||||
|
}
|
||||||
|
if existing.country.is_none() {
|
||||||
|
existing.country = definition.country;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reflectors.insert(endpoint, definition);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_reflectors(settings: &Settings) -> anyhow::Result<ReflectorSetup> {
|
||||||
|
let protocol = settings.advanced_settings.measurement_type;
|
||||||
|
let configured = resolve_reflectors(
|
||||||
|
&settings.reflectors.entries,
|
||||||
|
protocol,
|
||||||
|
&settings.reflectors.countries,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
let bootstrap = resolve_reflectors(&settings.reflectors.bootstrap, protocol, &[], false)?;
|
||||||
|
let mut reflectors = bootstrap.clone();
|
||||||
|
for (endpoint, definition) in &configured {
|
||||||
|
insert_reflector(&mut reflectors, *endpoint, definition.clone())?;
|
||||||
|
}
|
||||||
|
if reflectors.is_empty() {
|
||||||
|
anyhow::bail!("no usable reflectors for measurement protocol {protocol}");
|
||||||
|
}
|
||||||
|
let mut pool = configured.keys().copied().collect::<Vec<_>>();
|
||||||
|
pool.sort();
|
||||||
|
let mut initial = bootstrap.keys().copied().collect::<Vec<_>>();
|
||||||
|
initial.sort();
|
||||||
|
if initial.is_empty() {
|
||||||
|
initial = pool
|
||||||
|
.iter()
|
||||||
|
.take(settings.reflectors.num_reflectors as usize)
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
let reselection_enabled = pool.len() > settings.reflectors.num_reflectors as usize;
|
||||||
|
|
||||||
Ok(ReflectorSetup {
|
Ok(ReflectorSetup {
|
||||||
peers: Arc::new(RwLock::new(default_reflectors.to_vec())),
|
peers: Arc::new(RwLock::new(initial)),
|
||||||
|
active_streams: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
endpoint_streams: Arc::new(RwLock::new(HashMap::new())),
|
||||||
pool,
|
pool,
|
||||||
|
reflectors,
|
||||||
reselection_enabled,
|
reselection_enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -219,6 +322,12 @@ fn wait_for_exit(error_rx: &flume::Receiver<anyhow::Error>) -> anyhow::Result<()
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(settings: &Settings) -> anyhow::Result<()> {
|
fn run(settings: &Settings) -> anyhow::Result<()> {
|
||||||
|
#[cfg(not(feature = "irtt"))]
|
||||||
|
if settings.advanced_settings.measurement_type == ReflectorProtocol::Irtt {
|
||||||
|
anyhow::bail!(
|
||||||
|
"IRTT measurement selected, but this binary was built without the `irtt` feature"
|
||||||
|
);
|
||||||
|
}
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
|
||||||
if settings.advanced_settings.dry_run {
|
if settings.advanced_settings.dry_run {
|
||||||
@@ -229,13 +338,18 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let ReflectorSetup {
|
let ReflectorSetup {
|
||||||
peers: reflector_peers,
|
peers: reflector_peers,
|
||||||
|
active_streams,
|
||||||
|
endpoint_streams,
|
||||||
pool: reflector_pool,
|
pool: reflector_pool,
|
||||||
|
reflectors,
|
||||||
reselection_enabled,
|
reselection_enabled,
|
||||||
} = setup_reflectors(settings)?;
|
} = setup_reflectors(settings)?;
|
||||||
|
|
||||||
let (control_snapshot_tx, control_snapshot_rx) = flume::unbounded();
|
let (control_snapshot_tx, control_snapshot_rx) = flume::unbounded();
|
||||||
let (error_tx, error_rx) = flume::unbounded::<anyhow::Error>();
|
let (error_tx, error_rx) = flume::unbounded::<anyhow::Error>();
|
||||||
let (icmp_binding_tx, icmp_binding_rx) = flume::unbounded();
|
let (icmp_binding_tx, icmp_binding_rx) = flume::unbounded();
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
let (irtt_binding_tx, irtt_binding_rx) = flume::unbounded();
|
||||||
let (measurement_tx, measurement_rx) = flume::unbounded();
|
let (measurement_tx, measurement_rx) = flume::unbounded();
|
||||||
let (reflector_tx, reflector_rx) = flume::unbounded();
|
let (reflector_tx, reflector_rx) = flume::unbounded();
|
||||||
let (reselect_tx, reselect_rx) = flume::bounded(1);
|
let (reselect_tx, reselect_rx) = flume::bounded(1);
|
||||||
@@ -246,18 +360,45 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let controller = Controller::new(reflector_rx, icmp_binding_tx);
|
let source = match settings.advanced_settings.measurement_type {
|
||||||
let engine = {
|
ReflectorProtocol::IcmpEcho => MeasurementSource::IcmpEcho,
|
||||||
|
ReflectorProtocol::IcmpTimestamps => MeasurementSource::IcmpTimestamp,
|
||||||
|
ReflectorProtocol::Irtt => MeasurementSource::Irtt,
|
||||||
|
};
|
||||||
|
let controller = Controller::new(
|
||||||
|
reflector_rx,
|
||||||
|
icmp_binding_tx,
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
irtt_binding_tx,
|
||||||
|
reflectors,
|
||||||
|
Arc::clone(&active_streams),
|
||||||
|
Arc::clone(&endpoint_streams),
|
||||||
|
source,
|
||||||
|
Duration::from_secs_f64(settings.advanced_settings.tick_interval),
|
||||||
|
);
|
||||||
|
let icmp_engine = {
|
||||||
let _guard = rt.enter();
|
let _guard = rt.enter();
|
||||||
IcmpEngine::new(
|
IcmpEngine::new(
|
||||||
start_time,
|
start_time,
|
||||||
&settings.network,
|
&settings.network,
|
||||||
measurement_tx,
|
measurement_tx.clone(),
|
||||||
icmp_binding_rx,
|
icmp_binding_rx,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
let (irtt_task, irtt_engine) = {
|
||||||
|
let _guard = rt.enter();
|
||||||
|
IrttEngine::new(
|
||||||
|
Duration::from_secs_f64(settings.advanced_settings.tick_interval),
|
||||||
|
&settings.network,
|
||||||
|
measurement_tx,
|
||||||
|
irtt_binding_rx,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
spawn_task(&rt, &error_tx, engine.run());
|
spawn_task(&rt, &error_tx, icmp_engine.run());
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
spawn_task(&rt, &error_tx, irtt_engine.run(irtt_task));
|
||||||
|
|
||||||
let dropped = Arc::new(AtomicU32::new(0));
|
let dropped = Arc::new(AtomicU32::new(0));
|
||||||
|
|
||||||
@@ -329,6 +470,7 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
|
|||||||
.expect("reselection snapshot receiver must exist when reselection is enabled"),
|
.expect("reselection snapshot receiver must exist when reselection is enabled"),
|
||||||
reflector_peers_lock: Arc::clone(&reflector_peers),
|
reflector_peers_lock: Arc::clone(&reflector_peers),
|
||||||
reflector_pool,
|
reflector_pool,
|
||||||
|
endpoint_streams,
|
||||||
trigger_channel: reselect_rx,
|
trigger_channel: reselect_rx,
|
||||||
metrics: event_metrics,
|
metrics: event_metrics,
|
||||||
reflector_tx,
|
reflector_tx,
|
||||||
@@ -342,7 +484,7 @@ fn run(settings: &Settings) -> anyhow::Result<()> {
|
|||||||
let mut ratecontroller = Ratecontroller::new(
|
let mut ratecontroller = Ratecontroller::new(
|
||||||
settings.clone(),
|
settings.clone(),
|
||||||
control_snapshot_rx,
|
control_snapshot_rx,
|
||||||
reflector_peers,
|
active_streams,
|
||||||
reselect_tx,
|
reselect_tx,
|
||||||
rate_metrics,
|
rate_metrics,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ impl IcmpEngine {
|
|||||||
stream: probe.stream,
|
stream: probe.stream,
|
||||||
started_at: probe.sent_at,
|
started_at: probe.sent_at,
|
||||||
observed_at,
|
observed_at,
|
||||||
|
round_trip: None,
|
||||||
one_way: None,
|
one_way: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -385,6 +386,7 @@ impl IcmpEngine {
|
|||||||
stream: probe.stream,
|
stream: probe.stream,
|
||||||
started_at: probe.sent_at,
|
started_at: probe.sent_at,
|
||||||
observed_at,
|
observed_at,
|
||||||
|
round_trip: None,
|
||||||
one_way: Some(OneWayLatency {
|
one_way: Some(OneWayLatency {
|
||||||
uplink: SignedDuration::from_millis(uplink_ms),
|
uplink: SignedDuration::from_millis(uplink_ms),
|
||||||
downlink: SignedDuration::from_millis(downlink_ms),
|
downlink: SignedDuration::from_millis(downlink_ms),
|
||||||
|
|||||||
@@ -0,0 +1,418 @@
|
|||||||
|
use std::{collections::HashMap, net::SocketAddr};
|
||||||
|
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use irtt_client::{
|
||||||
|
ClientConfig, ClientEvent, NegotiationPolicy, OpenPolicy,
|
||||||
|
managed::{
|
||||||
|
ManagedClient, ManagedClientConfig, ManagedClientHandle, ManagedClientTask,
|
||||||
|
ManagedCommandApplyError, ManagedCommandError, ManagedCompletionPolicy, ManagedConfigError,
|
||||||
|
ManagedEndReason, ManagedEvent, ManagedLifecycle, ManagedSubscribeError,
|
||||||
|
ManagedTargetConfig, ManagedTargetEndReason, ManagedTargetLifecycle, TargetAuth, TargetId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use log::{debug, trace, warn};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::sync::broadcast::error::RecvError;
|
||||||
|
|
||||||
|
use crate::SHUTDOWN;
|
||||||
|
use crate::measurement::model::{
|
||||||
|
MeasurementEvent, MeasurementLoss, MeasurementObservation, MeasurementStream,
|
||||||
|
MeasurementStreamId, OneWayClock, OneWayLatency, SignedDuration,
|
||||||
|
};
|
||||||
|
use crate::settings::NetworkSettings;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IrttError {
|
||||||
|
#[error("failed to build IRTT config")]
|
||||||
|
InvalidConfig(#[from] ManagedConfigError),
|
||||||
|
|
||||||
|
#[error("event channel closed")]
|
||||||
|
EventChannelClosed,
|
||||||
|
|
||||||
|
#[error("binding channel closed")]
|
||||||
|
BindingChannelClosed,
|
||||||
|
|
||||||
|
#[error("error while reciving on channel")]
|
||||||
|
ChannelRecvError(#[from] RecvError),
|
||||||
|
|
||||||
|
#[error("unable to subscribe to irtt events")]
|
||||||
|
SubscriptionError(#[from] ManagedSubscribeError),
|
||||||
|
|
||||||
|
#[error("failed to submit IRTT target update")]
|
||||||
|
CommandError(#[from] ManagedCommandError),
|
||||||
|
|
||||||
|
#[error("failed to apply IRTT target update")]
|
||||||
|
CommandApplyError(#[from] ManagedCommandApplyError),
|
||||||
|
|
||||||
|
#[error("IRTT managed client failed: {0:?}")]
|
||||||
|
ManagedFailure(ManagedEndReason),
|
||||||
|
|
||||||
|
#[error("IRTT managed task was abandoned")]
|
||||||
|
ManagedAbandoned,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IrttBinding {
|
||||||
|
stream: MeasurementStream,
|
||||||
|
port: u16,
|
||||||
|
hmac: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IrttBinding {
|
||||||
|
pub fn new(stream: MeasurementStream, port: u16, hmac: Option<String>) -> Self {
|
||||||
|
Self { stream, port, hmac }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IrttEngine {
|
||||||
|
client: ManagedClientHandle,
|
||||||
|
|
||||||
|
bindings: HashMap<MeasurementStreamId, IrttBinding>,
|
||||||
|
targets: Vec<ManagedTargetConfig>,
|
||||||
|
|
||||||
|
binding_rx: Receiver<Vec<IrttBinding>>,
|
||||||
|
observation_tx: Sender<MeasurementEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IrttEngine {
|
||||||
|
pub fn new(
|
||||||
|
interval: std::time::Duration,
|
||||||
|
settings: &NetworkSettings,
|
||||||
|
observation_tx: Sender<MeasurementEvent>,
|
||||||
|
binding_rx: Receiver<Vec<IrttBinding>>,
|
||||||
|
) -> Result<(ManagedClientTask, Self), IrttError> {
|
||||||
|
let config = managed_client_config(interval, settings);
|
||||||
|
let (task, handle) = ManagedClient::task(config, Vec::new())?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
task,
|
||||||
|
Self {
|
||||||
|
client: handle,
|
||||||
|
bindings: HashMap::new(),
|
||||||
|
targets: Vec::new(),
|
||||||
|
binding_rx,
|
||||||
|
observation_tx,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_bindings(&mut self, bindings: Vec<IrttBinding>) -> Result<(), IrttError> {
|
||||||
|
let targets = bindings
|
||||||
|
.iter()
|
||||||
|
.map(|binding| {
|
||||||
|
let mut target = ManagedTargetConfig::new(
|
||||||
|
TargetId::new(binding.stream.id.0.to_string()),
|
||||||
|
SocketAddr::new(binding.stream.peer, binding.port).to_string(),
|
||||||
|
);
|
||||||
|
if let Some(key) = binding.hmac.as_ref().filter(|key| !key.trim().is_empty()) {
|
||||||
|
target.auth = TargetAuth::Override(irtt_client::Authentication::Hmac(
|
||||||
|
irtt_client::HmacKey::new(key.as_bytes().to_vec().into_boxed_slice()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
target
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.client.update_targets(targets.clone())?.await?;
|
||||||
|
self.targets = targets;
|
||||||
|
|
||||||
|
self.bindings = bindings
|
||||||
|
.into_iter()
|
||||||
|
.map(|binding| (binding.stream.id, binding))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(mut self, task: ManagedClientTask) -> Result<(), IrttError> {
|
||||||
|
let mut task = Box::pin(task);
|
||||||
|
let client = self.client.clone();
|
||||||
|
|
||||||
|
// Keep driving the client even while the event loop awaits command receipts.
|
||||||
|
let result = tokio::select! {
|
||||||
|
biased;
|
||||||
|
outcome = task.as_mut() => {
|
||||||
|
return match outcome.end_reason {
|
||||||
|
ManagedEndReason::StopRequested => Ok(()),
|
||||||
|
end_reason => Err(IrttError::ManagedFailure(end_reason)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
result = self.run_events() => result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every event-loop exit stops and joins the driver, including channel errors.
|
||||||
|
let stop = client.stop();
|
||||||
|
let (outcome, _) = tokio::join!(task, stop);
|
||||||
|
result?;
|
||||||
|
match outcome.end_reason {
|
||||||
|
ManagedEndReason::StopRequested => Ok(()),
|
||||||
|
end_reason => Err(IrttError::ManagedFailure(end_reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_events(&mut self) -> Result<(), IrttError> {
|
||||||
|
let mut events = self.client.subscribe()?;
|
||||||
|
// Durable status catches failures even if their presentation events were lost.
|
||||||
|
// Resubmitting the full desired set restarts terminal targets only.
|
||||||
|
let mut retry = tokio::time::interval(std::time::Duration::from_secs(5));
|
||||||
|
retry.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||||
|
retry.tick().await;
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
bindings = self.binding_rx.recv_async() => {
|
||||||
|
let bindings = bindings
|
||||||
|
.map_err(|_| IrttError::BindingChannelClosed)?;
|
||||||
|
|
||||||
|
self.set_bindings(bindings).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = retry.tick() => {
|
||||||
|
if SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.client.status().targets.iter().any(|target| {
|
||||||
|
target.desired && target.lifecycle == ManagedTargetLifecycle::Terminal
|
||||||
|
}) {
|
||||||
|
self.client.update_targets(self.targets.clone())?.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event = events.recv() => {
|
||||||
|
let event = match event {
|
||||||
|
Ok(event) => event,
|
||||||
|
|
||||||
|
Err(RecvError::Lagged(n)) => {
|
||||||
|
warn!("IRTT event receiver dropped {n} events");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(RecvError::Closed) => {
|
||||||
|
let status = self.client.status();
|
||||||
|
|
||||||
|
match status.lifecycle {
|
||||||
|
ManagedLifecycle::Completed => return Ok(()),
|
||||||
|
|
||||||
|
ManagedLifecycle::Failed => {
|
||||||
|
if let Some(outcome) = &status.final_outcome {
|
||||||
|
return Err(IrttError::ManagedFailure(
|
||||||
|
outcome.end_reason.clone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(IrttError::ManagedAbandoned);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => return Err(IrttError::ManagedAbandoned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
ManagedEvent::Started => {
|
||||||
|
debug!("IRTT managed client started");
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::TargetStateChanged {
|
||||||
|
target,
|
||||||
|
lifecycle,
|
||||||
|
} => {
|
||||||
|
debug!("IRTT target {target:?}: {lifecycle:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Client { target, event } => {
|
||||||
|
let stream = target.id.as_str()
|
||||||
|
.parse::<u64>()
|
||||||
|
.ok()
|
||||||
|
.and_then(|id| {
|
||||||
|
self.bindings.get(&MeasurementStreamId(id))
|
||||||
|
})
|
||||||
|
.map(|binding| binding.stream);
|
||||||
|
|
||||||
|
match event {
|
||||||
|
ClientEvent::SessionStarted(session) => {
|
||||||
|
debug!(
|
||||||
|
"IRTT session started for {}: {}",
|
||||||
|
target.id,
|
||||||
|
session.remote
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::NoTestCompleted(outcome) => {
|
||||||
|
debug!(
|
||||||
|
"IRTT no-test completed for {}: {}",
|
||||||
|
target.id,
|
||||||
|
outcome.remote
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::SessionClosed { .. } => {
|
||||||
|
debug!("IRTT session closed for {}", target.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::EchoSent { seq, .. } => {
|
||||||
|
trace!("IRTT {} sent probe {seq}", target.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::EchoReply {
|
||||||
|
sent_at,
|
||||||
|
received_at,
|
||||||
|
one_way,
|
||||||
|
rtt,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let Some(stream) = stream else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let observation = MeasurementObservation {
|
||||||
|
stream,
|
||||||
|
started_at: sent_at.mono,
|
||||||
|
observed_at: received_at.mono,
|
||||||
|
round_trip: i64::try_from(rtt.effective.as_nanos())
|
||||||
|
.ok().map(SignedDuration::from_nanos),
|
||||||
|
one_way: one_way.and_then(map_one_way),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.observation_tx
|
||||||
|
.send_async(MeasurementEvent::Observation(observation))
|
||||||
|
.await
|
||||||
|
.map_err(|_| IrttError::EventChannelClosed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::EchoLoss {
|
||||||
|
sent_at,
|
||||||
|
timeout_at,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let Some(stream) = stream else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let loss = MeasurementLoss {
|
||||||
|
stream,
|
||||||
|
started_at: sent_at.mono,
|
||||||
|
detected_at: timeout_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.observation_tx
|
||||||
|
.send_async(MeasurementEvent::Loss(loss))
|
||||||
|
.await
|
||||||
|
.map_err(|_| IrttError::EventChannelClosed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::DuplicateReply { seq, .. } => {
|
||||||
|
trace!(
|
||||||
|
"IRTT {} duplicate reply {seq}",
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::LateReply { seq, .. } => {
|
||||||
|
trace!(
|
||||||
|
"IRTT {} late reply {seq}",
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEvent::Warning { kind, message, .. } => {
|
||||||
|
debug!(
|
||||||
|
"IRTT {} warning ({kind:?}): {message}",
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::TargetFinished { outcome } => {
|
||||||
|
match &outcome.end_reason {
|
||||||
|
ManagedTargetEndReason::Failed(failure) => {
|
||||||
|
warn!(
|
||||||
|
"IRTT target {} failed: {failure:?}",
|
||||||
|
outcome.target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reason => {
|
||||||
|
debug!(
|
||||||
|
"IRTT target {} finished: {reason:?}",
|
||||||
|
outcome.target.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Stopping => {
|
||||||
|
debug!("IRTT managed client stopping");
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Completed { outcome } => {
|
||||||
|
debug!(
|
||||||
|
"IRTT managed client completed: {:?}",
|
||||||
|
outcome.end_reason
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Failed { outcome } => {
|
||||||
|
return Err(IrttError::ManagedFailure(
|
||||||
|
outcome.end_reason.clone()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ManagedEvent::Abandoned => {
|
||||||
|
return Err(IrttError::ManagedAbandoned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
|
||||||
|
if SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn managed_client_config(
|
||||||
|
interval: std::time::Duration,
|
||||||
|
#[allow(unused)] settings: &NetworkSettings,
|
||||||
|
) -> ManagedClientConfig {
|
||||||
|
ManagedClientConfig {
|
||||||
|
client: ClientConfig {
|
||||||
|
socket: irtt_client::SocketConfig {
|
||||||
|
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||||
|
bind_to_device: settings.measurement_bind_device.clone(),
|
||||||
|
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||||
|
mark: settings.measurement_mark,
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
fib: settings.measurement_fib,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
request: irtt_client::SessionRequest {
|
||||||
|
duration: None,
|
||||||
|
interval,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
open: OpenPolicy {
|
||||||
|
negotiation: NegotiationPolicy::Strict,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
completion: ManagedCompletionPolicy::ExplicitStop,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_one_way(sample: irtt_client::OneWayDelaySample) -> Option<OneWayLatency> {
|
||||||
|
let uplink = i64::try_from(sample.client_to_server?.as_nanos()).ok()?;
|
||||||
|
|
||||||
|
let downlink = i64::try_from(sample.server_to_client?.as_nanos()).ok()?;
|
||||||
|
|
||||||
|
Some(OneWayLatency {
|
||||||
|
uplink: SignedDuration::from_nanos(uplink),
|
||||||
|
downlink: SignedDuration::from_nanos(downlink),
|
||||||
|
clock: OneWayClock::Uncalibrated,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
pub(crate) mod icmp;
|
pub(crate) mod icmp;
|
||||||
|
#[cfg(feature = "irtt")]
|
||||||
|
pub(crate) mod irtt;
|
||||||
pub(crate) mod model;
|
pub(crate) mod model;
|
||||||
@@ -20,7 +20,7 @@ pub enum MeasurementSource {
|
|||||||
impl Display for MeasurementSource {
|
impl Display for MeasurementSource {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
MeasurementSource::IcmpEcho => write!(f, "icmp"),
|
MeasurementSource::IcmpEcho => write!(f, "icmp-echo"),
|
||||||
MeasurementSource::IcmpTimestamp => write!(f, "icmp-timestamps"),
|
MeasurementSource::IcmpTimestamp => write!(f, "icmp-timestamps"),
|
||||||
MeasurementSource::Irtt => write!(f, "irtt"),
|
MeasurementSource::Irtt => write!(f, "irtt"),
|
||||||
MeasurementSource::TcpSeqAck => write!(f, "tcp-seqack"),
|
MeasurementSource::TcpSeqAck => write!(f, "tcp-seqack"),
|
||||||
@@ -121,6 +121,9 @@ pub struct MeasurementObservation {
|
|||||||
/// Local monotonic time at which the result was observed.
|
/// Local monotonic time at which the result was observed.
|
||||||
pub observed_at: Instant,
|
pub observed_at: Instant,
|
||||||
|
|
||||||
|
/// Measured round-trip latency when it differs from elapsed local time.
|
||||||
|
pub round_trip: Option<SignedDuration>,
|
||||||
|
|
||||||
/// Directional timing, when the measurement mechanism can provide it.
|
/// Directional timing, when the measurement mechanism can provide it.
|
||||||
pub one_way: Option<OneWayLatency>,
|
pub one_way: Option<OneWayLatency>,
|
||||||
}
|
}
|
||||||
@@ -131,7 +134,10 @@ impl MeasurementObservation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn rtt_ms(&self) -> f64 {
|
pub fn rtt_ms(&self) -> f64 {
|
||||||
self.rtt().as_secs_f64() * 1000.0
|
self.round_trip
|
||||||
|
.map(SignedDuration::as_secs_f64)
|
||||||
|
.unwrap_or_else(|| self.rtt().as_secs_f64())
|
||||||
|
* 1000.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-15
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
use crate::baseliner::ControlSnapshot;
|
use crate::baseliner::ControlSnapshot;
|
||||||
|
use crate::measurement::model::MeasurementStream;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::platform::{
|
use crate::platform::{
|
||||||
InterfaceStatsProvider, PlatformInterfaceStats, PlatformTrafficControl, TrafficControlBackend,
|
InterfaceStatsProvider, PlatformInterfaceStats, PlatformTrafficControl, TrafficControlBackend,
|
||||||
@@ -19,7 +20,6 @@ use flume::{Receiver, Sender};
|
|||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -100,7 +100,7 @@ fn drain_latest_snapshot(
|
|||||||
|
|
||||||
fn deltas_from_snapshot(
|
fn deltas_from_snapshot(
|
||||||
snapshot: Option<&ControlSnapshot>,
|
snapshot: Option<&ControlSnapshot>,
|
||||||
reflectors: &[IpAddr],
|
reflectors: &[MeasurementStream],
|
||||||
now: Instant,
|
now: Instant,
|
||||||
tick_interval: f64,
|
tick_interval: f64,
|
||||||
) -> (Vec<f64>, Vec<f64>) {
|
) -> (Vec<f64>, Vec<f64>) {
|
||||||
@@ -123,8 +123,8 @@ fn deltas_from_snapshot(
|
|||||||
up.push(up_delta);
|
up.push(up_delta);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Reflector: {} down_delay: {} up_delay: {}",
|
"Reflector: {} ({:?}) down_delay: {} up_delay: {}",
|
||||||
reflector, down_delta, up_delta
|
reflector.peer, reflector.source, down_delta, up_delta
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ pub struct Ratecontroller<S: InterfaceStatsProvider, T: TrafficControlBackend> {
|
|||||||
settings: Settings,
|
settings: Settings,
|
||||||
control_snapshot: Option<ControlSnapshot>,
|
control_snapshot: Option<ControlSnapshot>,
|
||||||
control_snapshot_rx: Receiver<ControlSnapshot>,
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
reflectors_lock: ArcRwLock<Vec<IpAddr>>,
|
reflectors_lock: ArcRwLock<Vec<MeasurementStream>>,
|
||||||
reselect_trigger: Sender<bool>,
|
reselect_trigger: Sender<bool>,
|
||||||
state_dl: State<T::Handle>,
|
state_dl: State<T::Handle>,
|
||||||
state_ul: State<T::Handle>,
|
state_ul: State<T::Handle>,
|
||||||
@@ -153,7 +153,7 @@ impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
|
|||||||
fn new_with_backends(
|
fn new_with_backends(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
control_snapshot_rx: Receiver<ControlSnapshot>,
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
reflectors_lock: ArcRwLock<Vec<IpAddr>>,
|
reflectors_lock: ArcRwLock<Vec<MeasurementStream>>,
|
||||||
reselect_trigger: Sender<bool>,
|
reselect_trigger: Sender<bool>,
|
||||||
metrics: MetricsSender,
|
metrics: MetricsSender,
|
||||||
mut stats_provider: S,
|
mut stats_provider: S,
|
||||||
@@ -491,8 +491,7 @@ impl<S: InterfaceStatsProvider, T: TrafficControlBackend> Ratecontroller<S, T> {
|
|||||||
self.state_dl.deltas = down_deltas;
|
self.state_dl.deltas = down_deltas;
|
||||||
self.state_ul.deltas = up_deltas;
|
self.state_ul.deltas = up_deltas;
|
||||||
|
|
||||||
let minimum =
|
let minimum = usize::from(self.settings.reflectors.num_reflectors).min(reflectors.len());
|
||||||
usize::from(self.settings.advanced_settings.num_reflectors).min(reflectors.len());
|
|
||||||
if reflectors.is_empty()
|
if reflectors.is_empty()
|
||||||
|| self.state_dl.deltas.len() < minimum
|
|| self.state_dl.deltas.len() < minimum
|
||||||
|| self.state_ul.deltas.len() < minimum
|
|| self.state_ul.deltas.len() < minimum
|
||||||
@@ -515,7 +514,7 @@ impl Ratecontroller<PlatformInterfaceStats, PlatformTrafficControl> {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
control_snapshot_rx: Receiver<ControlSnapshot>,
|
control_snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
reflectors_lock: ArcRwLock<Vec<IpAddr>>,
|
reflectors_lock: ArcRwLock<Vec<MeasurementStream>>,
|
||||||
reselect_trigger: Sender<bool>,
|
reselect_trigger: Sender<bool>,
|
||||||
metrics: MetricsSender,
|
metrics: MetricsSender,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
@@ -536,7 +535,17 @@ impl Ratecontroller<PlatformInterfaceStats, PlatformTrafficControl> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::baseliner::{EwmaStats, ReflectorState};
|
use crate::baseliner::{EwmaStats, ReflectorState};
|
||||||
|
use crate::measurement::model::{MeasurementSource, MeasurementStreamId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
fn stream(id: u64, peer: IpAddr) -> MeasurementStream {
|
||||||
|
MeasurementStream {
|
||||||
|
id: MeasurementStreamId(id),
|
||||||
|
peer,
|
||||||
|
source: MeasurementSource::IcmpTimestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fixed_seed_reproduces_initial_safe_rates() {
|
fn fixed_seed_reproduces_initial_safe_rates() {
|
||||||
@@ -570,7 +579,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot(generated_at: Instant, marker: f64) -> ControlSnapshot {
|
fn snapshot(generated_at: Instant, marker: f64) -> ControlSnapshot {
|
||||||
let reflector = "192.0.2.1".parse().unwrap();
|
let reflector = stream(0, "192.0.2.1".parse().unwrap());
|
||||||
ControlSnapshot {
|
ControlSnapshot {
|
||||||
generated_at,
|
generated_at,
|
||||||
reflectors: HashMap::from([(
|
reflectors: HashMap::from([(
|
||||||
@@ -596,7 +605,9 @@ mod tests {
|
|||||||
assert!(rx.is_empty());
|
assert!(rx.is_empty());
|
||||||
assert_eq!(latest.generated_at, newest_at);
|
assert_eq!(latest.generated_at, newest_at);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
latest.reflectors[&"192.0.2.1".parse().unwrap()].recent.down,
|
latest.reflectors[&stream(0, "192.0.2.1".parse().unwrap())]
|
||||||
|
.recent
|
||||||
|
.down,
|
||||||
2.0
|
2.0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -604,10 +615,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn calculates_sorted_deltas_from_active_fresh_snapshot_state() {
|
fn calculates_sorted_deltas_from_active_fresh_snapshot_state() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let first: IpAddr = "192.0.2.1".parse().unwrap();
|
let first = stream(0, "192.0.2.1".parse().unwrap());
|
||||||
let second: IpAddr = "192.0.2.2".parse().unwrap();
|
let second = stream(1, "192.0.2.2".parse().unwrap());
|
||||||
let stale: IpAddr = "192.0.2.3".parse().unwrap();
|
let stale = stream(2, "192.0.2.3".parse().unwrap());
|
||||||
let inactive: IpAddr = "192.0.2.4".parse().unwrap();
|
let inactive = stream(3, "192.0.2.4".parse().unwrap());
|
||||||
let snapshot = ControlSnapshot {
|
let snapshot = ControlSnapshot {
|
||||||
generated_at: now,
|
generated_at: now,
|
||||||
reflectors: HashMap::from([
|
reflectors: HashMap::from([
|
||||||
|
|||||||
+47
-17
@@ -7,12 +7,13 @@
|
|||||||
|
|
||||||
use crate::SHUTDOWN;
|
use crate::SHUTDOWN;
|
||||||
use crate::baseliner::ControlSnapshot;
|
use crate::baseliner::ControlSnapshot;
|
||||||
|
use crate::measurement::model::MeasurementStream;
|
||||||
use crate::metrics::{Metric, MetricsSender};
|
use crate::metrics::{Metric, MetricsSender};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::util::RwLockExt;
|
use crate::util::RwLockExt;
|
||||||
use flume::{Receiver, RecvError, RecvTimeoutError, Selector, Sender};
|
use flume::{Receiver, RecvError, RecvTimeoutError, Selector, Sender};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use std::net::IpAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
@@ -98,18 +99,25 @@ fn receive_snapshots_until(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recent_rtt_for_peer(snapshot: Option<&ControlSnapshot>, peer: &IpAddr) -> Option<u64> {
|
fn recent_rtt_for_peer(
|
||||||
|
snapshot: Option<&ControlSnapshot>,
|
||||||
|
stream: Option<&MeasurementStream>,
|
||||||
|
now: Instant,
|
||||||
|
max_age: Duration,
|
||||||
|
) -> Option<u64> {
|
||||||
snapshot
|
snapshot
|
||||||
.and_then(|snapshot| snapshot.reflectors.get(peer))
|
.and_then(|snapshot| stream.and_then(|stream| snapshot.reflectors.get(stream)))
|
||||||
|
.filter(|state| now.saturating_duration_since(state.last_receive_at) < max_age)
|
||||||
.map(|state| (state.recent.down + state.recent.up) as u64)
|
.map(|state| (state.recent.down + state.recent.up) as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ReflectorSelector {
|
pub struct ReflectorSelector {
|
||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
pub snapshot_rx: Receiver<ControlSnapshot>,
|
pub snapshot_rx: Receiver<ControlSnapshot>,
|
||||||
pub reflector_peers_lock: Arc<RwLock<Vec<IpAddr>>>,
|
pub reflector_peers_lock: Arc<RwLock<Vec<SocketAddr>>>,
|
||||||
pub reflector_tx: Sender<Vec<IpAddr>>,
|
pub reflector_tx: Sender<Vec<SocketAddr>>,
|
||||||
pub reflector_pool: Vec<IpAddr>,
|
pub reflector_pool: Vec<SocketAddr>,
|
||||||
|
pub endpoint_streams: Arc<RwLock<std::collections::HashMap<SocketAddr, MeasurementStream>>>,
|
||||||
pub trigger_channel: Receiver<bool>,
|
pub trigger_channel: Receiver<bool>,
|
||||||
pub metrics: MetricsSender,
|
pub metrics: MetricsSender,
|
||||||
}
|
}
|
||||||
@@ -159,13 +167,11 @@ impl ReflectorSelector {
|
|||||||
|
|
||||||
// After 40 reselections, slow down to every 15 minutes
|
// After 40 reselections, slow down to every 15 minutes
|
||||||
if reselection_count > 40 {
|
if reselection_count > 40 {
|
||||||
selector_sleep_time = Duration::new(
|
selector_sleep_time =
|
||||||
self.settings.advanced_settings.peer_reselection_time * 60,
|
Duration::new(self.settings.reflectors.peer_reselection_time * 60, 0);
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut next_peers: Vec<IpAddr> = Vec::new();
|
let mut next_peers: Vec<SocketAddr> = Vec::new();
|
||||||
let mut reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
let mut reflectors_peers = self.reflector_peers_lock.write_anyhow()?;
|
||||||
let current_peers = reflectors_peers.clone();
|
let current_peers = reflectors_peers.clone();
|
||||||
|
|
||||||
@@ -175,6 +181,9 @@ impl ReflectorSelector {
|
|||||||
next_peers.push(*reflector);
|
next_peers.push(*reflector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.reflector_pool.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
let next_candidate =
|
let next_candidate =
|
||||||
&self.reflector_pool[fastrand::usize(..self.reflector_pool.len())];
|
&self.reflector_pool[fastrand::usize(..self.reflector_pool.len())];
|
||||||
@@ -208,7 +217,13 @@ impl ReflectorSelector {
|
|||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
for peer in next_peers {
|
for peer in next_peers {
|
||||||
if let Some(rtt) = recent_rtt_for_peer(latest_snapshot.as_ref(), &peer) {
|
let stream = self.endpoint_streams.read_anyhow()?.get(&peer).copied();
|
||||||
|
if let Some(rtt) = recent_rtt_for_peer(
|
||||||
|
latest_snapshot.as_ref(),
|
||||||
|
stream.as_ref(),
|
||||||
|
Instant::now(),
|
||||||
|
Duration::from_secs_f64(self.settings.advanced_settings.tick_interval * 2.0),
|
||||||
|
) {
|
||||||
candidates.push((peer, rtt));
|
candidates.push((peer, rtt));
|
||||||
info!("Candidate reflector: {} RTT: {}", peer, rtt);
|
info!("Candidate reflector: {} RTT: {}", peer, rtt);
|
||||||
} else {
|
} else {
|
||||||
@@ -223,7 +238,7 @@ impl ReflectorSelector {
|
|||||||
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
candidates.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
// Now we will just limit the candidates down to 2 * num_reflectors
|
// Now we will just limit the candidates down to 2 * num_reflectors
|
||||||
let mut num_reflectors = self.settings.advanced_settings.num_reflectors;
|
let num_reflectors = self.settings.reflectors.num_reflectors;
|
||||||
let candidate_pool_num = (2 * num_reflectors) as usize;
|
let candidate_pool_num = (2 * num_reflectors) as usize;
|
||||||
candidates.truncate(candidate_pool_num);
|
candidates.truncate(candidate_pool_num);
|
||||||
|
|
||||||
@@ -245,8 +260,12 @@ impl ReflectorSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (candidates.len() as u8) < num_reflectors {
|
if (candidates.len() as u8) < num_reflectors {
|
||||||
num_reflectors = candidates.len() as u8;
|
info!(
|
||||||
|
"Only {} usable reflectors, selecting available candidates",
|
||||||
|
candidates.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
let num_reflectors = num_reflectors.min(candidates.len() as u8);
|
||||||
|
|
||||||
let mut new_peers = Vec::new();
|
let mut new_peers = Vec::new();
|
||||||
for i in 0..num_reflectors {
|
for i in 0..num_reflectors {
|
||||||
@@ -256,7 +275,7 @@ impl ReflectorSelector {
|
|||||||
self.metrics.send(Metric::Event {
|
self.metrics.send(Metric::Event {
|
||||||
name: "reflector_selected",
|
name: "reflector_selected",
|
||||||
reason: "",
|
reason: "",
|
||||||
reflector: Some(peer),
|
reflector: Some(peer.ip()),
|
||||||
tags: &[],
|
tags: &[],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -271,7 +290,9 @@ impl ReflectorSelector {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::baseliner::{EwmaStats, ReflectorState};
|
use crate::baseliner::{EwmaStats, ReflectorState};
|
||||||
|
use crate::measurement::model::MeasurementSource;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
fn snapshot(generated_at: Instant) -> ControlSnapshot {
|
fn snapshot(generated_at: Instant) -> ControlSnapshot {
|
||||||
@@ -284,7 +305,11 @@ mod tests {
|
|||||||
fn snapshot_with_rtt(generated_at: Instant, peer: IpAddr, rtt: f64) -> ControlSnapshot {
|
fn snapshot_with_rtt(generated_at: Instant, peer: IpAddr, rtt: f64) -> ControlSnapshot {
|
||||||
let mut reflectors = HashMap::new();
|
let mut reflectors = HashMap::new();
|
||||||
reflectors.insert(
|
reflectors.insert(
|
||||||
|
crate::measurement::model::MeasurementStream {
|
||||||
|
id: crate::measurement::model::MeasurementStreamId(0),
|
||||||
peer,
|
peer,
|
||||||
|
source: MeasurementSource::IcmpTimestamp,
|
||||||
|
},
|
||||||
ReflectorState {
|
ReflectorState {
|
||||||
baseline: EwmaStats {
|
baseline: EwmaStats {
|
||||||
down: rtt / 2.0,
|
down: rtt / 2.0,
|
||||||
@@ -384,7 +409,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn baseline_wait_retains_snapshot_for_evaluation() {
|
fn baseline_wait_retains_snapshot_for_evaluation() {
|
||||||
let (snapshot_tx, snapshot_rx) = flume::unbounded();
|
let (snapshot_tx, snapshot_rx) = flume::unbounded();
|
||||||
let peer = "192.0.2.1".parse().unwrap();
|
let peer: IpAddr = "192.0.2.1".parse().unwrap();
|
||||||
let generated_at = Instant::now();
|
let generated_at = Instant::now();
|
||||||
snapshot_tx
|
snapshot_tx
|
||||||
.send(snapshot_with_rtt(generated_at, peer, 20.0))
|
.send(snapshot_with_rtt(generated_at, peer, 20.0))
|
||||||
@@ -405,7 +430,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
recent_rtt_for_peer(latest_snapshot.as_ref(), &peer),
|
recent_rtt_for_peer(
|
||||||
|
latest_snapshot.as_ref(),
|
||||||
|
latest_snapshot.as_ref().unwrap().reflectors.keys().next(),
|
||||||
|
generated_at + Duration::from_secs(1),
|
||||||
|
Duration::from_secs(2)
|
||||||
|
),
|
||||||
Some(42)
|
Some(42)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+200
-66
@@ -2,12 +2,10 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
use anyhow::{Context, bail};
|
|
||||||
use config::{Config, ConfigError, Environment};
|
use config::{Config, ConfigError, Environment};
|
||||||
use log::Level;
|
use log::Level;
|
||||||
use serde::{Deserialize, Deserializer, de};
|
use serde::{Deserialize, Deserializer, de};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use uci_derive::{UciConfig, UciSection};
|
use uci_derive::{UciConfig, UciSection};
|
||||||
|
|
||||||
@@ -48,7 +46,7 @@ where
|
|||||||
type Value = bool;
|
type Value = bool;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
formatter.write_str("a boolean or a UCI-compatible boolean string")
|
formatter.write_str("a boolean, integer 0 or 1, or a UCI-compatible boolean string")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
||||||
@@ -58,6 +56,28 @@ where
|
|||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
0 => Ok(false),
|
||||||
|
1 => Ok(true),
|
||||||
|
_ => Err(E::custom("boolean integer must be 0 or 1")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
0 => Ok(false),
|
||||||
|
1 => Ok(true),
|
||||||
|
_ => Err(E::custom("boolean integer must be 0 or 1")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: de::Error,
|
E: de::Error,
|
||||||
@@ -79,28 +99,32 @@ where
|
|||||||
deserializer.deserialize_any(FlexiBoolVisitor)
|
deserializer.deserialize_any(FlexiBoolVisitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub enum MeasurementType {
|
#[serde(rename_all = "kebab-case")]
|
||||||
Icmp = 1,
|
pub enum ReflectorProtocol {
|
||||||
|
IcmpEcho = 1,
|
||||||
IcmpTimestamps,
|
IcmpTimestamps,
|
||||||
|
Irtt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for MeasurementType {
|
impl Display for ReflectorProtocol {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
MeasurementType::Icmp => write!(f, "icmp"),
|
ReflectorProtocol::IcmpEcho => write!(f, "icmp-echo"),
|
||||||
MeasurementType::IcmpTimestamps => write!(f, "icmp-timestamps"),
|
ReflectorProtocol::IcmpTimestamps => write!(f, "icmp-timestamps"),
|
||||||
|
ReflectorProtocol::Irtt => write!(f, "irtt"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for MeasurementType {
|
impl FromStr for ReflectorProtocol {
|
||||||
type Err = ConfigError;
|
type Err = ConfigError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"icmp" => Ok(MeasurementType::Icmp),
|
"icmp-echo" => Ok(ReflectorProtocol::IcmpEcho),
|
||||||
"icmp-timestamps" | "icmp_timestamps" => Ok(MeasurementType::IcmpTimestamps),
|
"icmp-timestamps" => Ok(ReflectorProtocol::IcmpTimestamps),
|
||||||
|
"irtt" => Ok(ReflectorProtocol::Irtt),
|
||||||
_ => Err(ConfigError::Message(format!(
|
_ => Err(ConfigError::Message(format!(
|
||||||
"invalid measurement type: {s}"
|
"invalid measurement type: {s}"
|
||||||
))),
|
))),
|
||||||
@@ -128,12 +152,23 @@ impl FromStr for ObservabilityProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, UciSection)]
|
||||||
struct ReflectorRecord {
|
pub(crate) struct ReflectorDefinition {
|
||||||
reflector_ip: IpAddr,
|
pub(crate) host: String,
|
||||||
ip_version: u8,
|
pub(crate) country: Option<String>,
|
||||||
|
pub(crate) protocols: Vec<ReflectorProtocol>,
|
||||||
|
pub(crate) irtt_port: Option<u16>,
|
||||||
#[allow(dead_code)]
|
#[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)]
|
#[derive(Clone, Debug, Deserialize, UciConfig)]
|
||||||
@@ -142,6 +177,10 @@ pub(crate) struct Settings {
|
|||||||
#[uci(section)]
|
#[uci(section)]
|
||||||
pub(crate) network: NetworkSettings,
|
pub(crate) network: NetworkSettings,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[uci(section)]
|
||||||
|
pub(crate) reflectors: ReflectorSettings,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[uci(section)]
|
#[uci(section)]
|
||||||
pub(crate) output: OutputSettings,
|
pub(crate) output: OutputSettings,
|
||||||
@@ -160,55 +199,41 @@ impl Settings {
|
|||||||
let builder = Config::builder();
|
let builder = Config::builder();
|
||||||
|
|
||||||
#[cfg(all(feature = "uci", unix))]
|
#[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")]
|
#[cfg(feature = "toml")]
|
||||||
let builder = builder.add_source(
|
let builder = builder.add_source(
|
||||||
config::File::with_name("/etc/sqm-autorate-rust/config.toml").required(false),
|
config::File::with_name("/etc/sqm-autorate-rust/config.toml").required(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
let builder = builder.add_source(Environment::with_prefix("SQMA").separator("__"));
|
let builder = builder.add_source(
|
||||||
let built_config = builder.build()?;
|
Environment::with_prefix("SQMA")
|
||||||
|
.separator("__")
|
||||||
|
.try_parsing(true)
|
||||||
|
.list_separator(",")
|
||||||
|
.with_list_parse_key("reflectors.files")
|
||||||
|
.with_list_parse_key("reflectors.countries"),
|
||||||
|
);
|
||||||
|
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>> {
|
Ok(settings)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +266,100 @@ impl NetworkSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) struct ReflectorSettings {
|
||||||
|
#[serde(deserialize_with = "deserialize_reflector_files")]
|
||||||
|
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)]
|
#[derive(Clone, Debug, Deserialize, UciSection)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) struct OutputSettings {
|
pub(crate) struct OutputSettings {
|
||||||
@@ -310,10 +429,7 @@ pub(crate) struct AdvancedSettings {
|
|||||||
pub(crate) high_load_level: f64,
|
pub(crate) high_load_level: f64,
|
||||||
pub(crate) min_change_interval: f64,
|
pub(crate) min_change_interval: f64,
|
||||||
#[serde(deserialize_with = "deserialize_from_str")]
|
#[serde(deserialize_with = "deserialize_from_str")]
|
||||||
pub(crate) measurement_type: MeasurementType,
|
pub(crate) measurement_type: ReflectorProtocol,
|
||||||
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) speed_hist_size: u32,
|
||||||
pub(crate) tick_interval: f64,
|
pub(crate) tick_interval: f64,
|
||||||
pub(crate) upload_delay_ms: f64,
|
pub(crate) upload_delay_ms: f64,
|
||||||
@@ -327,10 +443,7 @@ impl Default for AdvancedSettings {
|
|||||||
download_delay_ms: 15.0,
|
download_delay_ms: 15.0,
|
||||||
high_load_level: 0.8,
|
high_load_level: 0.8,
|
||||||
min_change_interval: 0.5,
|
min_change_interval: 0.5,
|
||||||
measurement_type: MeasurementType::IcmpTimestamps,
|
measurement_type: ReflectorProtocol::IcmpTimestamps,
|
||||||
num_reflectors: 5,
|
|
||||||
peer_reselection_time: 15,
|
|
||||||
reflector_list_file: "/etc/sqm-autorate/reflectors-icmp.csv".to_owned(),
|
|
||||||
speed_hist_size: 100,
|
speed_hist_size: 100,
|
||||||
tick_interval: 0.5,
|
tick_interval: 0.5,
|
||||||
upload_delay_ms: 15.0,
|
upload_delay_ms: 15.0,
|
||||||
@@ -359,3 +472,24 @@ where
|
|||||||
let value = String::deserialize(deserializer)?;
|
let value = String::deserialize(deserializer)?;
|
||||||
value.parse().map_err(de::Error::custom)
|
value.parse().map_err(de::Error::custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UCI drops empty string options; `option files 'none'` disables file loading.
|
||||||
|
fn deserialize_reflector_files<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum Files {
|
||||||
|
List(Vec<String>),
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
match Files::deserialize(deserializer)? {
|
||||||
|
Files::List(files) => Ok(files),
|
||||||
|
Files::String(value) if value.trim().is_empty() || value.trim() == "none" => Ok(Vec::new()),
|
||||||
|
Files::String(_) => Err(de::Error::custom(
|
||||||
|
"reflector files must be a list, an empty string, or none",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
-18
@@ -3,6 +3,8 @@
|
|||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
//
|
//
|
||||||
|
|
||||||
|
use crate::settings::ReflectorSettings;
|
||||||
|
|
||||||
use super::{AdvancedSettings, NetworkSettings, ObservabilitySettings, OutputSettings, Settings};
|
use super::{AdvancedSettings, NetworkSettings, ObservabilitySettings, OutputSettings, Settings};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
@@ -88,6 +90,7 @@ impl Settings {
|
|||||||
let mut report = ValidationReport::default();
|
let mut report = ValidationReport::default();
|
||||||
|
|
||||||
self.network.validate(&mut report);
|
self.network.validate(&mut report);
|
||||||
|
self.reflectors.validate(&mut report);
|
||||||
self.output.validate(&mut report);
|
self.output.validate(&mut report);
|
||||||
self.observability.validate(&mut report);
|
self.observability.validate(&mut report);
|
||||||
self.advanced_settings.validate(&mut report);
|
self.advanced_settings.validate(&mut report);
|
||||||
@@ -192,6 +195,60 @@ 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, reflector) in self.entries.iter().enumerate() {
|
||||||
|
if reflector.host.trim().is_empty() {
|
||||||
|
report.error(
|
||||||
|
"reflectors.entries",
|
||||||
|
format!("entry {index} must have a nonempty host"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if reflector.protocols.is_empty() {
|
||||||
|
report.error(
|
||||||
|
"reflectors.entries",
|
||||||
|
format!("entry {index} must have at least one measurement protocol"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
impl OutputSettings {
|
||||||
fn validate(&self, report: &mut ValidationReport) {
|
fn validate(&self, report: &mut ValidationReport) {
|
||||||
if self.suppress_statistics {
|
if self.suppress_statistics {
|
||||||
@@ -298,24 +355,6 @@ impl AdvancedSettings {
|
|||||||
self.tick_interval,
|
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 {
|
if self.speed_hist_size == 0 {
|
||||||
report.error(
|
report.error(
|
||||||
"advanced_settings.speed_hist_size",
|
"advanced_settings.speed_hist_size",
|
||||||
|
|||||||
Reference in new issue
Block a user