status: deduplicate the healthy and degraded paths, keep the System block contiguous

print_status and print_degraded_sections carried four byte-identical blocks - host
firewall (25 lines), anti-torrent (24), last actions (9) and recent events (10) - plus a
Conntrack block that was the same logic assembled differently and a Traffic desired block
of four printlns. Roughly 80 lines of copy-paste with no test covering either copy, so a
format change applied to one path would have silently diverged from the other. The
divergence would only ever be visible while the container is down, which is the worst
moment to discover the diagnostic output is wrong.

Every shared section is now one function called from both paths:
print_traffic_desired_section, print_host_firewall_section, print_antitorrent_section,
print_conntrack_section, print_last_actions_section, print_recent_events_section. The file
already had this shape for print_swap_status and print_reboot_section; the pattern is now
followed consistently. The Docker/Amnezia port path block keeps two variants because they
genuinely differ - real values against unknown - but the shared FORWARD ownership note is
a single constant.

The runtime state paths were also duplicated across modules: status.rs re-declared the
literals that daemon.rs already owns. daemon::LAST_STATE_FILE and daemon::EVENTS_FILE are
now pub and reused, and the recent-events window is a named constant instead of a bare 10.

Ordering fix in the same function: print_network_tuning_status ran between the service
line and the container lines, so container/container_id/pid/netns/interface appeared under
the "Network tuning:" header at the same two-space indent as its fields. Container
identity is now printed inside the discovery match, before any other section opens, in
both the healthy and degraded paths. Field names are unchanged; only the order of two
blocks moved. IMPLEMENTATION_SPEC section 15 documented the old order and is updated.

print_degraded_sections returned anyhow::Result<()> while never failing; it returns unit
now.

