docs+test(wg): column count never identifies an AWG version [P2]
Both the code comment and the docs said the interface row is "14 columns for AWG 2.0, 28 for AWG 3.0". That reads as a version signal and it is not one. The width of the row is decided by the version of amneziawg-tools doing the printing, not by which protocol parameters the tunnel has configured: AWG 3.0 tools emit the wide row for an AWG 2.0 configuration too, with the newer fields simply zero, empty or (none). Any rule of the form "N columns means version X" is wrong by construction. The classifier never relied on column count for that purpose - it discriminates on the allowed-ips column - so this is a correction to what the code claims about itself and to what the docs would lead a future change to assume. Five fixtures added from the audit's list, all previously uncovered: - AWG 3.0 interface row with header protection disabled: the key column is (none) and the range fields collapse to single values; - an AWG 2.0 configuration read through AWG 3.0 tools, asserted to have the same width as a native AWG 3.0 row and to still be rejected while its peer parses; - a dump containing several interfaces, each with its own peers; - an interface with no peers at all; - malformed rows - truncated, non-numeric handshake, non-CIDR allowed-ips - which must be skipped rather than partially interpreted. Docs also pin what was verified and against which versions, rather than stating it as timeless: AmneziaVPN 5.0.0.5 and the master branches of amneziawg-tools and amneziawg-go at the time of the change. amnezia-awg3 is now described as what it is - not a confirmed official name. AmneziaVPN 5.0.0.5 added AWG 3.0 support but still exposes only Awg and Awg2 container variants. It stays in the example list on purpose, and the reasoning is written down: a name that does not match costs nothing because the search continues down the list, while a name missing from the list produces a silent "container not found". This is a deliberate deviation from the audit's suggestion to build defaults only from names in use today. cargo fmt --check, clippy -D warnings, and cargo test --locked --workspace --release all clean: 163 + 4 passed.
This commit is contained in:
+84
-8
@@ -173,10 +173,15 @@ fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer
|
||||
|
||||
/// Tells a peer line of `wg show all dump` apart from the interface line.
|
||||
///
|
||||
/// Column count alone is not a discriminator: the AmneziaWG interface line carries the
|
||||
/// AWG parameters (14 columns for AWG 2.0, 28 for AWG 3.0) and is therefore longer than
|
||||
/// a peer line, while its handshake/rx/tx positions hold junk-packet sizes that parse as
|
||||
/// integers just as well. The allowed-ips column is the reliable signal — on an interface
|
||||
/// Column count is not a discriminator and must never be used as one. The width of the
|
||||
/// interface line is decided by the version of `amneziawg-tools` doing the printing, not
|
||||
/// by which protocol parameters the tunnel actually has configured: AWG 3.0 tools emit the
|
||||
/// extended row for an AWG 2.0 configuration too, with the newer fields simply zero or
|
||||
/// empty. Any rule of the form "N columns means version X" is wrong by construction.
|
||||
///
|
||||
/// The interface line is also *longer* than a peer line, and its handshake/rx/tx positions
|
||||
/// hold junk-packet sizes that parse as integers just as well, so those columns cannot
|
||||
/// separate the two either. The allowed-ips column is the reliable signal: on an interface
|
||||
/// line it holds `junk_packet_count`, which is never `(none)` and never a CIDR list.
|
||||
///
|
||||
/// This matters beyond correctness: column 1 of an interface line is the device private
|
||||
@@ -268,16 +273,26 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Interface line of `wg show all dump` produced by AmneziaWG 3.0 tools: 28 columns,
|
||||
/// AWG parameters appended after the listen port and fwmark last.
|
||||
/// Interface line printed by AmneziaWG 3.0 tools with header protection enabled.
|
||||
const AWG3_INTERFACE_LINE: &str = "awg0\twPrivateKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAA=\twPublicKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t4\t40\t70\t0\t0\t0\t0\t1-5\t2-6\t3-7\t4-8\t(null)\t(null)\t(null)\t(null)\t(null)\thProtectionKeyAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t10-40\t120-180\t5-10\t180-200\t25-30\t5\toff";
|
||||
|
||||
/// Interface line produced by AmneziaWG 2.0 tools: 14 columns.
|
||||
/// Same tools, header protection disabled: the key column is `(none)` and the AWG 3.0
|
||||
/// range fields collapse to single values.
|
||||
const AWG3_INTERFACE_LINE_NO_HEADER_PROTECTION: &str = "awg0\twPrivateKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAA=\twPublicKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t4\t40\t70\t0\t0\t0\t0\t1\t2\t3\t4\t(null)\t(null)\t(null)\t(null)\t(null)\t(none)\t0\t120\t5\t180\t25\t5\toff";
|
||||
|
||||
/// An AWG 2.0 *configuration* read through AWG 3.0 tools: still the wide row, with the
|
||||
/// newer fields zeroed. This is precisely why column count cannot identify a version.
|
||||
const AWG2_CONFIG_VIA_AWG3_TOOLS_INTERFACE_LINE: &str = "awg0\twPrivateKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAA=\twPublicKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t4\t40\t70\t0\t0\t0\t0\t1\t2\t3\t4\t(null)\t(null)\t(null)\t(null)\t(null)\t(none)\t0\t0\t0\t0\t0\t0\toff";
|
||||
|
||||
/// Interface line printed by AmneziaWG 2.0 tools.
|
||||
const AWG2_INTERFACE_LINE: &str = "awg0\twPrivateKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAA=\twPublicKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t4\t40\t70\t0\t0\t1\t2\t3\t4\toff";
|
||||
|
||||
/// Interface line produced by plain WireGuard tools: 5 columns.
|
||||
/// Interface line printed by plain WireGuard tools.
|
||||
const WG_INTERFACE_LINE: &str = "wg0\twPrivateKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAA=\twPublicKeyDeviceAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\toff";
|
||||
|
||||
const PEER_LINE: &str =
|
||||
"awg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0";
|
||||
|
||||
struct TestTransport {
|
||||
replies: Mutex<BTreeMap<String, anyhow::Result<String>>>,
|
||||
}
|
||||
@@ -444,6 +459,67 @@ mod tests {
|
||||
assert!(!dump_line_is_peer(&cols));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_awg3_interface_line_without_header_protection() {
|
||||
let cols: Vec<&str> = AWG3_INTERFACE_LINE_NO_HEADER_PROTECTION
|
||||
.split('\t')
|
||||
.collect();
|
||||
assert!(!dump_line_is_peer(&cols));
|
||||
}
|
||||
|
||||
/// The case that makes "column count means protocol version" wrong: an AWG 2.0 tunnel
|
||||
/// read through AWG 3.0 tools still prints the wide interface row. Peers must parse
|
||||
/// and the interface row must be dropped, exactly as with a native AWG 3.0 tunnel.
|
||||
#[test]
|
||||
fn awg2_configuration_read_through_awg3_tools() {
|
||||
let raw = format!("{AWG2_CONFIG_VIA_AWG3_TOOLS_INTERFACE_LINE}\n{PEER_LINE}");
|
||||
let cols: Vec<&str> = AWG2_CONFIG_VIA_AWG3_TOOLS_INTERFACE_LINE
|
||||
.split('\t')
|
||||
.collect();
|
||||
assert_eq!(
|
||||
cols.len(),
|
||||
AWG3_INTERFACE_LINE.split('\t').count(),
|
||||
"an AWG 2.0 config through AWG 3.0 tools has the same width as AWG 3.0"
|
||||
);
|
||||
assert!(!dump_line_is_peer(&cols));
|
||||
|
||||
let peers = parse_wg_show_dump_runtime_peers(&raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 1);
|
||||
assert_eq!(peers[0].public_key, "pubkey1");
|
||||
}
|
||||
|
||||
/// Several interfaces in one `show all dump`, each with its own peers.
|
||||
#[test]
|
||||
fn parses_multiple_interfaces_in_one_dump() {
|
||||
let raw = format!(
|
||||
"{AWG3_INTERFACE_LINE}\n{PEER_LINE}\n{AWG2_INTERFACE_LINE}\nwg0\tpubkey2\t(none)\t5.6.7.8:51820\t10.8.1.20/32\t1715000001\t7\t8\t0"
|
||||
);
|
||||
let peers = parse_wg_show_dump_runtime_peers(&raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 2);
|
||||
assert_eq!(peers[0].allowed_ips, vec!["10.8.1.10".to_string()]);
|
||||
assert_eq!(peers[1].allowed_ips, vec!["10.8.1.20".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_without_peers_yields_no_peers() {
|
||||
let peers =
|
||||
parse_wg_show_dump_runtime_peers(AWG3_INTERFACE_LINE, "10.8.1.0/24").expect("parse");
|
||||
assert!(peers.is_empty());
|
||||
}
|
||||
|
||||
/// Truncated or garbled rows must be skipped, not partially interpreted.
|
||||
#[test]
|
||||
fn malformed_rows_are_skipped() {
|
||||
let raw = format!(
|
||||
"\nnot-a-dump-line\nawg0\tpubkey3\t(none)\n\
|
||||
awg0\tpubkey4\t(none)\t1.2.3.4:51820\t10.8.1.30/32\tnot-a-number\t1\t2\t0\n\
|
||||
awg0\tpubkey5\t(none)\t1.2.3.4:51820\tnot-a-cidr\t1715000000\t1\t2\t0\n{PEER_LINE}"
|
||||
);
|
||||
let peers = parse_wg_show_dump_runtime_peers(&raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 1);
|
||||
assert_eq!(peers[0].public_key, "pubkey1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_plain_wireguard_interface_line() {
|
||||
let cols: Vec<&str> = WG_INTERFACE_LINE.split('\t').collect();
|
||||
|
||||
Reference in New Issue
Block a user