Полная реализация production-фиксов из up3: anti-torrent rebuild, peer-status, drift rate/ceil, reboot window
This commit is contained in:
+234
-5
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user