feat: support AmneziaWG 3.1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "awg2-harness"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -881,12 +881,12 @@ mod tests {
|
||||
let raw = include_str!("../../config.example.toml");
|
||||
let cfg = toml::from_str::<Config>(raw).expect("config.example.toml parse");
|
||||
cfg.validate_config().expect("config.example.toml validate");
|
||||
assert_eq!(cfg.amnezia.wg_binaries, vec!["awg", "wg"]);
|
||||
assert!(
|
||||
cfg.amnezia
|
||||
.container_names
|
||||
.contains(&"amnezia-awg2".to_string())
|
||||
assert_eq!(
|
||||
cfg.amnezia.container_names,
|
||||
vec!["amnezia-awg2", "amnezia-awg"]
|
||||
);
|
||||
assert_eq!(cfg.amnezia.wg_binaries, vec!["awg", "wg"]);
|
||||
assert_eq!(cfg.anti_torrent.max_connections_per_peer, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+61
-25
@@ -29,10 +29,9 @@ pub struct PeerRuntime {
|
||||
pub transfer_tx_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
/// Peer state is only readable inside the container's mount namespace: AmneziaWG is a
|
||||
/// userspace implementation and its UAPI socket lives under `RUNSTATEDIR/amneziawg/`
|
||||
/// in the container filesystem. A host-side `wg` entered into the container netns via
|
||||
/// `nsenter -n` cannot reach that socket, so `docker exec` is the only valid transport.
|
||||
/// Peer state is read with the official container-side tools. This is the stable access
|
||||
/// point for both a kernel `amneziawg` interface and the `amneziawg-go` fallback, and it
|
||||
/// avoids coupling the harness to the version or presence of host-side wg/awg tools.
|
||||
fn container_transport(target: &RuntimeTarget) -> DockerExecTransport {
|
||||
DockerExecTransport {
|
||||
container: target.container_name.clone(),
|
||||
@@ -175,8 +174,8 @@ fn parse_fallback_wg_show(raw: &str, peer_cidr: &str) -> anyhow::Result<Vec<Peer
|
||||
///
|
||||
/// 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
|
||||
/// by which protocol parameters the tunnel actually has configured: AWG 3.x tools emit an
|
||||
/// extended row for an older 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
|
||||
@@ -274,15 +273,23 @@ mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// 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";
|
||||
const AWG30_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";
|
||||
|
||||
/// 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";
|
||||
const AWG30_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";
|
||||
const AWG2_CONFIG_VIA_AWG30_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 row emitted by amneziawg-tools v3.1.20260812. AWG 3.1 adds
|
||||
/// RandomTrailers and DisableCookies immediately before fwmark, for 30 fields total.
|
||||
const AWG31_INTERFACE_LINE: &str = "awg0\twPrivateKeyDevice31AAAAAAAAAAAAAAAAAAAAAA=\twPublicKeyDevice31AAAAAAAAAAAAAAAAAAAAAAA=\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)\thProtectionKey31AAAAAAAAAAAAAAAAAAAAAAAAAA=\t10-40\t120-180\t5-10\t180-200\t25-30\t5\ton\ton\toff";
|
||||
|
||||
/// Same AWG 3.1 tools with optional protection/range values disabled and both new
|
||||
/// boolean parameters off. The row width is still 30 fields.
|
||||
const AWG31_INTERFACE_LINE_DISABLED: &str = "awg0\twPrivateKeyDevice31DisabledAAAAAAAAAAAAA=\twPublicKeyDevice31DisabledAAAAAAAAAAAAAA=\t0\t0\t0\t0\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\toff\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";
|
||||
@@ -416,7 +423,7 @@ mod tests {
|
||||
#[test]
|
||||
fn parses_wg_show_dump_runtime_peers() {
|
||||
let raw = format!(
|
||||
"{AWG3_INTERFACE_LINE}\nawg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0\nawg0\tpubkey2\t(none)\t(none)\t10.8.1.27/32,192.168.1.5/32\t0\t0\t0\t0"
|
||||
"{AWG31_INTERFACE_LINE}\nawg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0\nawg0\tpubkey2\t(none)\t(none)\t10.8.1.27/32,192.168.1.5/32\t0\t0\t0\t0"
|
||||
);
|
||||
let peers = parse_wg_show_dump_runtime_peers(&raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 2);
|
||||
@@ -436,7 +443,7 @@ mod tests {
|
||||
/// AmneziaWG 3.0 prints protocol timers as ranges; the peer keepalive column may be
|
||||
/// `20-30` instead of a single integer. It is not read, but must not break parsing.
|
||||
#[test]
|
||||
fn accepts_awg3_range_persistent_keepalive() {
|
||||
fn accepts_awg30_range_persistent_keepalive() {
|
||||
let raw = "awg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t20-30";
|
||||
let peers = parse_wg_show_dump_runtime_peers(raw, "10.8.1.0/24").expect("parse");
|
||||
assert_eq!(peers.len(), 1);
|
||||
@@ -445,17 +452,45 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_awg3_interface_line() {
|
||||
let cols: Vec<&str> = AWG3_INTERFACE_LINE.split('\t').collect();
|
||||
fn rejects_awg30_interface_line() {
|
||||
let cols: Vec<&str> = AWG30_INTERFACE_LINE.split('\t').collect();
|
||||
assert_eq!(cols.len(), 28);
|
||||
assert!(!dump_line_is_peer(&cols));
|
||||
assert!(
|
||||
parse_wg_show_dump_runtime_peers(AWG3_INTERFACE_LINE, "0.0.0.0/0")
|
||||
parse_wg_show_dump_runtime_peers(AWG30_INTERFACE_LINE, "0.0.0.0/0")
|
||||
.expect("parse")
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_both_awg31_interface_variants() {
|
||||
for (raw, random_trailers, disable_cookies) in [
|
||||
(AWG31_INTERFACE_LINE, "on", "on"),
|
||||
(AWG31_INTERFACE_LINE_DISABLED, "off", "off"),
|
||||
] {
|
||||
let cols: Vec<&str> = raw.split('\t').collect();
|
||||
assert_eq!(cols.len(), 30);
|
||||
assert_eq!(cols[27], random_trailers);
|
||||
assert_eq!(cols[28], disable_cookies);
|
||||
assert_eq!(cols[29], "off");
|
||||
assert!(!dump_line_is_peer(&cols));
|
||||
assert!(
|
||||
parse_wg_show_dump_runtime_peers(raw, "0.0.0.0/0")
|
||||
.expect("parse")
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn awg31_interface_and_peer_yield_exactly_one_peer() {
|
||||
let raw = format!("{AWG31_INTERFACE_LINE}\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_awg2_interface_line() {
|
||||
let cols: Vec<&str> = AWG2_INTERFACE_LINE.split('\t').collect();
|
||||
@@ -464,8 +499,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_awg3_interface_line_without_header_protection() {
|
||||
let cols: Vec<&str> = AWG3_INTERFACE_LINE_NO_HEADER_PROTECTION
|
||||
fn rejects_awg30_interface_line_without_header_protection() {
|
||||
let cols: Vec<&str> = AWG30_INTERFACE_LINE_NO_HEADER_PROTECTION
|
||||
.split('\t')
|
||||
.collect();
|
||||
assert!(!dump_line_is_peer(&cols));
|
||||
@@ -475,14 +510,14 @@ mod tests {
|
||||
/// 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
|
||||
fn awg2_configuration_read_through_awg30_tools() {
|
||||
let raw = format!("{AWG2_CONFIG_VIA_AWG30_TOOLS_INTERFACE_LINE}\n{PEER_LINE}");
|
||||
let cols: Vec<&str> = AWG2_CONFIG_VIA_AWG30_TOOLS_INTERFACE_LINE
|
||||
.split('\t')
|
||||
.collect();
|
||||
assert_eq!(
|
||||
cols.len(),
|
||||
AWG3_INTERFACE_LINE.split('\t').count(),
|
||||
AWG30_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));
|
||||
@@ -496,7 +531,7 @@ mod tests {
|
||||
#[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"
|
||||
"{AWG31_INTERFACE_LINE}\n{PEER_LINE}\n{AWG30_INTERFACE_LINE}\n{AWG2_INTERFACE_LINE}\n{WG_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);
|
||||
@@ -506,9 +541,10 @@ mod tests {
|
||||
|
||||
#[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());
|
||||
for raw in [AWG31_INTERFACE_LINE, AWG31_INTERFACE_LINE_DISABLED] {
|
||||
let peers = parse_wg_show_dump_runtime_peers(raw, "10.8.1.0/24").expect("parse");
|
||||
assert!(peers.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncated or garbled rows must be skipped, not partially interpreted.
|
||||
@@ -536,7 +572,7 @@ mod tests {
|
||||
#[test]
|
||||
fn never_exposes_interface_private_key_as_peer() {
|
||||
let raw = format!(
|
||||
"{AWG3_INTERFACE_LINE}\n{AWG2_INTERFACE_LINE}\n{WG_INTERFACE_LINE}\nawg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0"
|
||||
"{AWG31_INTERFACE_LINE}\n{AWG31_INTERFACE_LINE_DISABLED}\n{AWG30_INTERFACE_LINE}\n{AWG2_INTERFACE_LINE}\n{WG_INTERFACE_LINE}\nawg0\tpubkey1\t(none)\t1.2.3.4:51820\t10.8.1.10/32\t1715000000\t123\t456\t0"
|
||||
);
|
||||
let peers = parse_wg_show_dump_runtime_peers(&raw, "0.0.0.0/0").expect("parse");
|
||||
assert_eq!(peers.len(), 1);
|
||||
|
||||
Reference in New Issue
Block a user