traffic_core: продакшн-фиксы runtime и валидации

This commit is contained in:
2026-05-10 21:29:52 +05:00
parent a4bb7adb4f
commit d4cd6a4e43
9 changed files with 353 additions and 52 deletions
+100 -2
View File
@@ -61,13 +61,30 @@ impl Config {
Ok(cfg)
}
pub fn validate(&self) -> anyhow::Result<()> {
pub fn validate_config(&self) -> anyhow::Result<()> {
if self.amnezia.container_names.is_empty() {
anyhow::bail!("amnezia.container_names must not be empty");
}
if self.amnezia.interface_candidates.is_empty() {
anyhow::bail!("amnezia.interface_candidates must not be empty");
}
if self.amnezia.interface_patterns.is_empty() {
anyhow::bail!("amnezia.interface_patterns must not be empty");
}
if self.traffic.class_start_id < 10 {
anyhow::bail!("traffic.class_start_id must be >= 10");
}
if self.traffic.ifb_name.trim().is_empty() {
anyhow::bail!("traffic.ifb_name must not be empty");
}
if self.traffic.peer_limit.trim().is_empty() {
anyhow::bail!("traffic.peer_limit must not be empty");
}
if self.traffic.root_rate.trim().is_empty() {
anyhow::bail!("traffic.root_rate must not be empty");
}
parse_tc_rate_bps(&self.traffic.peer_limit)?;
parse_tc_rate_bps(&self.traffic.root_rate)?;
if self.swap.swappiness > 100 {
anyhow::bail!("swap.swappiness must be <= 100");
}
@@ -77,12 +94,36 @@ impl Config {
if self.traffic.reconcile_interval_secs == 0 {
anyhow::bail!("traffic.reconcile_interval_secs must be > 0");
}
if self.swap.enabled {
if self.swap.size_mb == 0 {
anyhow::bail!("swap.size_mb must be > 0 when swap.enabled=true");
}
if !self.swap.path.starts_with('/') {
anyhow::bail!("swap.path must be absolute");
}
}
if self.anti_torrent.enabled && self.anti_torrent.max_connections_per_peer == 0 {
anyhow::bail!("anti_torrent.max_connections_per_peer must be > 0");
}
if self.reboot.enabled && self.reboot.window_start_utc == self.reboot.window_end_utc {
anyhow::bail!("reboot window start and end must differ");
}
if self.reboot.enabled {
parse_hhmm(&self.reboot.window_start_utc)?;
parse_hhmm(&self.reboot.window_end_utc)?;
}
parse_cidr_v4(&self.amnezia.peer_ipv4_cidr)?;
parse_cidr_v4(&self.anti_torrent.peer_ipv4_cidr)?;
self.check_host_tools()?;
Ok(())
}
pub fn validate_runtime(&self) -> anyhow::Result<()> {
self.check_host_tools()
}
pub fn validate_all(&self) -> anyhow::Result<()> {
self.validate_config()?;
self.validate_runtime()?;
Ok(())
}
@@ -106,6 +147,49 @@ impl Config {
}
}
fn parse_hhmm(v: &str) -> anyhow::Result<()> {
let mut s = v.split(':');
let h = s
.next()
.ok_or_else(|| anyhow::anyhow!("invalid time: {v}"))?
.parse::<u8>()?;
let m = s
.next()
.ok_or_else(|| anyhow::anyhow!("invalid time: {v}"))?
.parse::<u8>()?;
if s.next().is_some() {
anyhow::bail!("invalid time: {v}");
}
if h > 23 || m > 59 {
anyhow::bail!("invalid time: {v}");
}
Ok(())
}
fn parse_tc_rate_bps(v: &str) -> anyhow::Result<u64> {
let raw = v.trim().to_ascii_lowercase().replace(' ', "");
if raw.is_empty() {
anyhow::bail!("invalid tc rate: empty");
}
let (num, mul) = if let Some(n) = raw.strip_suffix("gbit") {
(n, 1_000_000_000u64)
} else if let Some(n) = raw.strip_suffix("mbit") {
(n, 1_000_000u64)
} else if let Some(n) = raw.strip_suffix("kbit") {
(n, 1_000u64)
} else if let Some(n) = raw.strip_suffix("bit") {
(n, 1u64)
} else {
anyhow::bail!("invalid tc rate unit: {v}");
};
let base = num
.parse::<u64>()
.map_err(|_| anyhow::anyhow!("invalid tc rate number: {v}"))?;
base.checked_mul(mul)
.ok_or_else(|| anyhow::anyhow!("tc rate overflow: {v}"))
}
fn tool_exists(tool: &str) -> bool {
let probes: &[&[&str]] = match tool {
"tc" => &[&["-V"]],
@@ -171,4 +255,18 @@ mod tests {
let no = ip_in_cidr("10.9.1.10".parse().expect("ip"), "10.8.1.0/24").expect("cidr");
assert!(!no);
}
#[test]
fn parse_hhmm_works() {
assert!(parse_hhmm("01:00").is_ok());
assert!(parse_hhmm("23:59").is_ok());
assert!(parse_hhmm("24:00").is_err());
}
#[test]
fn parse_tc_rate_works() {
assert_eq!(parse_tc_rate_bps("1gbit").expect("bps"), 1_000_000_000);
assert_eq!(parse_tc_rate_bps("1000mbit").expect("bps"), 1_000_000_000);
assert!(parse_tc_rate_bps("foo").is_err());
}
}