6 Commits
Author SHA1 Message Date
renovate Bot a815b5cfd0 chore(deps): update rust crate trybuild to v1.0.121
Build / build (pull_request) Failing after 4s
DevSkim / DevSkim (pull_request) Failing after 1m22s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Failing after 1m40s
2026-09-25 10:26:44 +00:00
Nils Andreas Svee 6d4849899e fix(settings): reject zero-floor minimum rates
Build / build (push) Failing after 4s
DevSkim / DevSkim (push) Failing after 1m22s
rust-clippy analyze / Run rust-clippy analyzing (push) Failing after 1m35s
2026-09-25 17:20:08 +07:00
Nils Andreas Svee 7254aa5cf9 fix(uci): propagate malformed optional sources 2026-09-25 17:20:08 +07:00
Nils Andreas Svee 5b0854920d fix(settings): reject IPv6 reflectors 2026-09-25 17:20:07 +07:00
Nils Andreas Svee 4c9fe15201 fix(log): honor configured maximum level 2026-09-25 17:20:07 +07:00
Nils Andreas Svee 5872f40d3e fix(icmp): handle RFC 792 timestamp rollover 2026-09-25 17:20:06 +07:00
6 changed files with 134 additions and 17 deletions

No files matched your search

Generated
+6 -6
View File
@@ -590,10 +590,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "target-triple" name = "target-tuple"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" checksum = "876fef147edbcbddc8ac5cbbba92c7b86519e314e86638596c09673b2ed01e7f"
[[package]] [[package]]
name = "termcolor" name = "termcolor"
@@ -665,15 +665,15 @@ checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]] [[package]]
name = "trybuild" name = "trybuild"
version = "1.0.120" version = "1.0.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" checksum = "c0cabaa10be1917331a313866bd94526343e03c77bcf69144b62b072ad35d47c"
dependencies = [ dependencies = [
"glob", "glob",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"target-triple", "target-tuple",
"termcolor", "termcolor",
"toml", "toml",
] ]
+2 -2
View File
@@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use log::{Level, LevelFilter, Metadata, Record, SetLoggerError}; use log::{Level, Metadata, Record, SetLoggerError};
use rustix::time::ClockId; use rustix::time::ClockId;
use crate::time::Time; use crate::time::Time;
@@ -36,5 +36,5 @@ impl log::Log for SimpleLogger {
pub fn init(level: Level) -> Result<(), SetLoggerError> { pub fn init(level: Level) -> Result<(), SetLoggerError> {
log::set_boxed_logger(Box::new(SimpleLogger { level })) log::set_boxed_logger(Box::new(SimpleLogger { level }))
.map(|()| log::set_max_level(LevelFilter::Trace)) .map(|()| log::set_max_level(level.to_level_filter()))
} }
+48 -5
View File
@@ -19,6 +19,19 @@ pub struct PingerICMPTimestampListener {}
pub struct PingerICMPTimestampSender {} pub struct PingerICMPTimestampSender {}
const MS_PER_DAY: i64 = 86_400_000;
fn timestamp_delta(later: i64, earlier: i64) -> i64 {
let delta = later - earlier;
if delta < -MS_PER_DAY / 2 {
delta + MS_PER_DAY
} else if delta > MS_PER_DAY / 2 {
delta - MS_PER_DAY
} else {
delta
}
}
impl PingListener for PingerICMPTimestampListener { impl PingListener for PingerICMPTimestampListener {
// Result: RTT, down time, up time // Result: RTT, down time, up time
fn parse_packet( fn parse_packet(
@@ -46,12 +59,18 @@ impl PingListener for PingerICMPTimestampListener {
}); });
} }
let time_now = Time::new(ClockId::Realtime); if receive & 0x8000_0000 != 0 || transmit & 0x8000_0000 != 0 {
let time_since_midnight = time_now.get_time_since_midnight(); return Err(PingError::InvalidPacket(
"non-standard RFC 792 timestamp".into(),
));
}
let rtt = (time_since_midnight - originate as i64) as f64; let time_since_midnight =
let dl_time = (time_since_midnight - transmit as i64) as f64; Time::new(ClockId::Realtime).get_time_since_midnight();
let ul_time = (receive as i64 - originate as i64) as f64;
let rtt = timestamp_delta(time_since_midnight, originate as i64) as f64;
let dl_time = timestamp_delta(time_since_midnight, transmit as i64) as f64;
let ul_time = timestamp_delta(receive as i64, originate as i64) as f64;
Ok(PingReply { Ok(PingReply {
reflector, reflector,
@@ -88,3 +107,27 @@ impl PingSender for PingerICMPTimestampSender {
) )
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use icmp_socket2::packet::WithTimestampReply;
#[test]
fn timestamps_wrap_at_midnight_in_both_directions() {
assert_eq!(timestamp_delta(20, 86_399_900), 120);
assert_eq!(timestamp_delta(86_399_900, 20), -120);
}
#[test]
fn non_standard_rfc_792_timestamps_are_rejected() {
let packet = Icmpv4Packet::with_timestamp_reply(7, 1, 10, 0x8000_0001, 20).unwrap();
let result = PingerICMPTimestampListener {}.parse_packet(
7,
"192.0.2.1".parse().unwrap(),
MeasurementType::IcmpTimestamps,
packet,
);
assert!(matches!(result, Err(PingError::InvalidPacket(_))));
}
}
+27 -1
View File
@@ -262,7 +262,7 @@ impl Source for UciSource {
let (_uci, packages) = match resolved { let (_uci, packages) = match resolved {
Ok(resolved) => resolved, Ok(resolved) => resolved,
Err(_) if !self.required => { Err(SourceError::NotFound(_)) if !self.required => {
return Ok(Map::new()); return Ok(Map::new());
} }
@@ -285,3 +285,29 @@ impl Source for UciSource {
Ok(values) Ok(values)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::Settings;
use std::fs;
#[test]
fn malformed_optional_package_is_an_error() {
let root = std::env::temp_dir().join(format!("sqma-uci-{}", std::process::id()));
let config_dir = root.join("config");
let save_dir = root.join("save");
fs::create_dir_all(&config_dir).unwrap();
fs::create_dir_all(&save_dir).unwrap();
let config_file = config_dir.join("sqm-autorate-rust");
fs::write(&config_file, "config network 'unterminated\n").unwrap();
let source = UciSource::from_schema::<Settings>()
.with_directories(&config_dir, &save_dir)
.required(false);
let result = source.collect();
fs::remove_dir_all(root).unwrap();
assert!(result.is_err());
}
}
+4 -1
View File
@@ -192,7 +192,10 @@ impl Settings {
})?; })?;
match (record.ip_version, record.reflector_ip) { match (record.ip_version, record.reflector_ip) {
(4, IpAddr::V4(_)) | (6, IpAddr::V6(_)) => {} (4, IpAddr::V4(_)) => {}
(6, address @ IpAddr::V6(_)) => {
bail!("reflector {address} is IPv6; only IPv4 reflectors are supported");
}
(version, address) => { (version, address) => {
bail!("reflector {address} has inconsistent IP version {version}"); bail!("reflector {address} has inconsistent IP version {version}");
} }
+47 -2
View File
@@ -153,7 +153,12 @@ impl NetworkSettings {
if download_base_valid && download_percent_valid { if download_base_valid && download_percent_valid {
let minimum = self.download_min_kbits(); let minimum = self.download_min_kbits();
if minimum < WARN_DOWNLOAD_MIN_KBITS { if minimum < 1.0 {
report.error(
"network.download_min_percent",
"minimum download rate floors to zero",
);
} else if minimum < WARN_DOWNLOAD_MIN_KBITS {
report.warning( report.warning(
"network.download_min_percent", "network.download_min_percent",
format!( format!(
@@ -168,7 +173,12 @@ impl NetworkSettings {
if upload_base_valid && upload_percent_valid { if upload_base_valid && upload_percent_valid {
let minimum = self.upload_min_kbits(); let minimum = self.upload_min_kbits();
if minimum < WARN_UPLOAD_MIN_KBITS { if minimum < 1.0 {
report.error(
"network.upload_min_percent",
"minimum upload rate floors to zero",
);
} else if minimum < WARN_UPLOAD_MIN_KBITS {
report.warning( report.warning(
"network.upload_min_percent", "network.upload_min_percent",
format!( format!(
@@ -376,3 +386,38 @@ fn validate_percent(report: &mut ValidationReport, path: &'static str, value: f6
true true
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn network(download_base_kbits: f64, upload_base_kbits: f64) -> NetworkSettings {
NetworkSettings {
download_interface: "ifb0".into(),
upload_interface: "eth0".into(),
download_base_kbits,
download_min_percent: 1.0,
upload_base_kbits,
upload_min_percent: 1.0,
}
}
#[test]
fn minimum_rates_that_floor_to_zero_are_errors_for_both_directions() {
let mut report = ValidationReport::default();
network(50.0, 50.0).validate(&mut report);
let error = report.finish().unwrap_err().to_string();
assert!(error.contains("network.download_min_percent"));
assert!(error.contains("network.upload_min_percent"));
assert!(error.contains("floors to zero"));
}
#[test]
fn representable_low_minimum_rates_remain_warnings() {
let mut report = ValidationReport::default();
network(10_000.0, 10_000.0).validate(&mut report);
assert!(report.finish().is_ok());
}
}