Полный продакшн-фикс: parity с old bash, drift-recovery, status/runtime и builder separation
This commit is contained in:
+181
-1
@@ -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}"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user