From 96b54db060e3be7107366d092a06124cabcd7a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 12 Sep 2023 14:14:46 +0200 Subject: [PATCH] Wireguard listener (#3868) * wip * wip * Most channels are in place * tidy * Send data to tunnel * wip: adding in boringtun * Handle timers * Add consume_wg * Split into mod * Reorder * Comments * Refine channel handling * Sort out dependency conflict * Move wireguard listener in gateway beind a feature flag --- Cargo.lock | 194 ++++++++++++++++++++++--------- Cargo.toml | 1 + common/wireguard/Cargo.toml | 26 +++++ common/wireguard/src/event.rs | 33 ++++++ common/wireguard/src/lib.rs | 137 ++++++++++++++++++++++ common/wireguard/src/tun.rs | 210 ++++++++++++++++++++++++++++++++++ gateway/Cargo.toml | 4 + gateway/src/node/mod.rs | 5 + sdk/rust/nym-sdk/Cargo.toml | 2 +- 9 files changed, 558 insertions(+), 54 deletions(-) create mode 100644 common/wireguard/Cargo.toml create mode 100644 common/wireguard/src/event.rs create mode 100644 common/wireguard/src/lib.rs create mode 100644 common/wireguard/src/tun.rs diff --git a/Cargo.lock b/Cargo.lock index d99d5fbbd7..054651b1eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,7 +36,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.3", - "base64 0.21.2", + "base64 0.21.4", "bitflags 1.3.2", "brotli", "bytes", @@ -631,6 +631,17 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -819,9 +830,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -1030,6 +1041,29 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" +[[package]] +name = "boringtun" +version = "0.6.0" +source = "git+https://github.com/cloudflare/boringtun?rev=e1d6360d6ab4529fc942a078e4c54df107abe2ba#e1d6360d6ab4529fc942a078e4c54df107abe2ba" +dependencies = [ + "aead 0.5.2", + "base64 0.13.1", + "blake2 0.10.6", + "chacha20poly1305 0.10.1", + "hex", + "hmac 0.12.1", + "ip_network", + "ip_network_table", + "libc", + "nix 0.25.1", + "parking_lot 0.12.1", + "rand_core 0.6.4", + "ring", + "tracing", + "untrusted 0.9.0", + "x25519-dalek 2.0.0", +] + [[package]] name = "brotli" version = "3.3.4" @@ -1096,9 +1130,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" dependencies = [ "serde", ] @@ -2107,18 +2141,31 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0-rc.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba9852b42210c7538b75484f9daa0655e9a3ac04f693747bb0f02cf3cfe16" +checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" dependencies = [ "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", "fiat-crypto", - "packed_simd_2", "platforms", + "rustc_version 0.4.0", "subtle 2.4.1", "zeroize", ] +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -3085,9 +3132,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" [[package]] name = "figment" @@ -4214,6 +4261,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + +[[package]] +name = "ip_network_table" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" +dependencies = [ + "ip_network", + "ip_network_table-deps-treebitmap", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + [[package]] name = "ipconfig" version = "0.3.1" @@ -4508,12 +4577,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - [[package]] name = "libm" version = "0.2.7" @@ -4699,7 +4762,7 @@ version = "0.44.0" source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" dependencies = [ "asynchronous-codec", - "base64 0.21.2", + "base64 0.21.4", "byteorder", "bytes", "fnv", @@ -4729,7 +4792,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70b34b6da8165c0bde35c82db8efda39b824776537e73973549e76cadb3a77c5" dependencies = [ "asynchronous-codec", - "base64 0.21.2", + "base64 0.21.4", "byteorder", "bytes", "either", @@ -5806,6 +5869,18 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg 1.1.0", + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -5913,7 +5988,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg 1.1.0", - "libm 0.2.7", + "libm", ] [[package]] @@ -6187,7 +6262,7 @@ name = "nym-client-core" version = "1.1.15" dependencies = [ "async-trait", - "base64 0.21.2", + "base64 0.21.4", "cfg-if", "dashmap 5.5.0", "dirs 4.0.0", @@ -6510,6 +6585,7 @@ dependencies = [ "nym-task", "nym-types", "nym-validator-client", + "nym-wireguard", "once_cell", "pretty_env_logger", "rand 0.7.3", @@ -7442,6 +7518,21 @@ dependencies = [ "nym-node-tester-wasm", ] +[[package]] +name = "nym-wireguard" +version = "0.1.0" +dependencies = [ + "async-recursion", + "base64 0.21.4", + "boringtun", + "bytes", + "futures", + "log", + "nym-task", + "tap", + "tokio", +] + [[package]] name = "object" version = "0.31.1" @@ -7698,16 +7789,6 @@ dependencies = [ "sha2 0.10.7", ] -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if", - "libm 0.1.4", -] - [[package]] name = "pairing" version = "0.20.0" @@ -8224,13 +8305,13 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b6a5217beb0ad503ee7fa752d451c905113d70721b937126158f3106a48cc1" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", ] [[package]] @@ -8792,7 +8873,7 @@ version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -8865,7 +8946,7 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] @@ -9036,7 +9117,7 @@ dependencies = [ "log", "netlink-packet-route", "netlink-proto", - "nix", + "nix 0.24.3", "thiserror", "tokio", ] @@ -9226,7 +9307,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", ] [[package]] @@ -9236,7 +9317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -9246,7 +9327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -9374,7 +9455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -9384,7 +9465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -9828,14 +9909,14 @@ dependencies = [ [[package]] name = "snow" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccba027ba85743e09d15c03296797cad56395089b832b48b5a5217880f57733" +checksum = "0c9d1425eb528a21de2755c75af4c9b5d57f50a0d4c3b7f1828a4cd03f8ba155" dependencies = [ "aes-gcm 0.9.4", "blake2 0.10.6", "chacha20poly1305 0.9.1", - "curve25519-dalek 4.0.0-rc.1", + "curve25519-dalek 4.1.0", "rand_core 0.6.4", "ring", "rustc_version 0.4.0", @@ -10874,7 +10955,7 @@ checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" dependencies = [ "async-trait", "axum", - "base64 0.21.2", + "base64 0.21.4", "bytes", "futures-core", "futures-util", @@ -11400,6 +11481,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "unwind_safe" version = "0.1.0" @@ -11412,7 +11499,7 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.21.2", + "base64 0.21.4", "log", "native-tls", "once_cell", @@ -11805,7 +11892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -11815,7 +11902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -11938,7 +12025,7 @@ dependencies = [ "tokio", "webpki 0.21.4", "webrtc-util", - "x25519-dalek 2.0.0-pre.1", + "x25519-dalek 2.0.0", "x509-parser 0.13.2", ] @@ -12047,7 +12134,7 @@ dependencies = [ "lazy_static", "libc", "log", - "nix", + "nix 0.24.3", "rand 0.8.5", "thiserror", "tokio", @@ -12347,12 +12434,13 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.1.0", "rand_core 0.6.4", + "serde", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 5099ec2098..0d166e9dea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ members = [ "common/wasm/client-core", "common/wasm/storage", "common/wasm/utils", + "common/wireguard", "explorer-api", "explorer-api/explorer-api-requests", "explorer-api/explorer-client", diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml new file mode 100644 index 0000000000..8aca7b03ad --- /dev/null +++ b/common/wireguard/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "nym-wireguard" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-recursion = "1.0.4" +base64 = "0.21.3" +# The latest version on crates.io at the time of writing this (6.0.0) has a +# version mismatch with x25519-dalek/curve25519-dalek that is resolved in the +# latest commit. So pick that for now. +#boringtun = "0.6.0" +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "e1d6360d6ab4529fc942a078e4c54df107abe2ba" } +bytes = "1.5.0" +futures = "0.3.28" +log.workspace = true +nym-task = { path = "../task" } +tap.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "net"]} diff --git a/common/wireguard/src/event.rs b/common/wireguard/src/event.rs new file mode 100644 index 0000000000..4808517434 --- /dev/null +++ b/common/wireguard/src/event.rs @@ -0,0 +1,33 @@ +use std::fmt::{Display, Formatter}; + +use bytes::Bytes; + +#[allow(unused)] +#[derive(Debug, Clone)] +pub enum Event { + /// Dumb event with no data. + Dumb, + /// IP packet received from the WireGuard tunnel that should be passed through to the corresponding virtual device/internet. + /// Original implementation also has protocol here since it understands it, but we'll have to infer it downstream + WgPacket(Bytes), + /// IP packet to be sent through the WireGuard tunnel as crafted by the virtual device. + IpPacket(Bytes), +} + +impl Display for Event { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Event::Dumb => { + write!(f, "Dumb{{}}") + } + Event::WgPacket(data) => { + let size = data.len(); + write!(f, "WgPacket{{ size={size} }}") + } + Event::IpPacket(data) => { + let size = data.len(); + write!(f, "IpPacket{{ size={size} }}") + } + } + } +} diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs new file mode 100644 index 0000000000..d0ea5bc012 --- /dev/null +++ b/common/wireguard/src/lib.rs @@ -0,0 +1,137 @@ +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; + +use base64::{engine::general_purpose, Engine as _}; +use boringtun::x25519; +use futures::StreamExt; +use log::{error, info}; +use nym_task::TaskClient; +use tap::TapFallible; +use tokio::{net::UdpSocket, sync::mpsc, task::JoinHandle}; +use tun::WireGuardTunnel; + +use crate::event::Event; + +mod event; +mod tun; + +//const WG_ADDRESS = "0.0.0.0:51820"; +const WG_ADDRESS: &str = "0.0.0.0:51822"; + +// The private key of the listener +// Corresponding public key: "WM8s8bYegwMa0TJ+xIwhk+dImk2IpDUKslDBCZPizlE=" +const PRIVATE_KEY: &str = "AEqXrLFT4qjYq3wmX0456iv94uM6nDj5ugp6Jedcflg="; + +// The public keys of the registered peers (clients) +const PEERS: &[&str; 1] = &[ + // Corresponding private key: "ILeN6gEh6vJ3Ju8RJ3HVswz+sPgkcKtAYTqzQRhTtlo=" + "NCIhkgiqxFx1ckKl3Zuh595DzIFl8mxju1Vg995EZhI=", // "mxV/mw7WZTe+0Msa0kvJHMHERDA/cSskiZWQce+TdEs=", +]; + +const MAX_PACKET: usize = 65535; + +fn init_static_dev_keys() -> (x25519::StaticSecret, x25519::PublicKey) { + // TODO: this is a temporary solution for development + let static_private_bytes: [u8; 32] = general_purpose::STANDARD + .decode(PRIVATE_KEY) + .unwrap() + .try_into() + .unwrap(); + let static_private = x25519::StaticSecret::try_from(static_private_bytes).unwrap(); + let static_public = x25519::PublicKey::from(&static_private); + info!( + "wg public key: {}", + general_purpose::STANDARD.encode(static_public) + ); + + // TODO: A single static public key is used for all peers during development + let peer_static_public_bytes: [u8; 32] = general_purpose::STANDARD + .decode(PEERS[0]) + .unwrap() + .try_into() + .unwrap(); + let peer_static_public = x25519::PublicKey::try_from(peer_static_public_bytes).unwrap(); + + (static_private, peer_static_public) +} + +fn start_wg_tunnel( + addr: SocketAddr, + udp: Arc, + static_private: x25519::StaticSecret, + peer_static_public: x25519::PublicKey, +) -> (JoinHandle, mpsc::UnboundedSender) { + let (mut tunnel, peer_tx) = WireGuardTunnel::new(udp, addr, static_private, peer_static_public); + let join_handle = tokio::spawn(async move { + tunnel.spin_off().await; + addr + }); + (join_handle, peer_tx) +} + +pub async fn start_wg_listener( + mut task_client: TaskClient, +) -> Result<(), Box> { + log::info!("Starting wireguard listener on {}", WG_ADDRESS); + let udp_socket = Arc::new(UdpSocket::bind(WG_ADDRESS).await?); + + // Setup some static keys for development + let (static_private, peer_static_public) = init_static_dev_keys(); + + tokio::spawn(async move { + // The set of active tunnels indexed by the peer's address + let mut active_peers: HashMap> = HashMap::new(); + // Each tunnel is run in its own task, and the task handle is stored here so we can remove + // it from `active_peers` when the tunnel is closed + let mut active_peers_task_handles = futures::stream::FuturesUnordered::new(); + let mut buf = [0u8; MAX_PACKET]; + + while !task_client.is_shutdown() { + tokio::select! { + _ = task_client.recv() => { + log::trace!("WireGuard listener: received shutdown"); + break; + } + // Handle tunnel closing + Some(addr) = active_peers_task_handles.next() => { + match addr { + Ok(addr) => { + info!("WireGuard listener: closed {addr:?}"); + active_peers.remove(&addr); + } + Err(err) => { + error!("WireGuard listener: error receiving shutdown from peer: {err}"); + } + } + } + // Handle incoming packets + Ok((len, addr)) = udp_socket.recv_from(&mut buf) => { + log::info!("Received {} bytes from {}", len, addr); + + if let Some(peer_tx) = active_peers.get_mut(&addr) { + log::info!("WireGuard listener: received packet from known peer"); + peer_tx.send(Event::WgPacket(buf[..len].to_vec().into())) + .tap_err(|err| log::error!("{err}")) + .unwrap(); + } else { + log::info!("WireGuard listener: received packet from unknown peer, starting tunnel"); + let (join_handle, peer_tx) = start_wg_tunnel( + addr, + udp_socket.clone(), + static_private.clone(), + peer_static_public + ); + peer_tx.send(Event::WgPacket(buf[..len].to_vec().into())) + .tap_err(|err| log::error!("{err}")) + .unwrap(); + + active_peers.insert(addr, peer_tx); + active_peers_task_handles.push(join_handle); + } + } + } + } + log::info!("WireGuard listener: shutting down"); + }); + + Ok(()) +} diff --git a/common/wireguard/src/tun.rs b/common/wireguard/src/tun.rs new file mode 100644 index 0000000000..ffe3b42e8b --- /dev/null +++ b/common/wireguard/src/tun.rs @@ -0,0 +1,210 @@ +use std::{net::SocketAddr, sync::Arc, time::Duration}; + +use async_recursion::async_recursion; +use boringtun::{ + noise::{errors::WireGuardError, Tunn, TunnResult}, + x25519, +}; +use bytes::Bytes; +use log::{debug, error, info, warn}; +use tokio::{ + net::UdpSocket, + sync::{broadcast, mpsc}, + time::timeout, +}; + +use crate::event::Event; + +const MAX_PACKET: usize = 65535; + +pub struct WireGuardTunnel { + // Incoming data from the UDP socket received in the main event loop + udp_rx: mpsc::UnboundedReceiver, + + // UDP socket used for sending data + udp: Arc, + + // Peer endpoint + addr: SocketAddr, + + // `boringtun` tunnel, used for crypto & WG protocol + wg_tunnel: Arc>, + + // Signal close + close_tx: broadcast::Sender<()>, + close_rx: broadcast::Receiver<()>, +} + +impl Drop for WireGuardTunnel { + fn drop(&mut self) { + info!("WireGuard tunnel: dropping"); + self.close(); + } +} + +impl WireGuardTunnel { + fn close(&self) { + let _ = self.close_tx.send(()); + } + + pub fn new( + udp: Arc, + addr: SocketAddr, + static_private: x25519::StaticSecret, + peer_static_public: x25519::PublicKey, + ) -> (Self, mpsc::UnboundedSender) { + let preshared_key = None; + let persistent_keepalive = None; + let index = 0; + let rate_limiter = None; + + let wg_tunnel = Arc::new(tokio::sync::Mutex::new( + Tunn::new( + static_private, + peer_static_public, + preshared_key, + persistent_keepalive, + index, + rate_limiter, + ) + .unwrap(), + )); + + // Channels with incoming data that is received by the main event loop + let (udp_tx, udp_rx) = mpsc::unbounded_channel(); + + // Signal close tunnel + let (close_tx, close_rx) = broadcast::channel(1); + + let tunnel = WireGuardTunnel { + udp_rx, + udp, + addr, + wg_tunnel, + close_tx, + close_rx, + }; + + (tunnel, udp_tx) + } + + pub async fn spin_off(&mut self) { + loop { + tokio::select! { + _ = self.close_rx.recv() => { + info!("WireGuard tunnel: received msg to close"); + break; + }, + packet = self.udp_rx.recv() => match packet { + Some(packet) => { + info!("WireGuard tunnel received: {packet}"); + match packet { + Event::WgPacket(data) => self.consume_wg(&data).await, + Event::IpPacket(data) => self.consume_eth(&data).await, + _ => {}, + } + }, + None => { + info!("WireGuard tunnel: incoming UDP stream closed, closing tunnel"); + break; + }, + }, + _ = tokio::time::sleep(Duration::from_millis(250)) => { + self.update_wg_timers().await; + }, + } + } + info!("WireGuard tunnel ({}): closed", self.addr); + } + + async fn wg_tunnel_lock(&self) -> tokio::sync::MutexGuard<'_, Tunn> { + timeout(Duration::from_millis(100), self.wg_tunnel.lock()) + .await + .unwrap() + } + + async fn consume_wg(&self, data: &[u8]) { + let mut send_buf = [0u8; MAX_PACKET]; + let mut peer = self.wg_tunnel_lock().await; + match peer.decapsulate(None, data, &mut send_buf) { + TunnResult::WriteToNetwork(packet) => { + debug!("WireGuard: writing to network"); + if let Err(err) = self.udp.send_to(packet, self.addr).await { + error!("Failed to send decapsulation-instructed packet to WireGuard endpoint: {err:?}"); + }; + // Flush pending queue + loop { + let mut send_buf = [0u8; MAX_PACKET]; + match peer.decapsulate(None, &[], &mut send_buf) { + TunnResult::WriteToNetwork(packet) => { + if let Err(err) = self.udp.send_to(packet, self.addr).await { + error!("Failed to send decapsulation-instructed packet to WireGuard endpoint: {err:?}"); + break; + }; + } + _ => { + break; + } + } + } + } + TunnResult::WriteToTunnelV4(packet, _) | TunnResult::WriteToTunnelV6(packet, _) => { + debug!("WireGuard: writing to tunnel"); + info!( + "WireGuard endpoint sent IP packet of {} bytes (not yet implemented)", + packet.len() + ); + // TODO + } + TunnResult::Done => { + debug!("WireGuard: decapsulate done"); + } + TunnResult::Err(err) => { + error!("WireGuard: decapsulate error: {err:?}"); + } + } + } + + async fn consume_eth(&self, _data: &Bytes) { + info!("WireGuard tunnel: consume_eth"); + todo!(); + } + + async fn update_wg_timers(&mut self) { + let mut send_buf = [0u8; MAX_PACKET]; + let mut tun = self.wg_tunnel_lock().await; + let tun_result = tun.update_timers(&mut send_buf); + self.handle_routine_tun_result(tun_result).await; + } + + #[async_recursion] + async fn handle_routine_tun_result<'a: 'async_recursion>(&self, result: TunnResult<'a>) { + match result { + TunnResult::WriteToNetwork(packet) => { + info!( + "Sending routine packet of {} bytes to WireGuard endpoint", + packet.len() + ); + if let Err(err) = self.udp.send_to(packet, self.addr).await { + error!("Failed to send routine packet to WireGuard endpoint: {err:?}",); + }; + } + TunnResult::Err(WireGuardError::ConnectionExpired) => { + warn!("Wireguard handshake has expired!"); + let mut buf = vec![0u8; MAX_PACKET]; + let result = self + .wg_tunnel_lock() + .await + .format_handshake_initiation(&mut buf[..], false); + self.handle_routine_tun_result(result).await + } + TunnResult::Err(err) => { + error!("Failed to prepare routine packet for WireGuard endpoint: {err:?}"); + } + TunnResult::Done => {} + other => { + warn!("Unexpected WireGuard routine task state: {other:?}"); + } + }; + } +} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index c3b4ea356e..e97f22fb2a 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -61,6 +61,7 @@ nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-wireguard = { path = "../common/wireguard" } [build-dependencies] tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] } @@ -70,3 +71,6 @@ sqlx = { version = "0.5", features = [ "macros", "migrate", ] } + +[features] +wireguard = [] diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 93bce3a6fd..e1d6b167af 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -293,6 +293,11 @@ impl Gateway { Arc::new(coconut_verifier), ); + // Once this is a bit more mature, make this a commandline flag instead of a compile time + // flag + #[cfg(feature = "wireguard")] + nym_wireguard::start_wg_listener(shutdown.subscribe()).await?; + info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); self.wait_for_interrupt(shutdown).await diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 9d8cdbe621..fb5cbf4ac8 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -54,4 +54,4 @@ parking_lot = "0.12" hex = "0.4" [features] -libp2p-vanilla = [] \ No newline at end of file +libp2p-vanilla = []