status.rs: 645 -> 602 lines. cargo test --workspace: 110 + 4 passed. clippy -D warnings
clean.
This commit is contained in:
2026-08-06 18:41:39 +05:00
parent d98ade42ce
commit 84275d0fe4
3 changed files with 131 additions and 172 deletions
+7 -5
View File
@@ -451,14 +451,14 @@ Status обязан показывать наличие таблицы/chain/п
```text ```text
System: System:
service: active|inactive|unknown service: active|inactive|unknown
Network tuning:
enabled, apply_on_daemon_start, tcp_bbr_mode, conf_path, modules_load_path,
значения управляемых ключей, bbr_available, bbr_active, drift[, reason][, warning]
container: <name> container: <name>
container_id: <id> container_id: <id>
pid: <pid> pid: <pid>
netns: /proc/<pid>/ns/net netns: /proc/<pid>/ns/net
interface: <iface> interface: <iface>
Network tuning:
enabled, apply_on_daemon_start, tcp_bbr_mode, conf_path, modules_load_path,
значения управляемых ключей, bbr_available, bbr_active, drift[, reason][, warning]
Peers: Peers:
total: N total: N
- <peer ip> - <peer ip>
@@ -493,9 +493,11 @@ Reboot scheduler:
enabled, window_start_utc, window_end_utc, service, planned enabled, window_start_utc, window_end_utc, service, planned
``` ```
Блок `Network tuning` печатается между строкой `service` и строками `container`/`pid`/`netns`/`interface`, хотя последние логически относятся к `System`. Это текущее фактическое поведение; порядок зафиксирован здесь, чтобы автоматические проверки опирались на реальность. Блок `System` обязан оставаться неразрывным: строки `container`/`container_id`/`pid`/`netns`/`interface` имеют тот же отступ, что и поля любой другой секции, поэтому открытие постороннего блока между ними и заголовком `System` делает вывод неоднозначным при чтении.
Если контейнер обнаружить не удалось, `status` не завершается ошибкой, а переходит в degraded-режим: секции `Peers` и `WireGuard clients` печатают `unavailable`, `Traffic actual` показывает `drift: unknown`, блок `Docker/Amnezia port path` заполняется значениями `unknown`, а блок `Peer traffic counters` не печатается вовсе. Секции host firewall, anti-torrent, swap, conntrack, событий и reboot остаются на месте. Если контейнер обнаружить не удалось, `status` не завершается ошибкой, а переходит в degraded-режим. Строки `container`/`pid`/`netns`/`interface` печатаются со значениями `n/a`, секции `Peers` и `WireGuard clients` печатают `unavailable`, `Traffic actual` показывает `drift: unknown`, блок `Docker/Amnezia port path` заполняется значениями `unknown`, а блок `Peer traffic counters` не печатается вовсе. Секции host firewall, anti-torrent, swap, conntrack, событий и reboot остаются на месте.
Секции, общие для здорового и degraded путей, рендерятся одними и теми же функциями. Это обязательное требование: раньше они были продублированы, и расхождение форматов проявилось бы ровно в тот момент, когда контейнер лежит, то есть в худший для диагностики момент.
Отдельные секции деградируют независимо: ошибка чтения peer state не мешает вывести traffic, firewall и swap. Отдельные секции деградируют независимо: ошибка чтения peer state не мешает вывести traffic, firewall и swap.
+2 -2
View File
@@ -19,8 +19,8 @@ struct LastActionState {
last_error: Option<String>, last_error: Option<String>,
} }
const LAST_STATE_FILE: &str = "/run/awg2-harness/last_action.state"; pub const LAST_STATE_FILE: &str = "/run/awg2-harness/last_action.state";
const EVENTS_FILE: &str = "/run/awg2-harness/events.log"; pub const EVENTS_FILE: &str = "/run/awg2-harness/events.log";
const EVENTS_MAX_LINES: usize = 1000; const EVENTS_MAX_LINES: usize = 1000;
const EVENTS_MAX_BYTES: usize = 256 * 1024; const EVENTS_MAX_BYTES: usize = 256 * 1024;
+102 -145
View File
@@ -1,5 +1,9 @@
use crate::{ use crate::{
config::Config, docker, firewall, netns::build_transport, swap, sysctl_tuning, traffic, wg, config::Config,
daemon::{EVENTS_FILE, LAST_STATE_FILE},
docker, firewall,
netns::build_transport,
swap, sysctl_tuning, traffic, wg,
}; };
use std::{ use std::{
@@ -10,6 +14,9 @@ use std::{
}; };
const ONLINE_HANDSHAKE_THRESHOLD_SECS: u64 = 180; const ONLINE_HANDSHAKE_THRESHOLD_SECS: u64 = 180;
const RECENT_EVENTS_LIMIT: usize = 10;
const FORWARD_OWNERSHIP_NOTE: &str =
" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD";
pub fn print_status(cfg: &Config) -> anyhow::Result<()> { pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
println!("System:"); println!("System:");
@@ -22,26 +29,33 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
Err(_) => println!(" service: unknown"), Err(_) => println!(" service: unknown"),
} }
print_network_tuning_status(cfg); // The System block stays contiguous: container identity is printed before any other
// section opens, otherwise its two-space fields read as belonging to that section.
let target = match docker::discover_target(cfg) { let target = match docker::discover_target(cfg) {
Ok(t) => t, Ok(t) => {
println!(" container: {}", t.container_name);
println!(" container_id: {}", t.container_id);
println!(" pid: {}", t.container_pid);
println!(" netns: {}", t.netns_path);
println!(" interface: {}", t.interface_name);
Some(t)
}
Err(e) => { Err(e) => {
println!(" container: not available ({e})"); println!(" container: not available ({e})");
println!(" container_id: n/a"); println!(" container_id: n/a");
println!(" pid: n/a"); println!(" pid: n/a");
println!(" netns: n/a"); println!(" netns: n/a");
println!(" interface: n/a"); println!(" interface: n/a");
print_degraded_sections(cfg)?; None
return Ok(());
} }
}; };
println!(" container: {}", target.container_name); print_network_tuning_status(cfg);
println!(" container_id: {}", target.container_id);
println!(" pid: {}", target.container_pid); let Some(target) = target else {
println!(" netns: {}", target.netns_path); print_degraded_sections(cfg);
println!(" interface: {}", target.interface_name); return Ok(());
};
let docker_path_diag = docker::collect_port_path_diagnostics( let docker_path_diag = docker::collect_port_path_diagnostics(
&target.container_name, &target.container_name,
@@ -101,11 +115,7 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
(_, Err(e)) => println!(" unavailable ({e})"), (_, Err(e)) => println!(" unavailable ({e})"),
} }
println!("Traffic desired:"); print_traffic_desired_section(cfg);
println!(" peer_limit: {}", cfg.traffic.peer_limit);
println!(" root_rate: {}", cfg.traffic.root_rate);
println!(" ifb_name: {}", cfg.traffic.ifb_name);
println!(" class_start_id: {}", cfg.traffic.class_start_id);
println!("Traffic actual:"); println!("Traffic actual:");
match traffic::inspect_drift_summary(cfg) { match traffic::inspect_drift_summary(cfg) {
@@ -170,99 +180,13 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
Err(_) => println!(" filter statistics: unavailable"), Err(_) => println!(" filter statistics: unavailable"),
} }
println!("Host firewall:"); print_host_firewall_section(cfg);
println!(" enabled: {}", cfg.host_firewall.enabled);
println!(" ssh_port: {}/tcp", cfg.host_firewall.ssh_port);
println!(
" amnezia_port: {}/{}",
cfg.host_firewall.amnezia_port,
cfg.host_firewall.amnezia_protocols.join(",")
);
println!(
" input_policy: {}",
cfg.host_firewall
.default_input_policy
.trim()
.to_ascii_lowercase()
);
if cfg.host_firewall.enabled {
match firewall::host_firewall_drift_reason(cfg) {
Ok(None) => println!(" drift: no"),
Ok(Some(reason)) => {
println!(" drift: yes");
println!(" reason: {}", reason);
}
Err(e) => println!(" drift: unknown ({e})"),
}
}
print_docker_amnezia_port_path(&docker_path_diag); print_docker_amnezia_port_path(&docker_path_diag);
print_antitorrent_section(cfg);
println!("Anti-torrent:");
println!(" enabled: {}", cfg.anti_torrent.enabled);
println!(
" max_connections_per_peer: {}",
cfg.anti_torrent.max_connections_per_peer
);
if cfg.anti_torrent.enabled {
match firewall::antitorrent_drift_reason(cfg) {
Ok(None) => println!(" drift: no"),
Ok(Some(reason)) => {
println!(" drift: yes");
println!(" reason: {}", reason);
}
Err(e) => println!(" drift: unknown ({e})"),
}
match firewall::antitorrent_drop_counter(cfg) {
Ok(Some((packets, bytes))) => {
println!(" dropped_packets: {}", packets);
println!(" dropped_bytes: {}", bytes);
}
Ok(None) => println!(" dropped_packets: n/a"),
Err(e) => println!(" dropped_packets: inspect error ({e})"),
}
}
print_swap_status(cfg); print_swap_status(cfg);
print_conntrack_section();
let ct_count = Command::new("sysctl") print_last_actions_section();
.args(["-n", "net.netfilter.nf_conntrack_count"]) print_recent_events_section();
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "n/a".to_string());
let ct_max = Command::new("sysctl")
.args(["-n", "net.netfilter.nf_conntrack_max"])
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "n/a".to_string());
println!("Conntrack:");
println!(" nf_conntrack_count: {}", ct_count);
println!(" nf_conntrack_max: {}", ct_max);
println!("Last actions:");
let last = fs::read_to_string("/run/awg2-harness/last_action.state").unwrap_or_default();
if last.trim().is_empty() {
println!(" no persisted state");
} else {
for line in last.lines() {
println!(" {}", line);
}
}
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}");
}
}
print_reboot_section(cfg); print_reboot_section(cfg);
Ok(()) Ok(())
@@ -279,22 +203,42 @@ fn print_network_tuning_status(cfg: &Config) {
} }
} }
fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> { /// Sections that stay meaningful when the container was not discovered.
///
/// Every section shared with the healthy path is rendered by the same helper, so the two
/// paths cannot drift apart in format. Only the sections that genuinely depend on the
/// container differ, and they say so explicitly.
fn print_degraded_sections(cfg: &Config) {
println!("Peers:"); println!("Peers:");
println!(" unavailable (container/netns not discovered)"); println!(" unavailable (container/netns not discovered)");
println!("WireGuard clients:"); println!("WireGuard clients:");
println!(" unavailable (container/netns not discovered)"); println!(" unavailable (container/netns not discovered)");
print_traffic_desired_section(cfg);
println!("Traffic actual:");
println!(" drift: unknown (container/netns not discovered)");
print_host_firewall_section(cfg);
print_unknown_docker_amnezia_port_path();
print_antitorrent_section(cfg);
print_swap_status(cfg);
print_conntrack_section();
print_last_actions_section();
print_recent_events_section();
print_reboot_section(cfg);
}
fn print_traffic_desired_section(cfg: &Config) {
println!("Traffic desired:"); println!("Traffic desired:");
println!(" peer_limit: {}", cfg.traffic.peer_limit); println!(" peer_limit: {}", cfg.traffic.peer_limit);
println!(" root_rate: {}", cfg.traffic.root_rate); println!(" root_rate: {}", cfg.traffic.root_rate);
println!(" ifb_name: {}", cfg.traffic.ifb_name); println!(" ifb_name: {}", cfg.traffic.ifb_name);
println!(" class_start_id: {}", cfg.traffic.class_start_id); println!(" class_start_id: {}", cfg.traffic.class_start_id);
}
println!("Traffic actual:"); fn print_host_firewall_section(cfg: &Config) {
println!(" drift: unknown (container/netns not discovered)");
println!("Host firewall:"); println!("Host firewall:");
println!(" enabled: {}", cfg.host_firewall.enabled); println!(" enabled: {}", cfg.host_firewall.enabled);
println!(" ssh_port: {}/tcp", cfg.host_firewall.ssh_port); println!(" ssh_port: {}/tcp", cfg.host_firewall.ssh_port);
@@ -320,21 +264,18 @@ fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
Err(e) => println!(" drift: unknown ({e})"), Err(e) => println!(" drift: unknown ({e})"),
} }
} }
}
println!("Docker/Amnezia port path:"); fn print_antitorrent_section(cfg: &Config) {
println!(" docker_userland_proxy: unknown");
println!(" published_udp_port: unknown");
println!(" host_forward_policy_drop: unknown");
println!(" docker_forward_rules_present: unknown");
println!(" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD");
println!("Anti-torrent:"); println!("Anti-torrent:");
println!(" enabled: {}", cfg.anti_torrent.enabled); println!(" enabled: {}", cfg.anti_torrent.enabled);
println!( println!(
" max_connections_per_peer: {}", " max_connections_per_peer: {}",
cfg.anti_torrent.max_connections_per_peer cfg.anti_torrent.max_connections_per_peer
); );
if cfg.anti_torrent.enabled { if !cfg.anti_torrent.enabled {
return;
}
match firewall::antitorrent_drift_reason(cfg) { match firewall::antitorrent_drift_reason(cfg) {
Ok(None) => println!(" drift: no"), Ok(None) => println!(" drift: no"),
Ok(Some(reason)) => { Ok(Some(reason)) => {
@@ -353,49 +294,54 @@ fn print_degraded_sections(cfg: &Config) -> anyhow::Result<()> {
} }
} }
print_swap_status(cfg); fn print_conntrack_section() {
println!("Conntrack:"); println!("Conntrack:");
let ct_count = Command::new("sysctl") println!(
.args(["-n", "net.netfilter.nf_conntrack_count"]) " nf_conntrack_count: {}",
.output() read_sysctl_or_na("net.netfilter.nf_conntrack_count")
.ok() );
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) println!(
.unwrap_or_else(|| "n/a".to_string()); " nf_conntrack_max: {}",
let ct_max = Command::new("sysctl") read_sysctl_or_na("net.netfilter.nf_conntrack_max")
.args(["-n", "net.netfilter.nf_conntrack_max"]) );
.output() }
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "n/a".to_string());
println!(" nf_conntrack_count: {}", ct_count);
println!(" nf_conntrack_max: {}", ct_max);
fn read_sysctl_or_na(key: &str) -> String {
Command::new("sysctl")
.args(["-n", key])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "n/a".to_string())
}
fn print_last_actions_section() {
println!("Last actions:"); println!("Last actions:");
let last = fs::read_to_string("/run/awg2-harness/last_action.state").unwrap_or_default(); let last = fs::read_to_string(LAST_STATE_FILE).unwrap_or_default();
if last.trim().is_empty() { if last.trim().is_empty() {
println!(" no persisted state"); println!(" no persisted state");
} else { return;
}
for line in last.lines() { for line in last.lines() {
println!(" {}", line); println!(" {}", line);
} }
} }
fn print_recent_events_section() {
println!("Recent events:"); println!("Recent events:");
let events = fs::read_to_string("/run/awg2-harness/events.log").unwrap_or_default(); let events = fs::read_to_string(EVENTS_FILE).unwrap_or_default();
let lines: Vec<&str> = events.lines().collect(); let lines: Vec<&str> = events.lines().collect();
if lines.is_empty() { if lines.is_empty() {
println!(" no recent events"); println!(" no recent events");
} else { return;
for l in lines.iter().rev().take(10).rev() { }
for l in lines.iter().rev().take(RECENT_EVENTS_LIMIT).rev() {
println!(" {l}"); println!(" {l}");
} }
} }
print_reboot_section(cfg);
Ok(())
}
fn print_reboot_section(cfg: &Config) { fn print_reboot_section(cfg: &Config) {
println!("Reboot scheduler:"); println!("Reboot scheduler:");
println!(" enabled: {}", cfg.reboot.enabled); println!(" enabled: {}", cfg.reboot.enabled);
@@ -487,12 +433,23 @@ fn print_docker_amnezia_port_path(diag: &docker::PortPathDiagnostics) {
" docker_forward_rules_present: {}", " docker_forward_rules_present: {}",
diag.docker_forward_rules_present.as_str() diag.docker_forward_rules_present.as_str()
); );
println!(" note: FORWARD is Docker-managed; awg2-harness does not modify host FORWARD"); println!("{FORWARD_OWNERSHIP_NOTE}");
if let Some(w) = diag.warning() { if let Some(w) = diag.warning() {
println!(" warning: {w}"); println!(" warning: {w}");
} }
} }
/// Same block as [`print_docker_amnezia_port_path`], but every probe needs the container,
/// so all four fields are reported as unknown rather than omitted.
fn print_unknown_docker_amnezia_port_path() {
println!("Docker/Amnezia port path:");
println!(" docker_userland_proxy: unknown");
println!(" published_udp_port: unknown");
println!(" host_forward_policy_drop: unknown");
println!(" docker_forward_rules_present: unknown");
println!("{FORWARD_OWNERSHIP_NOTE}");
}
fn handshake_is_online(epoch: Option<u64>, threshold_secs: u64) -> &'static str { fn handshake_is_online(epoch: Option<u64>, threshold_secs: u64) -> &'static str {
if let Some(ts) = epoch { if let Some(ts) = epoch {
let now = SystemTime::now() let now = SystemTime::now()