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
+46 -7
View File
@@ -147,9 +147,17 @@ Preferred strategy:
Implementation options:
- current path: spawn host tools with namespace entry, for example `nsenter -t <pid> -n tc ...`, `nsenter -t <pid> -n ip ...`, `nsenter -t <pid> -n wg ...`;
- current path: spawn host tools with namespace entry, for example `nsenter -t <pid> -n tc ...`, `nsenter -t <pid> -n ip ...`;
- later/native: use netlink crates and explicit namespace switching where practical.
Peer state is the one exception and must never be read with a host-side binary.
AmneziaWG is a userspace implementation whose UAPI socket lives under
`RUNSTATEDIR/amneziawg/<iface>.sock` inside the container mount namespace, while host
`wireguard-tools` looks under `/var/run/wireguard/`. `nsenter -n` enters only the network
namespace, so a host `wg` reaches neither the socket nor a kernel device and always
returns an empty peer list. Peer discovery therefore uses `docker exec` exclusively, and
`wireguard-tools` is not a host dependency.
`docker exec` should not be the architectural default. It can remain as compatibility fallback if `nsenter`/host tools are unavailable, but the design assumption is that the Rust binary has host authority and controls the relevant namespace.
This also reduces operational risk: if the Amnezia container drops `tc` or `iproute2`, host-side tools can still apply rules inside the container netns.
@@ -174,15 +182,28 @@ Interface detection order:
## 7. Peer discovery
Transport is always `docker exec <container>` — see section 5 for why a host-side `wg`
cannot work.
Binary resolution uses ordered `amnezia.wg_binaries`, default `["awg", "wg"]`. The first
binary that exits successfully wins. The official AmneziaWG image ships `/usr/bin/awg` and
symlinks `/usr/bin/wg` to it, so `awg` is the real dependency; `wg` remains for plain
WireGuard containers where `awg` does not exist. There is no probe call: the resolution is
a property of the command that is being run anyway.
Primary command:
```bash
wg show all allowed-ips
awg show all allowed-ips
```
Parsing rules:
- parse lines structurally into peer public key and allowed IPs;
- a peer's allowed ips are **space-separated** in this command, comma-separated in
`show all dump` and `", "`-separated in plain `show`; all tokens from column 3 onward
must be consumed, otherwise peers with more than one allowed IP are shaped only on their
first address;
- keep IPv4 `/32` addresses within `amnezia.peer_ipv4_cidr`;
- include offline peers; online state is not a requirement for shaping;
- sort peer IP strings lexicographically before class assignment to preserve the current lexicographic class-id contract.
@@ -190,16 +211,31 @@ Parsing rules:
Fallback:
```bash
wg show
awg show
```
The fallback parser should support standard `wg show` text output by extracting `allowed ips:` lines.
`show all dump` line classification:
- a line is a peer line only if it has at least 9 tab-separated columns, column 5
(allowed ips) is `(none)` or a comma-separated CIDR list, and columns 6-8 (handshake,
rx, tx) all parse as integers;
- column count is not a discriminator on its own: the AmneziaWG interface line carries the
AWG parameters and is *longer* than a peer line (14 columns for AWG 2.0, 28 for AWG 3.0),
and its handshake/rx/tx positions hold junk-packet sizes that parse as integers;
- this matters beyond correctness — column 2 of an interface line is the device private
key, so a misclassified interface line would surface it as a peer public key;
- AWG 3.0 prints protocol timers as ranges (`20-30`). The peer keepalive column is not
read, and a range value must not break parsing.
Execution policy for production:
- do not use shell wrappers (`sh -c`) for `wg` invocation;
- do not suppress stderr via redirection in command strings;
- execute `wg` directly and handle non-zero exit codes in Rust.
- execute the binary directly and handle non-zero exit codes in Rust;
- `amnezia.wg_binaries` values are executed inside the container and are validated as bare
command names: non-empty, no whitespace, no path separators.
## 8. Desired traffic topology
@@ -463,9 +499,12 @@ The operation must be idempotent.
Unit tests:
- config validation;
- `wg show all allowed-ips` parser;
- `wg show` fallback parser;
- config validation, including `amnezia.wg_binaries` shape;
- `awg|wg show all allowed-ips` parser, including multiple space-separated allowed ips;
- `awg|wg show` fallback parser;
- `awg|wg show all dump` peer/interface line classification against AWG 3.0 (28 columns),
AWG 2.0 (14 columns) and plain WireGuard (5 columns) interface lines;
- wg binary resolution order and the error raised when no binary is available;
- CIDR filtering;
- deterministic class assignment with lexicographic class-id stability contract;
- nftables ruleset rendering and parsing for the AWG2-owned table;