Полная реализация production-фиксов из up3: anti-torrent rebuild, peer-status, drift rate/ceil, reboot window

This commit is contained in:
2026-05-10 00:15:17 +05:00
parent d9f140b3e0
commit 535c1dbc33
8 changed files with 358 additions and 32 deletions
+234 -5
View File
@@ -17,6 +17,25 @@ struct TrafficDrift {
reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ClassRateCeil {
rate: String,
ceil: String,
}
#[derive(Debug, Clone)]
pub struct PeerTrafficStatus {
pub ip: String,
pub class_id: u16,
pub egress_filter_present: bool,
pub ingress_filter_present: bool,
pub egress_rate: Option<String>,
pub egress_ceil: Option<String>,
pub ingress_rate: Option<String>,
pub ingress_ceil: Option<String>,
pub drift_reason: Option<String>,
}
pub fn inspect_drift_summary(cfg: &Config) -> anyhow::Result<Option<String>> {
if !cfg.traffic.enabled {
return Ok(None);
@@ -39,6 +58,7 @@ pub fn inspect_drift_summary(cfg: &Config) -> anyhow::Result<Option<String>> {
&target.interface_name,
&cfg.traffic.ifb_name,
cfg.traffic.class_start_id,
&cfg.traffic.peer_limit,
&peers,
)?;
@@ -73,6 +93,7 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
&target.interface_name,
&cfg.traffic.ifb_name,
cfg.traffic.class_start_id,
&cfg.traffic.peer_limit,
&peers,
)?;
@@ -101,11 +122,104 @@ pub fn reconcile_traffic(cfg: &Config) -> anyhow::Result<()> {
Ok(())
}
pub fn peer_status_table(cfg: &Config) -> anyhow::Result<Option<Vec<PeerTrafficStatus>>> {
if !cfg.traffic.enabled {
return Ok(None);
}
let target = docker::discover_target(cfg)?;
let transport = build_transport(target.container_pid, &target.container_name);
let peers = wg::discover_peers(
target.container_pid,
&target.container_name,
&cfg.amnezia.peer_ipv4_cidr,
)?;
if peers.is_empty() {
return Ok(Some(Vec::new()));
}
let filters_egress = transport
.run(&["tc", "filter", "show", "dev", &target.interface_name, "parent", "1:"])
.unwrap_or_default();
let filters_ingress = transport
.run(&["tc", "filter", "show", "dev", &cfg.traffic.ifb_name, "parent", "2:"])
.unwrap_or_default();
let classes_egress_full = transport
.run(&["tc", "class", "show", "dev", &target.interface_name])
.unwrap_or_default();
let classes_ingress_full = transport
.run(&["tc", "class", "show", "dev", &cfg.traffic.ifb_name])
.unwrap_or_default();
let actual_map_egress = extract_filter_ip_class_map(&filters_egress);
let actual_map_ingress = extract_filter_ip_class_map(&filters_ingress);
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
let expected_map = expected_peer_class_map(&peers, cfg.traffic.class_start_id);
let expected_limit = normalize_tc_rate(&cfg.traffic.peer_limit);
let mut out = Vec::with_capacity(peers.len());
for p in peers {
let ip = p.ipv4.to_string();
let class_id = *expected_map.get(&ip).unwrap_or(&cfg.traffic.class_start_id);
let class_egress = format!("1:{class_id}");
let class_ingress = format!("2:{class_id}");
let egress_filter_present = actual_map_egress.get(&ip).copied() == Some(class_id);
let ingress_filter_present = actual_map_ingress.get(&ip).copied() == Some(class_id);
let e = egress_rates.get(&class_egress);
let i = ingress_rates.get(&class_ingress);
let mut reasons = Vec::new();
if !egress_filter_present {
reasons.push("missing/mismatched egress filter".to_string());
}
if !ingress_filter_present {
reasons.push("missing/mismatched ingress filter".to_string());
}
if e.is_none() {
reasons.push("missing egress class".to_string());
}
if i.is_none() {
reasons.push("missing ingress class".to_string());
}
if let Some(v) = e
&& !rate_ceil_matches_limit(v, &expected_limit)
{
reasons.push("egress rate/ceil mismatch".to_string());
}
if let Some(v) = i
&& !rate_ceil_matches_limit(v, &expected_limit)
{
reasons.push("ingress rate/ceil mismatch".to_string());
}
out.push(PeerTrafficStatus {
ip,
class_id,
egress_filter_present,
ingress_filter_present,
egress_rate: e.map(|v| v.rate.clone()),
egress_ceil: e.map(|v| v.ceil.clone()),
ingress_rate: i.map(|v| v.rate.clone()),
ingress_ceil: i.map(|v| v.ceil.clone()),
drift_reason: if reasons.is_empty() {
None
} else {
Some(reasons.join(", "))
},
});
}
Ok(Some(out))
}
fn inspect_and_diff(
transport: &dyn NsTransport,
iface: &str,
ifb: &str,
class_start_id: u16,
peer_limit: &str,
peers: &[wg::Peer],
) -> anyhow::Result<TrafficDrift> {
let root_qdisc = transport
@@ -195,6 +309,37 @@ fn inspect_and_diff(
});
}
let expected_limit = normalize_tc_rate(peer_limit);
let classes_egress_full = transport
.run(&["tc", "class", "show", "dev", iface])
.unwrap_or_default();
let classes_ingress_full = transport
.run(&["tc", "class", "show", "dev", ifb])
.unwrap_or_default();
let egress_rates = parse_class_rate_ceil_map(&classes_egress_full, "1:");
let ingress_rates = parse_class_rate_ceil_map(&classes_ingress_full, "2:");
for class_id in expected_map.values() {
let class_egress = format!("1:{class_id}");
let class_ingress = format!("2:{class_id}");
let e = egress_rates.get(&class_egress);
let i = ingress_rates.get(&class_ingress);
if e.is_none() || i.is_none() {
return Ok(TrafficDrift {
has_drift: true,
reason: format!("missing rate/ceil class details for peer class_id={class_id}"),
});
}
if e.map(|v| !rate_ceil_matches_limit(v, &expected_limit)).unwrap_or(true)
|| i.map(|v| !rate_ceil_matches_limit(v, &expected_limit)).unwrap_or(true)
{
return Ok(TrafficDrift {
has_drift: true,
reason: format!("rate/ceil mismatch for peer class_id={class_id}"),
});
}
}
let classes_egress = transport
.run(&["tc", "class", "show", "dev", iface, "parent", "1:1"])
.unwrap_or_default();
@@ -324,9 +469,9 @@ fn teardown_owned_topology(
iface: &str,
ifb: &str,
) -> anyhow::Result<()> {
let _ = transport.run(&["tc", "qdisc", "del", "dev", iface, "root"]);
let _ = transport.run(&["tc", "qdisc", "del", "dev", iface, "ingress"]);
let _ = transport.run(&["ip", "link", "del", ifb]);
run_allow_notfound(transport, &["tc", "qdisc", "del", "dev", iface, "root"])?;
run_allow_notfound(transport, &["tc", "qdisc", "del", "dev", iface, "ingress"])?;
run_allow_notfound(transport, &["ip", "link", "del", ifb])?;
Ok(())
}
@@ -499,7 +644,14 @@ fn apply_full_topology(
)?;
}
let verify = inspect_and_diff(transport, iface, ifb, cfg.traffic.class_start_id, peers)?;
let verify = inspect_and_diff(
transport,
iface,
ifb,
cfg.traffic.class_start_id,
&cfg.traffic.peer_limit,
peers,
)?;
if verify.has_drift {
anyhow::bail!("verify-after-apply failed: {}", verify.reason);
}
@@ -517,7 +669,7 @@ fn run_allow_exists(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Resul
Ok(_) => Ok(()),
Err(e) => {
let msg = e.to_string();
if msg.contains("File exists") || msg.contains("exists") || msg.contains("EEXIST") {
if msg.contains("File exists") || msg.contains("EEXIST") {
return Ok(());
}
Err(e)
@@ -525,6 +677,64 @@ fn run_allow_exists(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Resul
}
}
fn run_allow_notfound(transport: &dyn NsTransport, args: &[&str]) -> anyhow::Result<()> {
match transport.run(args) {
Ok(_) => Ok(()),
Err(e) => {
let msg = e.to_string();
if msg.contains("No such file")
|| msg.contains("No such")
|| msg.contains("not found")
|| msg.contains("Cannot find")
{
return Ok(());
}
Err(e)
}
}
}
fn parse_class_rate_ceil_map(raw: &str, prefix: &str) -> BTreeMap<String, ClassRateCeil> {
let mut out = BTreeMap::new();
for line in raw.lines() {
let t = line.trim();
if !t.contains("class htb") || !t.contains(" rate ") || !t.contains(" ceil ") {
continue;
}
let toks: Vec<&str> = t.split_whitespace().collect();
let class = toks
.iter()
.position(|v| *v == "htb")
.and_then(|i| toks.get(i + 1))
.map(|s| s.to_string());
let rate = toks
.iter()
.position(|v| *v == "rate")
.and_then(|i| toks.get(i + 1))
.map(|s| s.to_string());
let ceil = toks
.iter()
.position(|v| *v == "ceil")
.and_then(|i| toks.get(i + 1))
.map(|s| s.to_string());
if let (Some(class), Some(rate), Some(ceil)) = (class, rate, ceil)
&& class.starts_with(prefix)
&& !class.ends_with(":1")
{
out.insert(class, ClassRateCeil { rate, ceil });
}
}
out
}
fn rate_ceil_matches_limit(v: &ClassRateCeil, limit: &str) -> bool {
normalize_tc_rate(&v.rate) == limit && normalize_tc_rate(&v.ceil) == limit
}
fn normalize_tc_rate(v: &str) -> String {
v.trim().to_ascii_lowercase().replace(' ', "")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -543,4 +753,23 @@ mod tests {
assert_eq!(m.get("10.8.1.2").copied(), Some(10));
assert_eq!(m.get("10.8.1.3").copied(), Some(11));
}
#[test]
fn parse_class_rate_ceil() {
let raw = "class htb 1:10 parent 1:1 rate 70Mbit ceil 70Mbit burst 1599b cburst 1599b\nclass htb 1:1 root rate 1000Mbit ceil 1000Mbit";
let m = parse_class_rate_ceil_map(raw, "1:");
let rc = m.get("1:10").expect("class exists");
assert_eq!(normalize_tc_rate(&rc.rate), "70mbit");
assert_eq!(normalize_tc_rate(&rc.ceil), "70mbit");
}
#[test]
fn rate_limit_match_normalized() {
let rc = ClassRateCeil {
rate: "70Mbit".to_string(),
ceil: "70mbit".to_string(),
};
assert!(rate_ceil_matches_limit(&rc, "70mbit"));
assert!(!rate_ceil_matches_limit(&rc, "50mbit"));
}
}