Полный продакшн-фикс: parity с old bash, drift-recovery, status/runtime и builder separation

This commit is contained in:
2026-05-10 17:09:39 +05:00
parent b8d2c4592b
commit 76c6b52f4e
14 changed files with 558 additions and 62 deletions
+181 -1
View File
@@ -1,6 +1,13 @@
use crate::{config::Config, docker, firewall, netns::build_transport, traffic, wg};
use std::{fs, process::Command};
use std::{
collections::BTreeMap,
fs,
process::Command,
time::{SystemTime, UNIX_EPOCH},
};
const ONLINE_HANDSHAKE_THRESHOLD_SECS: u64 = 180;
pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
println!("System:");
@@ -38,6 +45,52 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
println!(" - {}", p.ipv4);
}
println!("WireGuard clients:");
println!(" IP State Latest handshake RX TX Endpoint");
match wg::discover_peer_runtime(
target.container_pid,
&target.container_name,
&cfg.amnezia.peer_ipv4_cidr,
) {
Ok(runtime_peers) => {
let mut by_ip: BTreeMap<String, &wg::PeerRuntime> = BTreeMap::new();
for rp in &runtime_peers {
for ip in &rp.allowed_ips {
by_ip.insert(ip.clone(), rp);
}
}
let peers_sorted = wg::discover_peers(
target.container_pid,
&target.container_name,
&cfg.amnezia.peer_ipv4_cidr,
)?;
for p in peers_sorted {
let ip = p.ipv4.to_string();
if let Some(rp) = by_ip.get(&ip) {
let state = handshake_is_online(
rp.latest_handshake_epoch,
ONLINE_HANDSHAKE_THRESHOLD_SECS,
);
let hs = format_handshake_age(rp.latest_handshake_epoch);
let rx = format_bytes(rp.transfer_rx_bytes.unwrap_or(0));
let tx = format_bytes(rp.transfer_tx_bytes.unwrap_or(0));
let ep = rp.endpoint.clone().unwrap_or_else(|| "-".to_string());
println!(
" {:<11} {:<8} {:<19} {:<9} {:<9} {}",
ip, state, hs, rx, tx, ep
);
} else {
println!(
" {:<11} {:<8} {:<19} {:<9} {:<9} -",
ip, "offline", "never", "0B", "0B"
);
}
}
}
Err(e) => println!(" unavailable ({e})"),
}
println!("Traffic desired:");
println!(" peer_limit: {}", cfg.traffic.peer_limit);
println!(" root_rate: {}", cfg.traffic.root_rate);
@@ -90,6 +143,23 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
Err(e) => println!(" unavailable ({e})"),
}
println!("Peer traffic counters:");
println!(" IP Egress hits Ingress hits");
match collect_peer_filter_hits(
&target.interface_name,
&cfg.traffic.ifb_name,
target.container_pid,
&target.container_name,
) {
Ok(map) if map.is_empty() => println!(" filter statistics: unavailable"),
Ok(map) => {
for (ip, (eg, ing)) in map {
println!(" {:<11} {:<13} {}", ip, eg, ing);
}
}
Err(_) => println!(" filter statistics: unavailable"),
}
println!("Anti-torrent:");
println!(" enabled: {}", cfg.anti_torrent.enabled);
println!(
@@ -134,5 +204,115 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
}
}
println!("Recent events:");
let events = fs::read_to_string("/run/awg2-harness/events.log").unwrap_or_default();
let lines: Vec<&str> = events.lines().collect();
if lines.is_empty() {
println!(" no recent events");
} else {
for l in lines.iter().rev().take(10).rev() {
println!(" {l}");
}
}
Ok(())
}
fn handshake_is_online(epoch: Option<u64>, threshold_secs: u64) -> &'static str {
if let Some(ts) = epoch {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now.saturating_sub(ts) <= threshold_secs {
return "online";
}
}
"offline"
}
fn format_handshake_age(epoch: Option<u64>) -> String {
let Some(ts) = epoch else {
return "never".to_string();
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let age = now.saturating_sub(ts);
format!("{}s ago", age)
}
fn format_bytes(v: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let b = v as f64;
if b >= GB {
format!("{:.1}GB", b / GB)
} else if b >= MB {
format!("{:.1}MB", b / MB)
} else if b >= KB {
format!("{:.1}KB", b / KB)
} else {
format!("{}B", v)
}
}
fn collect_peer_filter_hits(
iface: &str,
ifb: &str,
pid: u32,
container_name: &str,
) -> anyhow::Result<BTreeMap<String, (u64, u64)>> {
let transport = build_transport(pid, container_name);
let egress = transport.run(&["tc", "-s", "filter", "show", "dev", iface, "parent", "1:"])?;
let ingress = transport.run(&["tc", "-s", "filter", "show", "dev", ifb, "parent", "2:"])?;
let egress_hits = parse_tc_filter_hits(&egress);
let ingress_hits = parse_tc_filter_hits(&ingress);
let mut out: BTreeMap<String, (u64, u64)> = BTreeMap::new();
for (ip, c) in egress_hits {
out.entry(ip).or_insert((0, 0)).0 = c;
}
for (ip, c) in ingress_hits {
out.entry(ip).or_insert((0, 0)).1 = c;
}
Ok(out)
}
fn parse_tc_filter_hits(raw: &str) -> BTreeMap<String, u64> {
let mut out = BTreeMap::new();
let mut current_ip: Option<String> = None;
for line in raw.lines() {
let t = line.trim();
if t.contains("match ") {
current_ip = extract_tc_match_ip(t);
} else if t.starts_with("Sent ") {
if let Some(ip) = current_ip.clone() {
let hits = t
.split_whitespace()
.nth(4)
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
out.insert(ip, hits);
}
}
}
out
}
fn extract_tc_match_ip(line: &str) -> Option<String> {
let toks: Vec<&str> = line.split_whitespace().collect();
let pos = toks.iter().position(|v| *v == "match")?;
let match_val = *toks.get(pos + 1)?;
let (hex, mask) = match_val.split_once('/')?;
if !mask.eq_ignore_ascii_case("ffffffff") || hex.len() != 8 {
return None;
}
let b1 = u8::from_str_radix(&hex[0..2], 16).ok()?;
let b2 = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b3 = u8::from_str_radix(&hex[4..6], 16).ok()?;
let b4 = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some(format!("{b1}.{b2}.{b3}.{b4}"))
}