diff --git a/Cargo.lock b/Cargo.lock index 8278c00..3cf782d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-core", "futures-sink", @@ -29,8 +29,8 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64", - "bitflags", + "base64 0.22.1", + "bitflags 2.10.0", "brotli", "bytes", "bytestring", @@ -229,6 +229,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "arc-swap" version = "1.8.1" @@ -287,12 +293,24 @@ dependencies = [ "fastrand", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -400,13 +418,17 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" name = "client" version = "0.1.0" dependencies = [ + "base64 0.22.1", "console", "dirs", "futures", "futures-util", "ipnetwork", + "libc", + "netlink-packet-route 0.19.0", "registry", "reqwest", + "rtnetlink", "serde", "serde_json", "stunclient", @@ -418,6 +440,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "wireguard-control", ] [[package]] @@ -545,6 +568,32 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -675,6 +724,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -888,6 +943,12 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -997,7 +1058,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1231,7 +1292,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] @@ -1300,6 +1361,24 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1334,6 +1413,164 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-generic" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c171cd77b4ee8c7708da746ce392440cb7bcf618d122ec9ecc607b12938bf4" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-packet-wireguard" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b25b050ff1f6a1e23c6777b72db22790fe5b6b5ccfd3858672587a79876c8f" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "log", + "netlink-packet-generic", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-proto" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-request" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d83636acdf5aba609ff27a66f6a235f1bcfafe20ccdaad6a5bf9d7dfa64a811" +dependencies = [ + "netlink-packet-core", + "netlink-packet-generic", + "netlink-packet-route 0.21.0", + "netlink-packet-utils", + "netlink-sys", + "nix 0.25.1", + "once_cell", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1418,6 +1655,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1573,7 +1816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1583,7 +1826,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1628,7 +1880,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -1683,7 +1935,7 @@ version = "0.1.0" dependencies = [ "actix-web", "actix-ws", - "base64", + "base64 0.22.1", "console", "futures", "futures-util", @@ -1698,6 +1950,7 @@ dependencies = [ "tracing", "tracing-actix-web", "tracing-subscriber", + "wireguard-control", ] [[package]] @@ -1706,7 +1959,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -1754,6 +2007,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rtnetlink" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b684475344d8df1859ddb2d395dd3dac4f8f3422a1aa0725993cb375fc5caba5" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route 0.19.0", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.27.1", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1887,7 +2158,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2141,7 +2412,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2410,7 +2681,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http 1.4.0", @@ -3030,6 +3301,28 @@ version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +[[package]] +name = "wireguard-control" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a1ff6cde8cce93098564c1020e59da217c87e37b9f1bac3f539a6261a9af128" +dependencies = [ + "base64 0.13.1", + "hex", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-generic", + "netlink-packet-route 0.21.0", + "netlink-packet-utils", + "netlink-packet-wireguard", + "netlink-request", + "netlink-sys", + "nix 0.30.1", + "rand_core 0.6.4", + "x25519-dalek", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3042,6 +3335,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -3117,6 +3422,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "zerotrie" diff --git a/client/Cargo.toml b/client/Cargo.toml index 891d821..298788c 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,7 +11,7 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" thiserror = "2.0.18" thiserror-ext = "0.3.0" -tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.49.0", features = ["macros", "net", "rt-multi-thread", "signal"] } tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] } tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } @@ -22,3 +22,12 @@ url = "2.5.8" reqwest = { version = "0.13.2", features = ["json"] } stunclient = "0.4.2" ipnetwork = { version = "0.21.1", features = ["serde"] } +base64 = "0.22.1" +libc = "0.2" + +# WireGuard netlink control +wireguard-control = "1.1" + +# Network interface management +rtnetlink = "0.14" +netlink-packet-route = "0.19" diff --git a/client/src/app_state.rs b/client/src/app_state.rs deleted file mode 100644 index afe991c..0000000 --- a/client/src/app_state.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::{ops::Deref, sync::Arc}; - -#[derive(Clone)] -pub struct AppState { - pub reqwest_client: reqwest::Client, -} - -#[derive(Clone)] -pub struct Data(Arc); - -impl Deref for Data { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Data { - pub fn new(inner: T) -> Self { - Self(Arc::new(inner)) - } -} diff --git a/client/src/config.rs b/client/src/config.rs index 301aaf7..ab99e9f 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -4,19 +4,33 @@ use crate::error::{Error, Result}; use ipnetwork::IpNetwork; use serde::Deserialize; +pub const DEFAULT_KEEPALIVE: u16 = 25; +pub const INTERFACE_NAME: &str = "mesh0"; + #[derive(Deserialize)] pub struct Config { pub interface: InterfaceConfig, pub server: ServerConfig, } + #[derive(Deserialize)] pub struct InterfaceConfig { pub private_key: String, pub public_key: String, + #[serde(default = "default_listen_port")] pub listen_port: u16, - pub address: String, - pub allowed_ips: Vec, + #[serde(default = "default_keepalive")] + pub persistent_keepalive: u16, + pub allowed_ips: Option>, } + +fn default_listen_port() -> u16 { + 51820 +} +fn default_keepalive() -> u16 { + DEFAULT_KEEPALIVE +} + #[derive(Deserialize)] pub struct ServerConfig { pub ws_url: String, @@ -36,7 +50,3 @@ pub fn base_path() -> PathBuf { .unwrap_or_else(|| PathBuf::from(".")) .join("wg-mesh") } - -pub fn wg_config_path() -> PathBuf { - PathBuf::from("/etc/wireguard/mesh0.conf") -} diff --git a/client/src/error.rs b/client/src/error.rs index a80dca7..b955e46 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -29,12 +29,6 @@ pub enum ErrorKind { Url(#[from] url::ParseError), #[error("invalid url scheme: {url}")] UrlScheme { url: String }, - #[error("error writing wireguard config to {path}")] - WriteConfig { - path: PathBuf, - #[source] - source: std::io::Error, - }, #[error("STUN discovery failed: {message}")] StunDiscovery { message: String }, #[error("HTTP IP discovery failed")] @@ -45,6 +39,53 @@ pub enum ErrorKind { DiscoveryFailed, #[error("error with request")] Reqwest(#[from] reqwest::Error), + #[error("invalid base64 key: {context}")] + InvalidKey { context: String }, + #[error("error creating WireGuard interface {interface}")] + CreateInterface { + interface: String, + #[source] + source: std::io::Error, + }, + #[error("error configuring WireGuard device {interface}")] + ConfigureDevice { + interface: String, + #[source] + source: std::io::Error, + }, + #[error("error with netlink operation: {context}")] + Netlink { context: String }, + #[error("error setting interface address")] + SetAddress { + #[source] + source: rtnetlink::Error, + }, + #[error("error bringing interface up")] + SetLinkUp { + #[source] + source: rtnetlink::Error, + }, + #[error("error getting interface index for {interface}")] + GetInterface { interface: String }, + #[error("registration failed")] + Registration { + #[source] + source: reqwest::Error, + }, + #[error("error deregistering device {public_key}")] + Deregister { + public_key: String, + #[source] + source: reqwest::Error, + }, + #[error("registration failed with status {status}: {body}")] + RegistrationStatus { status: u16, body: String }, + #[error("error deleting interface")] + DeleteInterface { + name: String, + #[source] + source: rtnetlink::Error, + }, } pub type Result = core::result::Result; diff --git a/client/src/main.rs b/client/src/main.rs index df2888d..4dc09c0 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,27 +1,29 @@ -mod app_state; mod config; mod discovery; mod error; +mod netlink; mod wireguard; -use std::{fs::OpenOptions, io::Write, net::IpAddr, os::unix::fs::OpenOptionsExt}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use console::style; -use futures::StreamExt; +use futures::{StreamExt, TryFutureExt, stream::SplitStream}; use ipnetwork::IpNetwork; -use registry::{Peer, PeerMessage}; +use registry::{Peer, PeerMessage, RegisterResponse}; use serde::Serialize; use thiserror_ext::AsReport; -use tokio_tungstenite::tungstenite::Message; +use tokio::{ + net::TcpStream, + signal::{self, unix::SignalKind}, +}; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite::Message}; use tracing::level_filters::LevelFilter; use tracing_subscriber::{ EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, }; -use url::Url; use crate::{ - app_state::{AppState, Data}, - config::{Config, InterfaceConfig, wg_config_path}, + config::{Config, INTERFACE_NAME}, discovery::{PublicEndpoint, discover_public_endpoint}, error::{Error, Result}, }; @@ -34,105 +36,209 @@ pub struct RegisterRequest { pub allowed_ips: Vec, } -fn parse_ws_url(input: &Url) -> Result { - let url = input.join("/ws/peers")?; - if url.scheme() != "ws" && url.scheme() != "wss" { - return Err(Error::url_scheme(url.to_string())); - } - Ok(url.to_string()) -} - -fn write_wg_config(interface: &InterfaceConfig, peers: &[Peer]) -> Result<()> { - let path = wg_config_path(); - let config = wireguard::generate_config(interface, peers); - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&path) - .map_err(|e| Error::write_config(e, &path))?; - file.write_all(config.as_bytes()) - .map_err(|e| Error::write_config(e, &path))?; - tracing::info!("wrote {} with {} peers", path.display(), peers.len()); - Ok(()) -} - async fn register_self( - app_state: Data, + client: &reqwest::Client, endpoint: &PublicEndpoint, - config: &InterfaceConfig, - url: &str, -) -> Result<()> { - app_state - .reqwest_client - .post(url) + config: &Config, +) -> Result { + let url = format!("{}/register", &config.server.url); + + tracing::info!( + public_ip = %endpoint.ip, + port = endpoint.port, + "registering with registry" + ); + + let response = client + .post(&url) .json(&RegisterRequest { - public_key: config.public_key.clone(), + public_key: config.interface.public_key.clone(), public_ip: endpoint.ip, port: endpoint.port.to_string(), - allowed_ips: config.allowed_ips.clone(), + allowed_ips: config.interface.allowed_ips.clone().unwrap_or(vec![]), }) .send() + .await + .map_err(Error::registration)?; + + if !response.status().is_success() { + let status = response.status().as_u16(); + let body = response.text().await.unwrap_or_default(); + return Err(Error::registration_status(status, body)); + } + + let register_response: RegisterResponse = response.json().await.map_err(Error::registration)?; + + tracing::info!( + mesh_ip = %register_response.mesh_ip, + "registration successful, assigned mesh IP" + ); + + Ok(register_response.mesh_ip) +} + +async fn deregister_self(client: &reqwest::Client, config: &Config) -> Result<()> { + let url = format!( + "{}/deregister?public_key={}", + config.server.url, config.interface.public_key + ); + client + .delete(&url) + .send() .await? - .error_for_status()?; + .error_for_status() + .map_err(|e| Error::deregister(e, &config.interface.public_key))?; Ok(()) } -async fn run() -> crate::error::Result<()> { - let config = Config::load()?; - let ws_url = &config.server.ws_url; - let (ws_stream, response) = tokio_tungstenite::connect_async(ws_url) - .await - .map_err(|e| Error::ws_connect(e, ws_url))?; - let (_, mut read) = ws_stream.split(); - let app_state = Data::new(AppState { - reqwest_client: reqwest::Client::new(), - }); - tracing::info!("connected, response: {:?}", response.status()); - let endpoint = discover_public_endpoint(config.interface.listen_port).await?; - register_self( - app_state.clone(), - &endpoint, - &config.interface, - &format!("{}/register", &config.server.url), - ) - .await?; +fn configure_peer(peer: &Peer, local_public_key: &str, keepalive: u16) -> Result<()> { + if peer.public_key == local_public_key { + tracing::debug!(peer_key = %peer.public_key, "skipping self"); + return Ok(()); + } - let mut peers: Vec = Vec::new(); + let peer_key = wireguard::parse_key(&peer.public_key)?; + let endpoint: SocketAddr = format!("{}:{}", peer.public_ip, peer.port) + .parse() + .map_err(|_| { + Error::netlink(format!( + "invalid endpoint: {}:{}", + peer.public_ip, peer.port + )) + })?; + let mut allowed_ips: Vec = + vec![IpNetwork::new(std::net::IpAddr::V4(peer.mesh_ip), 32).expect("valid network")]; + allowed_ips.extend(peer.allowed_ips.iter().cloned()); + + wireguard::configure_peer(&peer_key, Some(endpoint), &allowed_ips, Some(keepalive))?; + + tracing::info!( + peer_key = %peer.public_key, + mesh_ip = %peer.mesh_ip, + endpoint = %endpoint, + "configured peer" + ); + + Ok(()) +} + +fn configure_all_peers(peers: &[Peer], local_public_key: &str, keepalive: u16) -> Result<()> { + for peer in peers { + if let Err(e) = configure_peer(peer, local_public_key, keepalive) { + tracing::warn!(peer_key = %peer.public_key, error = %e, "failed to configure peer"); + } + } + Ok(()) +} + +async fn events( + read: &mut SplitStream>>, + config: &Config, +) -> Result<()> { while let Some(msg) = read.next().await { - match msg.map_err(|e| Error::ws_read(e))? { + match msg.map_err(Error::ws_read)? { Message::Text(text) => { let server_msg: PeerMessage = - serde_json::from_str(&text).map_err(|e| Error::deserialize_json(e))?; + serde_json::from_str(&text).map_err(Error::deserialize_json)?; + match server_msg { - PeerMessage::HydratePeers { peers: new_peers } => { - tracing::info!("received {} peers", new_peers.len()); - peers = new_peers; - write_wg_config(&config.interface, &peers)?; + PeerMessage::HydratePeers { peers } => { + tracing::info!(count = peers.len(), "Received initial peer list"); + configure_all_peers( + &peers, + &config.interface.public_key, + config.interface.persistent_keepalive, + )?; } PeerMessage::PeerUpdate { peer } => { - tracing::info!("peer update: {}", peer.public_key); - if let Some(existing) = - peers.iter_mut().find(|p| p.public_key == peer.public_key) - { - *existing = peer; - } else { - peers.push(peer); - } - write_wg_config(&config.interface, &peers)?; + tracing::info!( + peer_key = %peer.public_key, + mesh_ip = %peer.mesh_ip, + "Received peer update" + ); + configure_peer( + &peer, + &config.interface.public_key, + config.interface.persistent_keepalive, + )?; } } } + Message::Ping(_) => {} + Message::Pong(_) => {} + Message::Close(_) => { + tracing::warn!("connection closed by server"); + break; + } _ => {} } } - Ok(()) } +async fn run() -> Result<()> { + let config = Config::load()?; + let private_key = wireguard::parse_key(&config.interface.private_key)?; + let endpoint = discover_public_endpoint(config.interface.listen_port).await?; + tracing::info!( + public_ip = %endpoint.ip, + public_port = endpoint.port, + "public endpoint" + ); + let http_client = reqwest::Client::new(); + let mesh_ip = register_self(&http_client, &endpoint, &config).await?; + wireguard::create_interface()?; + wireguard::configure_interface(&private_key, config.interface.listen_port)?; + + let (nl_handle, _nl_task) = netlink::connect().await?; + netlink::add_address(&nl_handle, mesh_ip, 32).await?; + netlink::set_link_up(&nl_handle).await?; + + tracing::info!( + interface = INTERFACE_NAME, + mesh_ip = %mesh_ip, + "interface configured and up" + ); + + let ws_url = &config.server.ws_url; + + let (ws_stream, response) = tokio_tungstenite::connect_async(ws_url) + .await + .map_err(|e| Error::ws_connect(e, ws_url))?; + + tracing::info!( + status = ?response.status(), + "connected to registry WebSocket" + ); + + let (_, mut read) = ws_stream.split(); + + tokio::select! { + receiver = events(&mut read, &config) => receiver?, + _ = signal::ctrl_c() => { + }, + _ = on_shutdown() => {} + }; + tracing::debug!("gracefully shutting down"); + netlink::delete_interface(&nl_handle, INTERFACE_NAME).await?; + deregister_self(&http_client, &config).await?; + tracing::info!("connection closed"); + Ok(()) +} + +async fn on_shutdown() { + use tokio::signal::unix::signal; + let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM"); + let mut sigint = signal(SignalKind::interrupt()).expect("failed to register SIGINT"); + + tokio::select! { + _ = sigterm.recv() => tracing::info!("received sigterm"), + _ = sigint.recv() => tracing::info!("received sigint") + } +} + #[tokio::main] async fn main() { let tracing_env_filter = EnvFilter::builder() diff --git a/client/src/netlink.rs b/client/src/netlink.rs new file mode 100644 index 0000000..6915ff4 --- /dev/null +++ b/client/src/netlink.rs @@ -0,0 +1,84 @@ +use std::net::Ipv4Addr; + +use futures::TryStreamExt; +use rtnetlink::Handle; + +use crate::config::INTERFACE_NAME; +use crate::error::{Error, Result}; + +async fn get_interface_index(handle: &Handle, name: &str) -> Result { + let mut links = handle.link().get().match_name(name.to_string()).execute(); + + if let Some(link) = links.try_next().await.map_err(Error::set_link_up)? { + Ok(link.header.index) + } else { + Err(Error::get_interface(name.to_string())) + } +} + +pub async fn add_address(handle: &Handle, addr: Ipv4Addr, prefix_len: u8) -> Result<()> { + let index = get_interface_index(handle, INTERFACE_NAME).await?; + + tracing::debug!( + interface = INTERFACE_NAME, + address = %addr, + prefix_len, + "adding address to interface" + ); + + match handle + .address() + .add(index, std::net::IpAddr::V4(addr), prefix_len) + .execute() + .await + { + Ok(()) => Ok(()), + Err(rtnetlink::Error::NetlinkError(e)) if e.raw_code() == -libc::EEXIST => { + tracing::debug!( + interface = INTERFACE_NAME, + address = %addr, + "address already exists on interface" + ); + Ok(()) + } + Err(e) => Err(Error::set_address(e)), + } +} + +pub async fn set_link_up(handle: &Handle) -> Result<()> { + let index = get_interface_index(handle, INTERFACE_NAME).await?; + + tracing::info!(interface = INTERFACE_NAME, "set interface up"); + + handle + .link() + .set(index) + .up() + .execute() + .await + .map_err(Error::set_link_up)?; + + Ok(()) +} + +pub async fn delete_interface(handle: &Handle, name: &str) -> Result<()> { + let index = get_interface_index(&handle, name).await?; + + handle + .link() + .del(index) + .execute() + .await + .map_err(|e| Error::delete_interface(e, name))?; + + Ok(()) +} + +pub async fn connect() -> Result<(Handle, tokio::task::JoinHandle<()>)> { + let (connection, handle, _) = rtnetlink::new_connection() + .map_err(|e| Error::netlink(format!("failed to create netlink connection: {}", e)))?; + + let join_handle = tokio::spawn(connection); + + Ok((handle, join_handle)) +} diff --git a/client/src/wireguard.rs b/client/src/wireguard.rs index 2822a06..f491a57 100644 --- a/client/src/wireguard.rs +++ b/client/src/wireguard.rs @@ -1,24 +1,117 @@ -use registry::Peer; +use std::net::SocketAddr; -use crate::config::InterfaceConfig; +use ipnetwork::IpNetwork; +use wireguard_control::{ + AllowedIp, Backend, Device, DeviceUpdate, InterfaceName, InvalidKey, Key, PeerConfigBuilder, +}; -pub fn generate_config(interface: &InterfaceConfig, peers: &[Peer]) -> String { - let mut config = format!( - "[Interface]\nPrivateKey = {}\nListenPort = {}\nAddress = {}\n", - interface.private_key, interface.listen_port, interface.address, - ); - for peer in peers { - config.push_str(&format!( - "\n[Peer]\nPublicKey = {}\nEndpoint = {}:{}\nAllowedIPs = {}\n", - peer.public_key, - peer.public_ip, - peer.port, - peer.allowed_ips - .iter() - .map(|ip| ip.to_string()) - .collect::>() - .join(", "), - )); - } - config +use crate::config::INTERFACE_NAME; +use crate::error::{Error, Result}; + +pub fn parse_key(key_b64: &str) -> Result { + Key::from_base64(key_b64).map_err(|e: InvalidKey| Error::invalid_key(e.to_string())) +} + +pub fn interface_name() -> InterfaceName { + INTERFACE_NAME.parse().expect("valid interface name") +} + +pub fn create_interface() -> Result<()> { + let name = interface_name(); + + if Device::get(&name, Backend::Kernel).is_ok() { + tracing::debug!(interface = INTERFACE_NAME, "interface already exists"); + return Ok(()); + } + + tracing::info!(interface = INTERFACE_NAME, "creating WireGuard interface"); + + DeviceUpdate::new() + .apply(&name, Backend::Kernel) + .map_err(|e| Error::create_interface(e, INTERFACE_NAME.to_string()))?; + + Ok(()) +} + +pub fn configure_interface(private_key: &Key, listen_port: u16) -> Result<()> { + let name = interface_name(); + + tracing::debug!( + interface = INTERFACE_NAME, + listen_port, + "configuring WireGuard interface" + ); + + DeviceUpdate::new() + .set_private_key(private_key.clone()) + .set_listen_port(listen_port) + .apply(&name, Backend::Kernel) + .map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))?; + + Ok(()) +} + +pub fn configure_peer( + public_key: &Key, + endpoint: Option, + allowed_ips: &[IpNetwork], + persistent_keepalive: Option, +) -> Result<()> { + let name = interface_name(); + + let allowed: Vec = allowed_ips + .iter() + .map(|ip| AllowedIp { + address: ip.ip(), + cidr: ip.prefix(), + }) + .collect(); + + let mut peer = PeerConfigBuilder::new(public_key) + .replace_allowed_ips() + .add_allowed_ips(&allowed); + if let Some(ep) = endpoint { + peer = peer.set_endpoint(ep); + } + if let Some(keepalive) = persistent_keepalive { + peer = peer.set_persistent_keepalive_interval(keepalive); + } + + tracing::debug!( + peer_key = %public_key.to_base64(), + endpoint = ?endpoint, + allowed_ips = ?allowed_ips, + "configuring peer" + ); + + DeviceUpdate::new() + .add_peer(peer) + .apply(&name, Backend::Kernel) + .map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))?; + + Ok(()) +} + +#[allow(dead_code)] +pub fn remove_peer(public_key: &Key) -> Result<()> { + let name = interface_name(); + + tracing::info!( + peer_key = %public_key.to_base64(), + "removing peer" + ); + + DeviceUpdate::new() + .remove_peer_by_key(public_key) + .apply(&name, Backend::Kernel) + .map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string()))?; + + Ok(()) +} + +#[allow(dead_code)] +pub fn get_device() -> Result { + let name = interface_name(); + Device::get(&name, Backend::Kernel) + .map_err(|e| Error::configure_device(e, INTERFACE_NAME.to_string())) } diff --git a/justfile b/justfile index 0e59899..7f89ad6 100644 --- a/justfile +++ b/justfile @@ -1,2 +1,7 @@ dev bin="registry": bacon dev -- --bin {{bin}} + +client target="debug": + cargo build -p client + sudo setcap cap_net_admin+ep ./target/{{target}}/client + ./target/{{target}}/client diff --git a/registry/Cargo.toml b/registry/Cargo.toml index 7b917f4..fc60fa9 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -21,6 +21,7 @@ tracing-actix-web = "0.7.21" tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "sync"] } thiserror = "2.0.18" thiserror-ext = "0.3.0" +wireguard-control = "1.7.1" [lib] name = "registry" diff --git a/registry/src/endpoints/deregister.rs b/registry/src/endpoints/deregister.rs new file mode 100644 index 0000000..05fdb38 --- /dev/null +++ b/registry/src/endpoints/deregister.rs @@ -0,0 +1,27 @@ +use actix_web::{HttpResponse, web}; +use serde::Deserialize; +use wireguard_control::Key; + +use crate::{ + AppState, + error::{Error, Result}, + storage::StorageImpl, +}; + +#[derive(Deserialize)] +pub struct DeregisterRequest { + public_key: String, +} + +pub async fn deregister( + app_state: web::Data, + query: web::Query, +) -> Result { + Key::from_base64(&query.public_key).map_err(|_| Error::invalid_key(&query.public_key))?; + + app_state + .storage + .deregister_device(&query.public_key) + .await?; + Ok(HttpResponse::Ok().finish()) +} diff --git a/registry/src/endpoints/mod.rs b/registry/src/endpoints/mod.rs index 7175a3d..c063063 100644 --- a/registry/src/endpoints/mod.rs +++ b/registry/src/endpoints/mod.rs @@ -1,3 +1,4 @@ +pub mod deregister; pub mod peers; pub mod register; pub mod ws; diff --git a/registry/src/endpoints/register.rs b/registry/src/endpoints/register.rs index a9b5dd0..5988ccf 100644 --- a/registry/src/endpoints/register.rs +++ b/registry/src/endpoints/register.rs @@ -4,13 +4,14 @@ use crate::{ storage::{RegisterRequest, StorageImpl}, }; use actix_web::{HttpResponse, web}; -use registry::Peer; +use registry::{Peer, RegisterResponse}; pub async fn register_peer( app_state: web::Data, request: web::Json, ) -> Result { - app_state.storage.register_device(&request).await?; + let mesh_ip = app_state.storage.register_device(&request).await?; + app_state .peer_updates .send(PeerUpdate { @@ -18,10 +19,11 @@ pub async fn register_peer( public_key: request.public_key.as_str().to_string(), public_ip: request.public_ip.to_string(), port: request.port.clone(), + mesh_ip, allowed_ips: request.allowed_ips.clone(), }, }) - .unwrap(); + .ok(); - Ok(HttpResponse::Ok().finish()) + Ok(HttpResponse::Ok().json(RegisterResponse { mesh_ip })) } diff --git a/registry/src/error.rs b/registry/src/error.rs index 4162477..ecae9d7 100644 --- a/registry/src/error.rs +++ b/registry/src/error.rs @@ -1,4 +1,5 @@ use actix_web::{HttpResponse, ResponseError}; +use serde::Serialize; use thiserror::Error; use thiserror_ext::{Box, Construct}; @@ -32,12 +33,30 @@ pub enum ErrorKind { }, #[error("error handling ws")] Ws(#[source] actix_web::Error), + #[error("IP pool exhausted: no available addresses in {pool}")] + IpPoolExhausted { pool: String }, + #[error("error deregistering device {public_key}")] + DeregisterDevice { + public_key: String, + #[source] + source: redis::RedisError, + }, + #[error("error invalid key")] + InvalidKey(String), +} + +#[derive(Serialize)] +struct ErrorResponse { + error: String, } impl ResponseError for Error { fn error_response(&self) -> actix_web::HttpResponse { match self.inner() { ErrorKind::Ws(e) => e.error_response(), + ErrorKind::InvalidKey(key) => HttpResponse::BadRequest().json(ErrorResponse { + error: format!("error invalid key: {key}"), + }), _ => HttpResponse::InternalServerError().finish(), } } diff --git a/registry/src/lib.rs b/registry/src/lib.rs index 0aeb8a0..abf87e5 100644 --- a/registry/src/lib.rs +++ b/registry/src/lib.rs @@ -1,4 +1,4 @@ mod types; mod utils; -pub use types::peer_message::*; +pub use types::peer_message::{Peer, PeerMessage, RegisterResponse}; diff --git a/registry/src/main.rs b/registry/src/main.rs index c722630..9c2cccc 100644 --- a/registry/src/main.rs +++ b/registry/src/main.rs @@ -47,6 +47,10 @@ async fn run() -> crate::error::Result<()> { "/register", web::post().to(endpoints::register::register_peer), ) + .route( + "/deregister", + web::delete().to(endpoints::deregister::deregister), + ) .route("/peers", web::get().to(endpoints::peers::get_peers)) .route("/ws/peers", web::get().to(endpoints::ws::peers::peers)) }) diff --git a/registry/src/storage/mod.rs b/registry/src/storage/mod.rs index 340a478..68ed9ef 100644 --- a/registry/src/storage/mod.rs +++ b/registry/src/storage/mod.rs @@ -1,3 +1,5 @@ +use std::net::Ipv4Addr; + use crate::error::{Error, Result}; mod valkey; @@ -10,12 +12,13 @@ pub enum Storage { } pub trait StorageImpl { - async fn register_device(&self, request: &RegisterRequest) -> Result<()>; + async fn register_device(&self, request: &RegisterRequest) -> Result; + async fn deregister_device(&self, public_key: &str) -> Result<()>; async fn get_peers(&self) -> Result>; } impl StorageImpl for Storage { - async fn register_device(&self, request: &RegisterRequest) -> Result<()> { + async fn register_device(&self, request: &RegisterRequest) -> Result { match self { Self::Valkey(storage) => storage.register_device(request).await, } @@ -26,6 +29,12 @@ impl StorageImpl for Storage { Self::Valkey(storage) => storage.get_peers().await, } } + + async fn deregister_device(&self, public_key: &str) -> Result<()> { + match self { + Self::Valkey(storage) => storage.deregister_device(public_key).await, + } + } } pub fn get_storage_from_env() -> Result { diff --git a/registry/src/storage/valkey.rs b/registry/src/storage/valkey.rs index 2cdcadb..679bcf7 100644 --- a/registry/src/storage/valkey.rs +++ b/registry/src/storage/valkey.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr}; +use futures::TryFutureExt; use ipnetwork::IpNetwork; use redis::AsyncTypedCommands; use registry::Peer; @@ -10,6 +11,10 @@ use crate::error::Result; use crate::utils::WireguardPublicKey; use crate::{error::Error, storage::StorageImpl}; +const MESH_NETWORK_BASE: [u8; 4] = [10, 100, 0, 0]; +const MESH_POOL_START: u8 = 1; +const MESH_POOL_END: u8 = 254; + pub struct ValkeyStorage { pub valkey_client: redis::Client, } @@ -23,22 +28,38 @@ pub struct RegisterRequest { } impl StorageImpl for ValkeyStorage { - async fn register_device(&self, request: &RegisterRequest) -> Result<()> { + async fn register_device(&self, request: &RegisterRequest) -> Result { let mut conn = self .valkey_client .get_multiplexed_async_connection() .await .map_err(|e| Error::valkey_get_connection(e))?; + + let peer_key = format!("peer:{}", request.public_key.as_str()); + + let existing_mesh_ip: Option = conn + .hget(&peer_key, "mesh_ip") + .await + .map_err(|e| Error::get_peer(e))?; + + let mesh_ip = if let Some(ip_str) = existing_mesh_ip { + ip_str.parse::().unwrap() + } else { + let allocated_ip = self.allocate_mesh_ip(&mut conn).await?; + allocated_ip + }; + conn.hset_multiple::<_, _, _>( - format!("peer:{}", request.public_key.as_str()), + &peer_key, &[ - ("public_ip", &request.public_ip.to_string()), + ("public_ip", request.public_ip.to_string()), ( "allowed_ips", - &serde_json::to_string(&request.allowed_ips) + serde_json::to_string(&request.allowed_ips) .map_err(|e| Error::serialize_json(e, "serializing allowed_ips"))?, ), - ("port", &request.port), + ("port", request.port.clone()), + ("mesh_ip", mesh_ip.to_string()), ], ) .await @@ -49,7 +70,8 @@ impl StorageImpl for ValkeyStorage { request.public_ip.to_string(), ) })?; - conn.sadd("peers", &request.public_key.as_str()) + + conn.sadd("peers", request.public_key.as_str()) .await .map_err(|e| { Error::add_peer( @@ -59,6 +81,25 @@ impl StorageImpl for ValkeyStorage { ) })?; + Ok(mesh_ip) + } + + async fn deregister_device(&self, public_key: &str) -> Result<()> { + let mut conn = self + .valkey_client + .get_multiplexed_async_connection() + .await + .map_err(|e| Error::valkey_get_connection(e))?; + let hash_key = format!("peer:{public_key}"); + conn.srem("peers", public_key) + .map_err(|e| Error::deregister_device(e, public_key)) + .await?; + let response = conn + .del(hash_key) + .await + .map_err(|e| Error::deregister_device(e, public_key))?; + tracing::debug!("deleted hash {keys} key(s) removed", keys = response); + Ok(()) } @@ -89,21 +130,72 @@ impl StorageImpl for ValkeyStorage { .map_err(|e| Error::get_peer(e))? .into_iter() .zip(keys.iter()) - .map(|(peer, key): (HashMap, &String)| { + .filter_map(|(peer, key): (HashMap, &String)| { let allowed_ips: Vec = peer .get("allowed_ips") .map(|s| serde_json::from_str(s).unwrap_or_default()) .unwrap_or_default(); - Peer { + let mesh_ip: Ipv4Addr = peer.get("mesh_ip")?.parse().ok()?; + Some(Peer { public_key: key.clone(), - public_ip: peer.get("public_ip").unwrap().to_string(), - port: peer.get("port").unwrap().to_string(), + public_ip: peer.get("public_ip")?.to_string(), + port: peer.get("port")?.to_string(), + mesh_ip, allowed_ips, - } + }) }) .collect(); Ok(peers) } } + +impl ValkeyStorage { + async fn allocate_mesh_ip( + &self, + conn: &mut redis::aio::MultiplexedConnection, + ) -> Result { + let keys: HashSet = conn + .smembers("peers") + .await + .map_err(|e| Error::get_peer(e))?; + + let mut assigned_ips: HashSet = HashSet::new(); + + if !keys.is_empty() { + let mut pipe = redis::pipe(); + for key in keys.iter() { + pipe.hget(format!("peer:{key}"), "mesh_ip"); + } + + let ips: Vec> = pipe + .query_async(conn) + .await + .map_err(|e| Error::get_peer(e))?; + + for ip_opt in ips { + if let Some(ip_str) = ip_opt { + if let Ok(ip) = ip_str.parse::() { + assigned_ips.insert(ip); + } + } + } + } + + for last_octet in MESH_POOL_START..=MESH_POOL_END { + let candidate = Ipv4Addr::new( + MESH_NETWORK_BASE[0], + MESH_NETWORK_BASE[1], + MESH_NETWORK_BASE[2], + last_octet, + ); + + if !assigned_ips.contains(&candidate) { + return Ok(candidate); + } + } + + Err(Error::ip_pool_exhausted("10.100.0.0/24".to_string())) + } +} diff --git a/registry/src/types/peer_message.rs b/registry/src/types/peer_message.rs index abd9f86..a8f0862 100644 --- a/registry/src/types/peer_message.rs +++ b/registry/src/types/peer_message.rs @@ -1,3 +1,5 @@ +use std::net::Ipv4Addr; + use serde::{Deserialize, Serialize}; use ipnetwork::IpNetwork; @@ -7,9 +9,15 @@ pub struct Peer { pub public_key: String, pub public_ip: String, pub port: String, + pub mesh_ip: Ipv4Addr, pub allowed_ips: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RegisterResponse { + pub mesh_ip: Ipv4Addr, +} + #[derive(Serialize, Deserialize)] #[serde(tag = "type")] pub enum PeerMessage { diff --git a/registry/src/utils/wg.rs b/registry/src/utils/wg.rs index 6efc933..b5bd45a 100644 --- a/registry/src/utils/wg.rs +++ b/registry/src/utils/wg.rs @@ -1,5 +1,5 @@ use base64::Engine; -use serde::{Deserialize, de}; +use serde::{de, Deserialize}; #[derive(Clone)] pub struct WireguardPublicKey(String);