AmneziaWG 3.0 readiness: centralize wg CLI contract, drop dead host-side fallback

Peer discovery is now the single owner of how wg is invoked and parsed.

wg.rs:
- resolve the binary through ordered amnezia.wg_binaries (default [awg, wg]);
  awg is the real binary in the official image and wg is a symlink to it, so awg is
  tried first and wg stays for plain WireGuard containers. No probe call: resolution
  is a property of the command being run anyway.
- drop the nsenter fallback. AmneziaWG is userspace and its UAPI socket lives under
  RUNSTATEDIR/amneziawg/ in the container mount namespace, while host wireguard-tools
  looks under /var/run/wireguard/. nsenter -n enters only the network namespace, so
  that path could never return peers.
- classify dump lines structurally instead of by column count. The AWG interface line
  is longer than a peer line (14 columns on AWG 2.0, 28 on AWG 3.0) and its
  handshake/rx/tx positions hold junk-packet sizes that parse as integers, so only the
  allowed-ips column discriminates. Column 1 of an interface line is the device private
  key, which the previous heuristic rejected only incidentally.
- fix allowed-ips parsing: `show all allowed-ips` separates a peer's addresses with
  spaces, not commas, so peers with more than one allowed IP were shaped on their first
  address only and the rest ran unlimited.
- collapse the three duplicated /32-in-CIDR filters into one tokenizer shared by the
  allowed-ips, dump and pretty-print parsers.

config.rs:
- add amnezia.wg_binaries with serde default, validated as bare command names
  (non-empty, no whitespace, no path separators) since they execute inside the
  container. Existing deployed configs keep parsing unchanged.
- remove host wg from the required-tool preflight. It gated daemon startup on
  wireguard-tools for a code path that produced nothing.

status.rs:
- discover peers once instead of twice per status call and degrade gracefully like the
  neighbouring sections instead of aborting the whole output.

Tests: real AWG 3.0 / AWG 2.0 / plain WireGuard interface-line fixtures, AWG 3.0 range
keepalive, space-separated allowed-ips, binary resolution order, and a test that parses
and validates the shipped config.example.toml.
This commit is contained in:
2026-08-06 15:32:26 +05:00
parent 5edefa74b1
commit 16214f3c66
13 changed files with 566 additions and 191 deletions
+14 -20
View File
@@ -48,27 +48,25 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
cfg.host_firewall.amnezia_port,
);
let peers = wg::discover_peers(
target.container_pid,
&target.container_name,
&cfg.amnezia.peer_ipv4_cidr,
)?;
// Peer discovery is one container round-trip, shared by both sections below.
let peers = wg::discover_peers(cfg, &target);
println!("Peers:");
println!(" total: {}", peers.len());
for p in peers {
println!(" - {}", p.ipv4);
match &peers {
Ok(list) => {
println!(" total: {}", list.len());
for p in list {
println!(" - {}", p.ipv4);
}
}
Err(e) => println!(" unavailable ({e})"),
}
println!("WireGuard clients:");
println!(
" IP State Latest handshake RX TX Public key Endpoint"
);
match wg::discover_peer_runtime(
target.container_pid,
&target.container_name,
&cfg.amnezia.peer_ipv4_cidr,
) {
Ok(runtime_peers) => {
match (&peers, wg::discover_peer_runtime(cfg, &target)) {
(Ok(peers_sorted), Ok(runtime_peers)) => {
let mut by_ip: BTreeMap<String, &wg::PeerRuntime> = BTreeMap::new();
for rp in &runtime_peers {
for ip in &rp.allowed_ips {
@@ -76,11 +74,6 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
}
}
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) {
@@ -104,7 +97,8 @@ pub fn print_status(cfg: &Config) -> anyhow::Result<()> {
}
}
}
Err(e) => println!(" unavailable ({e})"),
(Err(e), _) => println!(" unavailable ({e})"),
(_, Err(e)) => println!(" unavailable ({e})"),
}
println!("Traffic desired:");