Skip to content

Commit ee19020

Browse files
committed
Add support for AmneziaWG
1 parent 0a92a6a commit ee19020

File tree

6 files changed

+166
-32
lines changed

6 files changed

+166
-32
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ name = "stun-test"
1717
bytecodec = "0.5"
1818
clap = { version = "4.5", features = [ "derive" ] }
1919
rand = "0.9"
20+
rand_chacha = "0.9"
2021
serde_json = "1.0"
2122
serde = { version = "1.0", features = [ "derive" ] }
2223
socket2 = "0.6"

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,18 @@ traversal succeeds.
117117
- `wireguard = true` (default is false) - send all traffic between peers using
118118
wireguard, eliminating added latency even under load. Only supported on linux,
119119
and requires CAP_NET_ADMIN capability or root privileges. See [Bridging over
120-
WireGuard](#Bridging-over-WireGuard).
120+
WireGuard](#Bridging-over-WireGuard) and [Bridging over
121+
AmneziaWG](#Bridging-over-AmneziaWG).
121122

122123
- `wireguard_yggdrasil_keepalive` (default is false) - whether to keep
123124
yggdrasil session alive, while wireguard bridge is active.
124125

126+
- `wireguard_types = ["wireguard", "amneziawg"]` (default is "wireguard") - set
127+
allowed wireguard implementations.
128+
129+
- `wireguard_device_params.<type>.<param> = ...` - override wireguard device
130+
configuration, including amneziawg obfuscation options.
131+
125132
- `yggdrasil_dpi` (highly experimental, prefer wireguard if available) - send
126133
network traffic over an unreliable channel, reducing latency under network
127134
load. See [Yggdrasil DPI](#Yggdrasil-DPI).
@@ -167,6 +174,25 @@ encrypted.
167174
168175
[repology]: https://repology.org/projects/
169176

177+
## Bridging over AmneziaWG
178+
179+
[AmneziaWG][amneziawg] is a fork of Wireguard adding support for traffic
180+
obfuscation. You need to install a separate kernel module and amneziawg-tools
181+
package.
182+
183+
[amneziawg]: https://docs.amnezia.org/documentation/amnezia-wg/
184+
185+
Set `wireguard_types = [ "wireguard", "amneziawg" ]` on both sides (amneziawg
186+
is prioritized when supported). Recommended obfuscation parameters are randomly
187+
selected per connection, but of course you can customize them. Note that jumper
188+
doesn't negotiate non-default parameters.
189+
190+
```toml
191+
[wireguard_device_params.amneziawg]
192+
i1 = "<b 0x1234>"
193+
...
194+
```
195+
170196
## Yggdrasil DPI
171197

172198
Currently yggdrasil router expects all communication between peers be conducted

src/bridge_wireguard.rs

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,46 @@ use tokio::{
1616
use tracing::{debug, error, event, info, instrument, warn, Level};
1717

1818
use crate::{
19+
config::ConfigInner,
1920
map_debug, map_error, map_warn,
2021
utils::{self, defer_async, CsvIter, FutureAttach},
2122
Config, SilentResult, State,
2223
};
2324

2425
const WIREGUARD_KEEPALIVE_LEN: usize = 32;
2526

26-
pub async fn verify() -> SilentResult<()> {
27+
pub fn wg_cmd(wg_type: &str) -> Option<&'static str> {
28+
match wg_type {
29+
"wireguard" => Some("wg"),
30+
"amneziawg" => Some("awg"),
31+
_ => None,
32+
}
33+
}
34+
35+
pub async fn verify(config: &ConfigInner) -> SilentResult<()> {
2736
let mut res = Ok(());
2837

29-
let spec = [
38+
let mut spec = vec![
3039
("ip link", "iproute2"),
3140
("iptables --help", "iptables"),
3241
("ip6tables --help", "iptables"),
33-
("wg help", "wireguard-tools"),
3442
("conntrack help", "conntrack-tools"),
3543
];
3644

45+
for wg_type in Iterator::chain(
46+
config.wireguard_types.iter(),
47+
config.wireguard_device_params.keys(),
48+
) {
49+
match wg_type.as_str() {
50+
"wireguard" => spec.push(("wg help", "wireguard-tools")),
51+
"amneziawg" => spec.push(("awg help", "amneziawg-tools")),
52+
_ => {
53+
error!("Wireguard type {wg_type:?} is not supported");
54+
return Err(());
55+
}
56+
}
57+
}
58+
3759
for (cmd, pkg) in spec {
3860
if run(cmd).await.is_err() {
3961
error!(
@@ -52,15 +74,30 @@ async fn run(line: impl AsRef<str>) -> SilentResult<Vec<u8>> {
5274
}
5375

5476
async fn run_stdin(line: impl AsRef<str>, stdin: impl AsRef<[u8]>) -> SilentResult<Vec<u8>> {
55-
let line = line.as_ref();
77+
run_stdin_args(line.as_ref().split(" "), stdin).await
78+
}
79+
80+
async fn run_stdin_args<S: AsRef<str>>(
81+
args: impl IntoIterator<Item = S>,
82+
stdin: impl AsRef<[u8]>,
83+
) -> SilentResult<Vec<u8>> {
5684
let stdin = stdin.as_ref();
5785

58-
debug!("Running {line:?}");
86+
let mut args = args.into_iter();
87+
let command = args.next().unwrap();
88+
let command = command.as_ref();
5989

60-
let (command, args) = line.split_once(" ").unwrap();
90+
let mut line = command.to_string();
91+
let mut cmd = tokio::process::Command::new(command);
92+
for arg in args {
93+
cmd.arg(arg.as_ref());
94+
line.push_str(" ");
95+
line.push_str(arg.as_ref());
96+
}
97+
98+
debug!("Running {line:?}");
6199

62-
let mut proc = tokio::process::Command::new(command)
63-
.args(args.split(" "))
100+
let mut proc = cmd
64101
.stdin(if !stdin.is_empty() {
65102
Stdio::piped()
66103
} else {
@@ -118,8 +155,8 @@ struct WgLink {
118155
transfer_tx: u64,
119156
}
120157

121-
async fn wg_dump(dev: &str) -> SilentResult<WgDev> {
122-
let lines = run(format!("wg show {dev} dump")).await?;
158+
async fn wg_dump(wg_cmd: &str, dev: &str) -> SilentResult<WgDev> {
159+
let lines = run(format!("{wg_cmd} show {dev} dump")).await?;
123160
let Some(Ok(line)) = lines.lines().next() else {
124161
warn!("Can't get wireguard device");
125162
return Err(());
@@ -134,8 +171,8 @@ async fn wg_dump(dev: &str) -> SilentResult<WgDev> {
134171
Ok(WgDev { listen_port })
135172
}
136173

137-
async fn wg_dump_peer(dev: &str, pub_key: &str) -> SilentResult<WgLink> {
138-
let lines = run(format!("wg show {dev} dump")).await?;
174+
async fn wg_dump_peer(wg_cmd: &str, dev: &str, pub_key: &str) -> SilentResult<WgLink> {
175+
let lines = run(format!("{wg_cmd} show {dev} dump")).await?;
139176
let Some(line) = lines
140177
.lines()
141178
.skip(1)
@@ -164,13 +201,14 @@ async fn wg_dump_peer(dev: &str, pub_key: &str) -> SilentResult<WgLink> {
164201
})
165202
}
166203

167-
pub async fn wg_genkeys() -> SilentResult<(String, String)> {
204+
pub async fn wg_genkeys(config: &Config) -> SilentResult<(String, String)> {
205+
let wg_cmd = wg_cmd(config.wireguard_types.first().unwrap()).unwrap();
168206
let trim = |key: Vec<u8>| {
169207
String::from_utf8(key.trim_ascii().into()).map_err(map_error!("Invalid key encoding"))
170208
};
171-
let priv_key = trim(run("wg genkey").await?)?;
209+
let priv_key = trim(run(format!("{wg_cmd} genkey")).await?)?;
172210

173-
let pub_key = trim(run_stdin("wg pubkey", &priv_key).await?)?;
211+
let pub_key = trim(run_stdin(format!("{wg_cmd} pubkey"), &priv_key).await?)?;
174212

175213
Ok((pub_key, priv_key))
176214
}
@@ -324,6 +362,8 @@ pub struct WgBridgeParams {
324362
pub monitor_address: Ipv6Addr,
325363
pub inet_socket: UdpSocket,
326364
pub yggdrasil_socket: UdpSocket,
365+
pub shared_secret: u64,
366+
pub wg_type: String,
327367
}
328368

329369
#[instrument(parent = None, name = "Wireguard bridge ", skip_all,
@@ -359,6 +399,9 @@ pub async fn start_bridge(
359399

360400
let remote = &params.peer_addr;
361401

402+
let wg_type = &params.wg_type;
403+
let wg_cmd = wg_cmd(wg_type).unwrap();
404+
362405
// Remove previous association of traversal socket
363406
flush_firewall(*remote).await;
364407
let _guard = defer_async(flush_firewall(*remote));
@@ -369,20 +412,50 @@ pub async fn start_bridge(
369412

370413
// TODO: Multiplex on a single wireguard device
371414
// TODO: add a queue?
372-
run(format!("ip link add dev {dev} type wireguard")).await?;
415+
run(format!("ip link add dev {dev} type {wg_type}")).await?;
373416
// Associated routing entries are removed automatically
374417
let _guard =
375418
defer_async(run(format!("ip link del dev {dev}")).attach(cancellation.get_active()));
376419

377420
run(format!("ip link set dev {dev} up")).await?;
378421

379-
let wg_port = wg_dump(dev).await?.listen_port;
422+
let wg_port = wg_dump(wg_cmd, dev).await?.listen_port;
423+
424+
let mut wg_dev_args: Vec<_> =
425+
format!("{wg_cmd} set {dev} listen-port {wg_port} private-key /dev/stdin")
426+
.split(" ")
427+
.map(|s| s.to_string())
428+
.collect();
429+
430+
if wg_type == "amneziawg" {
431+
use rand::{Rng, SeedableRng};
432+
let mut seed = params.shared_secret;
433+
let mut rand_param = |name: &str, range| {
434+
seed = seed.wrapping_add(1);
435+
let value = rand_chacha::ChaCha12Rng::seed_from_u64(seed).random_range(range);
436+
wg_dev_args.push(name.into());
437+
wg_dev_args.push(format!("{value}"));
438+
};
439+
440+
rand_param("jc", 4..=12);
441+
rand_param("jmin", 8..=8);
442+
rand_param("jmax", 80..=80);
443+
for s in ["s1", "s2"] {
444+
rand_param(s, 15..=150);
445+
}
446+
for h in ["h1", "h2", "h3", "h4"] {
447+
rand_param(h, 5..=2147483647);
448+
}
449+
}
380450

381-
run_stdin(
382-
format!("wg set {dev} listen-port {wg_port} private-key /dev/stdin"),
383-
&self_priv,
384-
)
385-
.await?;
451+
if let Some(params) = config.wireguard_device_params.get(wg_type) {
452+
for (k, v) in params {
453+
wg_dev_args.push(k.clone());
454+
wg_dev_args.push(v.clone());
455+
}
456+
}
457+
458+
run_stdin_args(wg_dev_args, &self_priv).await?;
386459

387460
setup_firewall("-I", wg_port, &params).await;
388461
let _guard =
@@ -394,7 +467,7 @@ pub async fn start_bridge(
394467
.map_err(map_warn!("Can't bind socket to device"));
395468

396469
run(format!(
397-
"wg set {dev} peer {remote_pub} persistent-keepalive 20 endpoint {remote} allowed-ips {remote_ygg_addr}/128"
470+
"{wg_cmd} set {dev} peer {remote_pub} persistent-keepalive 20 endpoint {remote} allowed-ips {remote_ygg_addr}/128"
398471
))
399472
.await?;
400473

@@ -488,7 +561,7 @@ pub async fn start_bridge(
488561
},
489562
}
490563

491-
let dump = wg_dump_peer(dev, &params.peer_pub).await?;
564+
let dump = wg_dump_peer(wg_cmd, dev, &params.peer_pub).await?;
492565

493566
if !started {
494567
if dump.latest_handshake != 0 {

src/config.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use serde::Deserialize;
22
use std::{
3-
collections::HashSet, net::Ipv6Addr, num::NonZero, path::Path, sync::Arc, time::Duration,
3+
collections::{HashMap, HashSet},
4+
net::Ipv6Addr,
5+
num::NonZero,
6+
path::Path,
7+
sync::Arc,
8+
time::Duration,
49
};
510
use tracing::error;
611

@@ -43,6 +48,8 @@ pub struct ConfigInner {
4348
pub failed_yggdrasil_traversal_limit: Option<NonZero<u32>>,
4449

4550
pub wireguard: bool,
51+
pub wireguard_types: Vec<String>,
52+
pub wireguard_device_params: HashMap<String, HashMap<String, String>>,
4653
pub wireguard_skip_checks: bool,
4754
#[serde(deserialize_with = "parse_duration")]
4855
pub wireguard_query_delay: Duration,
@@ -141,6 +148,8 @@ impl Default for ConfigInner {
141148
failed_yggdrasil_traversal_limit: None,
142149

143150
wireguard: false,
151+
wireguard_types: vec!["wireguard".to_string()],
152+
wireguard_device_params: Default::default(),
144153
wireguard_skip_checks: false,
145154
wireguard_query_delay: Duration::from_secs(2),
146155
// Time to initially wait, until connection is established
@@ -155,7 +164,7 @@ impl Default for ConfigInner {
155164
wireguard_handshake_renew_timeout: 180,
156165
wireguard_shutdown_notification_count: 5,
157166
wireguard_shutdown_notification_delay: Duration::from_secs_f64(0.3),
158-
wireguard_device_prefix: "wg-jumper".into(),
167+
wireguard_device_prefix: "wg-jumper-".into(),
159168
wireguard_device_rounds: 1000,
160169
// Keep yggdrasil session alive, while wireguard bridge is active
161170
wireguard_yggdrasil_keepalive: false,
@@ -216,7 +225,7 @@ impl ConfigInner {
216225

217226
if self.wireguard && !self.wireguard_skip_checks {
218227
#[cfg(target_os = "linux")]
219-
crate::bridge_wireguard::verify().await?;
228+
crate::bridge_wireguard::verify(&self).await?;
220229
#[cfg(not(target_os = "linux"))]
221230
{
222231
error!("Wireguard not supported on this platform");

0 commit comments

Comments
 (0)