diff --git a/Cargo.toml b/Cargo.toml index a7a72139..d1931b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ actix-web-lab = "0" actix-session = { version = "0", features = ["cookie-session"] } futures-util = { version = "0", default-features = false, features = ["std"] } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde_json = "1.0.80" uuid = {version = "1", features = ["serde", "v4", "fast-rng", "macro-diagnostics"] } chrono = {version = "0", features = ["serde"] } qrcode-generator = "4" diff --git a/assets/screenshot.png b/assets/screenshot.png deleted file mode 100644 index c5b73ccb..00000000 Binary files a/assets/screenshot.png and /dev/null differ diff --git a/assets/wg-easy.sketch b/assets/wg-easy.sketch deleted file mode 100644 index c72b7c3d..00000000 Binary files a/assets/wg-easy.sketch and /dev/null differ diff --git a/install.sh b/build_install.sh similarity index 100% rename from install.sh rename to build_install.sh diff --git a/release.sh b/build_release.sh similarity index 100% rename from release.sh rename to build_release.sh diff --git a/data/wg0.conf b/data/wg0.conf new file mode 100644 index 00000000..eacfad92 --- /dev/null +++ b/data/wg0.conf @@ -0,0 +1,13 @@ +# Note: Do not edit this file directly. +# Your changes will be overwritten! + +# Server +[Interface] +PrivateKey = aCjYU+CBAkIerzXXFj8hzc4ZmMDLNGrhsxqJjOwtPH0= +Address = 10.8.0.1/24 +ListenPort = 51820 +PreUp = +PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE; iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT; iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; +PreDown = +PostDown = + diff --git a/data/wg0.json b/data/wg0.json new file mode 100644 index 00000000..1220045d --- /dev/null +++ b/data/wg0.json @@ -0,0 +1,8 @@ +{ + "server": { + "privateKey": "aCjYU+CBAkIerzXXFj8hzc4ZmMDLNGrhsxqJjOwtPH0=", + "publicKey": "Gi9QI2VgCJK1ziPaE2gE/jXbiXj+Rx3mUbIq8RcHPEE=", + "address": "10.8.0.1" + }, + "clients": {} +} diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index cb49ecf5..40325a80 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -13,8 +13,8 @@ services: # - INTERFACE_NAME=wg0 # - RELEASE=rs0 # - API_PORT=8080 - - PASSWORD=123321 - - HOST= + # - PASSWORD=123321 + - HOST=localhost # - WG_PORT=51820 # - MTU=0 # - PERSISTENT_KEEPALIVE=25 diff --git a/src/main.rs b/src/main.rs index 77d1d8b7..a826a856 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ mod utils; -mod wireguard; +mod wg_rs; use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware}; use actix_web::{ @@ -10,9 +10,12 @@ use actix_web::{ }; use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; use utils::{misc, os}; use uuid::Uuid; -use wireguard::config::{WireGuard, WireguardSettings}; +use wg_rs::config::Settings; + +use wg_rs::wireguard::WireGuard; #[derive(Deserialize)] struct Name { @@ -131,23 +134,26 @@ async fn session_logout(session: Session) -> impl Responder { } #[get("/api/wireguard/client")] -async fn all_clients(wireguard: web::Data) -> impl Responder { - let memento = wireguard.get_memento().await; - let clients = wireguard.get_clients(&memento).await; +async fn all_clients(wireguard: web::Data>) -> impl Responder { + let wireguard = wireguard.read().await; + let clients = wireguard.get_clients().await; HttpResponse::Ok().body(serde_json::to_string_pretty(&clients).unwrap()) } #[get("/api/wireguard/client/{id}/qrcode.svg")] -async fn client_qr(id: web::Path, wireguard: web::Data) -> impl Responder { - let memento = wireguard.get_memento().await; - let qr = wireguard.get_client_qrcode_svg(&memento, *id).await; +async fn client_qr(id: web::Path, wireguard: web::Data>) -> impl Responder { + let wireguard = wireguard.read().await; + let qr = wireguard.get_client_qrcode_svg(*id).await; HttpResponse::Ok().body(qr) } #[get("/api/wireguard/client/{id}/configuration")] -async fn client_config(id: web::Path, wireguard: web::Data) -> impl Responder { - let memento = wireguard.get_memento().await; - let (name, config) = wireguard.get_client_configuration(&memento, *id); +async fn client_config( + id: web::Path, + wireguard: web::Data>, +) -> impl Responder { + let wireguard = wireguard.read().await; + let (name, config) = wireguard.get_client_configuration(*id); HttpResponse::Ok() .content_type("text/plain") .append_header(ContentDisposition { @@ -161,33 +167,42 @@ async fn client_config(id: web::Path, wireguard: web::Data) -> } #[post("/api/wireguard/client")] -async fn create_client(data: web::Json, wireguard: web::Data) -> impl Responder { - let mut memento = wireguard.get_memento().await; - let client = wireguard - .create_client(&mut memento, data.name.to_string()) - .await; - HttpResponse::Ok().body(serde_json::to_string_pretty(client).unwrap()) +async fn create_client( + data: web::Json, + wireguard: web::Data>, +) -> impl Responder { + let mut wireguard = wireguard.write().await; + let client = wireguard.create_client(data.name.to_string()).await; + HttpResponse::Ok().body(serde_json::to_string_pretty(&client).unwrap()) } #[delete("/api/wireguard/client/{id}")] -async fn delete_client(id: web::Path, wireguard: web::Data) -> impl Responder { - let mut memento = wireguard.get_memento().await; - wireguard.delete_client(&mut memento, *id).await; +async fn delete_client( + id: web::Path, + wireguard: web::Data>, +) -> impl Responder { + let mut wireguard = wireguard.write().await; + wireguard.delete_client(*id).await; HttpResponse::NoContent() } #[post("/api/wireguard/client/{id}/enable")] -async fn enable_client(id: web::Path, wireguard: web::Data) -> impl Responder { - // todo here is a bug that kills whole app - // let mut memento = wireguard.get_memento().await; - // wireguard.enable_client(&mut memento, *id).await; +async fn enable_client( + id: web::Path, + wireguard: web::Data>, +) -> impl Responder { + let mut wireguard = wireguard.write().await; + wireguard.enable_client(*id).await; HttpResponse::NoContent() } #[post("/api/wireguard/client/{id}/disable")] -async fn disable_client(id: web::Path, wireguard: web::Data) -> impl Responder { - // let mut memento = wireguard.get_memento().await; - // wireguard.disable_client(&mut memento, *id).await; +async fn disable_client( + id: web::Path, + wireguard: web::Data>, +) -> impl Responder { + let mut wireguard = wireguard.write().await; + wireguard.disable_client(*id).await; HttpResponse::NoContent() } @@ -206,8 +221,7 @@ async fn main() -> std::io::Result<()> { let password = get_env("PASSWORD", "123321"); let settings = get_wireguard_settings(); - let wireguard = WireGuard::new("wg0", wireguard_path, settings); - + let mut wireguard = WireGuard::new("wg0", wireguard_path, settings).await; wireguard.start().await; let index_path = format!("{}/index.html", static_dir); @@ -216,7 +230,7 @@ async fn main() -> std::io::Result<()> { let (_, manifest_file) = os::load_and_read_file_unhandled(mainfest_path.as_str()).await; let security = web::Data::new(Security { password }); - let web_wireguard = web::Data::new(wireguard); + let web_wireguard = web::Data::new(RwLock::new(wireguard)); let static_files = web::Data::new(Static { dir: static_dir, index_page, @@ -261,7 +275,7 @@ async fn main() -> std::io::Result<()> { .await } -fn get_wireguard_settings() -> WireguardSettings { +fn get_wireguard_settings() -> Settings { let interface_name = get_env("INTERFACE_NAME", "wg0"); let release_ = get_env("RELEASE", "rs0"); let api_port = get_env("API_PORT", "8080"); @@ -293,7 +307,7 @@ fn get_wireguard_settings() -> WireguardSettings { let pre_down = get_env("PRE_DOWN", ""); let post_down = get_env("POST_DOWN", ""); - WireguardSettings { + Settings { interface_name, release: release_, api_port: api_port.parse::().expect("Invalid api port was set"), diff --git a/src/utils/os.rs b/src/utils/os.rs index 08491b6d..7924e8f8 100644 --- a/src/utils/os.rs +++ b/src/utils/os.rs @@ -2,10 +2,7 @@ use std::{ffi::OsStr, string::FromUtf8Error}; use log::{info, warn}; use serde::Serialize; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - process::Command, -}; +use tokio::{fs::File, io::AsyncReadExt, process::Command}; #[derive(Serialize)] pub struct StdResult { @@ -24,7 +21,22 @@ impl StdResult { } } -pub async fn load_file_unhandled(path: &str) -> tokio::fs::File { +pub async fn load_file>(path: S) -> Result { + tokio::fs::OpenOptions::new() + .append(false) + .read(true) + .write(true) + .create(true) + .open(path.into()) + .await +} + +pub async fn read_file(mut file: File) -> Result { + let mut dst = String::new(); + file.read_to_string(&mut dst).await.map(|_| dst) +} + +pub async fn load_file_unhandled(path: &str) -> File { match tokio::fs::OpenOptions::new() .append(false) .read(true) @@ -41,7 +53,7 @@ pub async fn load_file_unhandled(path: &str) -> tokio::fs::File { } } -pub async fn load_and_read_file_unhandled(path: &str) -> (tokio::fs::File, String) { +pub async fn load_and_read_file_unhandled(path: &str) -> (File, String) { let mut buffer = String::new(); let mut file = load_file_unhandled(path).await; match file.read_to_string(&mut buffer).await { @@ -53,18 +65,9 @@ pub async fn load_and_read_file_unhandled(path: &str) -> (tokio::fs::File, Strin } } -pub async fn write_to_path_unhandled(path: &str, data: String) -> tokio::fs::File { - let mut file = load_file_unhandled(path).await; - match file.write(data.as_bytes()).await { - Ok(_) => file, - Err(error) => { - log::error!("Could not write to file: {}", error); - panic!() - } - } -} - -pub async fn exec_sh>(command: S) -> Result { +pub async fn exec_sh>(command: S) -> Result { + let command = command.into(); + info!("$ {}", command.clone()); let res = match Command::new("sh").arg("-c").arg(command).output().await { Ok(val) => val, Err(err) => return Err(err.to_string()), diff --git a/src/wg_rs.rs b/src/wg_rs.rs new file mode 100644 index 00000000..d700d25a --- /dev/null +++ b/src/wg_rs.rs @@ -0,0 +1,2 @@ +pub mod wireguard; +pub mod config; \ No newline at end of file diff --git a/src/wg_rs/config.rs b/src/wg_rs/config.rs new file mode 100644 index 00000000..5dafb6b4 --- /dev/null +++ b/src/wg_rs/config.rs @@ -0,0 +1,231 @@ +use std::{collections::HashMap, sync::Arc}; + +use chrono::{SecondsFormat, SubsecRound, Utc}; + +use futures_util::TryFutureExt; +use serde::{Deserialize, Serialize}; + +use tokio::{io::AsyncWriteExt, join}; +use uuid::Uuid; + +use crate::utils::{misc, os}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Settings { + pub interface_name: String, + pub release: String, + pub api_port: i32, + pub password: String, + pub host: String, + pub wg_port: i32, + pub mtu: i32, + pub persistent_keepalive: i32, + pub default_address: String, + pub default_address_base: String, + pub default_dns: String, + pub allowed_ips: String, + pub pre_up: String, + pub post_up: String, + pub pre_down: String, + pub post_down: String, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "camelCase"))] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Server { + pub private_key: String, + pub public_key: String, + pub address: String, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "camelCase"))] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Client { + pub uuid: Uuid, + pub name: String, + pub address: String, + pub private_key: String, + pub public_key: String, + pub pre_shared_key: String, + pub created_at: String, + pub updated_at: String, + pub enabled: bool, +} + +impl Client { + pub fn enable(&mut self) { + self.enabled = true; + self.updated_at = Utc::now() + .trunc_subsecs(3) + .to_rfc3339_opts(SecondsFormat::Millis, true) + } + pub fn disable(&mut self) { + self.enabled = false; + self.updated_at = Utc::now() + .trunc_subsecs(3) + .to_rfc3339_opts(SecondsFormat::Millis, true) + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "camelCase"))] +#[serde(rename_all(deserialize = "snake_case"))] +pub struct WebClient { + pub id: Uuid, + pub name: String, + pub enabled: bool, + pub address: String, + pub public_key: String, + pub created_at: String, + pub updated_at: String, + pub allowed_ips: String, + pub persistent_keepalive: String, + pub latest_handshake_at: String, + pub transfer_rx: i64, + pub transfer_tx: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "camelCase"))] +#[serde(rename_all(deserialize = "snake_case"))] +pub struct DumpClient { + pub public_key: String, + pub pre_shared_key: String, + pub endpoint: String, + pub allowed_ips: String, + pub latest_handshake_at: String, + pub transfer_rx: i64, + pub transfer_tx: i64, + pub persistent_keepalive: String, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all(serialize = "camelCase"))] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Memento { + pub server: Server, + pub clients: HashMap, +} + +impl Memento { + pub fn new(server: Server) -> Self { + Self { + server, + clients: HashMap::new(), + } + } +} + +#[derive(Clone)] +pub struct Accessor { + memento_path: String, + config_path: String, + settings: Arc, + memento: Memento, +} + +impl Accessor { + pub async fn new(memento_path: String, config_path: String, settings: Arc) -> Self { + let memento_str = os::load_file(memento_path.clone()) + .and_then(|file| os::read_file(file)) + .await + .expect("Could not read memento"); + + let memento = if memento_str.is_empty() { + log::info!("Creating new WireGuard configuration..."); + let private_key = os::exec_sh("wg genkey").await.expect("error genkey").stdout; + let public_key = os::exec_sh(format!("echo {} | wg pubkey", private_key)) + .await + .expect("error pubkey") + .stdout; + + let address = settings.default_address.clone(); + let memento = Memento::new(Server { + private_key, + public_key, + address, + }); + log::info!("WireGuard configuration generated..."); + memento + } else { + serde_json::from_str(&memento_str).expect("Could not serialize memento") + }; + + Self { + config_path, + memento_path, + memento, + settings, + } + } + + pub async fn set(&mut self, memento: Memento) { + let mut ser_memento = + serde_json::to_string_pretty(&memento).expect("Could not serialize memento"); + let mut ser_config = self.format_config(&memento); + + ser_memento.push('\n'); + ser_config.push('\n'); + + _ = join!( + async { + let mut file = os::load_file(self.memento_path.clone()) + .await + .expect("Could not load memento"); + file.write(ser_memento.as_bytes()) + .await + .expect("Could not write memento"); + }, + async { + let mut file = os::load_file(self.config_path.clone()) + .await + .expect("Could not load config"); + file.write(ser_config.as_bytes()) + .await + .expect("Could not write config"); + } + ); + + self.memento = memento; + } + + pub fn get(&self) -> Memento { + self.memento.clone() + } + + fn format_config(&self, config: &Memento) -> String { + let result_vec = vec![ + format!("# Note: Do not edit this file directly.\n"), + format!("# Your changes will be overwritten!\n"), + format!("\n"), + format!("# Server\n"), + format!("[Interface]\n"), + format!("PrivateKey = {}\n", config.server.private_key), + format!("Address = {}/24\n", config.server.address), + format!("ListenPort = 51820\n"), + format!("PreUp = {}\n", self.settings.pre_up), + format!("PostUp = {}\n", self.settings.post_up), + format!("PreDown = {}\n", self.settings.pre_down), + format!("PostDown = {}\n", self.settings.post_down), + ]; + let mut result = misc::multi_line(&result_vec); + + for (uid, client) in &config.clients { + if !client.enabled { + continue; + } + let result_inner = vec![ + format!("\n"), + format!("# Client: {} ({})\n", client.name, uid), + format!("[Peer]\n"), + format!("PublicKey = {}\n", client.public_key), + format!("PresharedKey = {}\n", client.pre_shared_key), + format!("AllowedIPs = {}/32\n", client.address), + ]; + result.push_str(misc::multi_line(&result_inner).as_str()); + } + result + } +} diff --git a/src/wg_rs/wireguard.rs b/src/wg_rs/wireguard.rs new file mode 100644 index 00000000..f7770c43 --- /dev/null +++ b/src/wg_rs/wireguard.rs @@ -0,0 +1,250 @@ +use std::sync::Arc; + +use chrono::{SecondsFormat, SubsecRound, Utc}; + +use uuid::Uuid; + +use crate::utils::{misc, os}; + +use super::config::{Accessor, Client, DumpClient, Settings, WebClient}; + +#[derive(Clone)] +pub struct WireGuard { + pub name: String, + settings: Arc, + memento_accessor: Accessor, +} + +impl WireGuard { + pub async fn new(name: &str, wg_path: String, settings: Settings) -> Self { + let path = format!("{}{}", wg_path, name); + let config_path = format!("{}.conf", path); + let memento_path = format!("{}.json", path); + let name = format!("{}", name); + + let counter = Arc::new(settings); + + Self { + name, + settings: counter.clone(), + memento_accessor: Accessor::new(memento_path, config_path, counter).await, + } + } + + pub async fn quick_up(&self) { + let result = os::exec_sh(format!("wg-quick up {}", &self.name)) + .await + .expect("Error"); + result.log(); + } + + pub async fn quick_down(&self) { + let result = os::exec_sh(format!("wg-quick down {}", &self.name)) + .await + .expect("Error"); + result.log(); + } + + pub async fn sync_config(&self) { + let result = os::exec_sh(format!( + "wg syncconf {} <(wg-quick strip {})", + self.name, self.name + )) + .await + .expect("Error syncing"); + result.log(); + } + + pub async fn start(&mut self) { + let memento = self.memento_accessor.get(); + self.memento_accessor.set(memento).await; + self.quick_down().await; + self.quick_up().await; + self.sync_config().await; + } + + pub async fn create_client(&mut self, name: String) -> Client { + let private_key = os::exec_sh("wg genkey").await.expect("error genkey").stdout; + + let pub_cmd = format!("echo {} | wg pubkey", private_key); + let public_key = os::exec_sh(pub_cmd).await.expect("error pubkey").stdout; + + let pre_shared_key = os::exec_sh("wg genpsk").await.expect("error genpsk").stdout; + + let mut memento = self.memento_accessor.get(); + + let mut i: u8 = 2; + let address = loop { + match memento.clients.iter().find(|(_, client)| { + client.address == format!("{}.{}", self.settings.default_address_base, i) + }) { + Some(_) => { + if i == 255 { + break None; + } + i += 1; + } + None => break Some(format!("{}.{}", self.settings.default_address_base, i)), + } + } + .expect("Maximum number of clients reached"); + + let uuid = Uuid::new_v4(); + let now = Utc::now().trunc_subsecs(3); + + let client = Client { + uuid, + name, + address, + created_at: now.to_rfc3339_opts(SecondsFormat::Secs, true), + updated_at: now.to_rfc3339_opts(SecondsFormat::Secs, true), + enabled: true, + pre_shared_key, + private_key, + public_key, + }; + + memento.clients.insert(uuid, client.clone()); + + let client = memento.clients.get(&uuid).unwrap().clone(); + self.memento_accessor.set(memento).await; + self.sync_config().await; + client + } + + pub fn get_client(&self, client_id: Uuid) -> Client { + self.memento_accessor + .get() + .clients + .get(&client_id) + .expect("Could not find user") + .clone() + } + + pub async fn get_clients(&self) -> Vec { + let res = os::exec_sh("wg show wg0 dump").await.unwrap(); + + let lines: Vec> = res + .stdout + .trim() + .split('\n') + .skip(1) + .map(|line| line.split('\t').map(|x| x.to_string()).collect()) + .collect(); + + let lines: Vec = lines + .iter() + .map(|x| DumpClient { + public_key: x[0].to_string(), + pre_shared_key: x[1].to_string(), + endpoint: x[2].to_string(), + allowed_ips: x[3].to_string(), + latest_handshake_at: x[4].to_string(), + transfer_rx: x[5].parse::().unwrap(), + transfer_tx: x[6].parse::().unwrap(), + persistent_keepalive: x[7].to_string(), + }) + .collect(); + + let memento = self.memento_accessor.get(); + let clients: Vec = memento + .clients + .iter() + .map(|(uuid, value)| { + let uuid = uuid.to_owned(); + let value = value.to_owned(); + let client_conf; + + match lines.iter().find(|x| x.public_key.eq(&value.public_key)) { + Some(conf) => client_conf = conf, + None => return None, + }; + Some(WebClient { + id: uuid, + name: value.name, + enabled: value.enabled, + address: value.address, + public_key: value.public_key, + created_at: value.created_at, + updated_at: value.updated_at, + allowed_ips: client_conf.allowed_ips.to_string(), + persistent_keepalive: client_conf.persistent_keepalive.to_string(), + latest_handshake_at: client_conf.latest_handshake_at.to_string(), + transfer_rx: client_conf.transfer_rx, + transfer_tx: client_conf.transfer_tx, + }) + }) + .filter(|x| x.is_some()) + .map(|x| x.unwrap()) + .collect(); + + clients + } + + pub fn get_client_configuration(&self, client_id: Uuid) -> (String, String) { + let memento = self.memento_accessor.get(); + let client = self.get_client(client_id); + let result_vec = vec![ + format!("[Interface]\n"), + format!("PrivateKey = {}\n", client.private_key), + format!("Address = {}/24\n", client.address), + format!("DNS = {}\n", self.settings.default_dns), + format!("MTU = {}\n", self.settings.mtu), + format!("\n"), + format!("[Peer]\n"), + format!("PublicKey = {}\n", memento.server.public_key), + format!("PresharedKey = {}\n", client.pre_shared_key), + format!("AllowedIPs = {}\n", self.settings.allowed_ips), + format!( + "PersistentKeepalive = {}\n", + self.settings.persistent_keepalive + ), + format!( + "Endpoint = {}:{}\n", + self.settings.host, self.settings.wg_port + ), + ]; + + (client.name.to_string(), misc::multi_line(&result_vec)) + } + + pub async fn delete_client(&mut self, client_id: Uuid) { + let mut memento = self.memento_accessor.get(); + if memento.clients.remove(&client_id).is_some() { + self.memento_accessor.set(memento).await; + } + } + + pub async fn enable_client(&mut self, client_id: Uuid) { + let mut memento = self.memento_accessor.get(); + match memento.clients.get_mut(&client_id) { + Some(client) => { + client.enable(); + self.memento_accessor.set(memento).await; + } + None => return, + } + } + + pub async fn disable_client(&mut self, client_id: Uuid) { + let mut memento = self.memento_accessor.get(); + match memento.clients.get_mut(&client_id) { + Some(client) => { + client.disable(); + self.memento_accessor.set(memento).await; + } + None => return, + } + } + + pub async fn get_client_qrcode_svg(&self, client_id: Uuid) -> String { + let (_, config) = self.get_client_configuration(client_id); + return qrcode_generator::to_svg_to_string_from_str( + config, + qrcode_generator::QrCodeEcc::Low, + 512, + None::<&str>, + ) + .unwrap(); + } +} diff --git a/src/wireguard.rs b/src/wireguard.rs deleted file mode 100644 index e2d6c580..00000000 --- a/src/wireguard.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod commands; -pub mod config; \ No newline at end of file diff --git a/src/wireguard/commands.rs b/src/wireguard/commands.rs deleted file mode 100644 index ea2b39ee..00000000 --- a/src/wireguard/commands.rs +++ /dev/null @@ -1,108 +0,0 @@ -// use log::info; - -// use super::config::WireguardSettings; -// use crate::{ -// utils::{os::{self}, misc}, -// wireguard::config::{self, WireguardMemento}, -// }; - -// // pub async fn wg_start(settings: WireguardSettings) { -// // let (wg, quick) = get_config().await; -// // sync_config().await; -// // let config_str = format_config(wg, quick).await; -// // os::write_to_path_unhandled("/etc/wireguard/wg0.conf", config_str).await; -// // _ = os::exec_bash(format!("wg-quick down wg0")).await; -// // _ = os::exec_bash(format!("wg-quick up wg0")).await; -// // //todo Add error feedback if host kernel does not support wireguard -// // } - -// pub async fn get_config() -> (WireguardMemento, WireguardSettings) { -// let quick = load_wireguard_settings("./quick_config.json").await; -// log::info!("Loading configuration..."); - -// let wg_config_path = &mut quick.wg_path.to_owned(); -// wg_config_path.push_str("/wg0"); - -// let wg_config: WireguardMemento; -// let (mut wg_config_file, wg_config_str) = -// os::load_and_read_file_unhandled(wg_config_path).await; -// if !wg_config_str.is_empty() { -// wg_config = serde_json::from_str(wg_config_str.as_str()).unwrap(); -// } else { -// info!("Creating new WireGuard configuration..."); -// let private_key = os::exec_bash(format!("wg genkey")).await; -// let public_key = os::exec_bash(format!("echo {private_key} | wg pubkey")).await; -// let address = quick.wg_default_address.repeat(1); -// wg_config = WireguardMemento::new(config::Server { -// private_key, -// public_key, -// address, -// }); -// info!("WireGuard configuration generated..."); -// save_config_directly(&wg_config, &mut wg_config_file).await; -// } -// (wg_config, quick) -// } - -// async fn save_config_directly(config: &WireguardMemento, file: &mut tokio::fs::File) { -// let wg_config_str = -// serde_json::to_string_pretty(config).expect("Could not serialize wg_config"); -// os::write_to_file_unhandled(file, wg_config_str).await; -// } - -// async fn load_wireguard_settings(config_path: &str) -> WireguardSettings { -// let (_, config_str) = os::load_and_read_file_unhandled(config_path).await; - -// match serde_json::from_str(&config_str) { -// Ok(config) => config, -// Err(error) => { -// log::error!( -// "Could not parse settings {}: {}", -// config_path, -// error.to_string() -// ); -// panic!() -// } -// } -// } - -// async fn sync_config() { -// info!("Config syncing..."); -// os::exec_bash(format!("wg syncconf wg0 <(wg-quick strip wg0)")).await; -// info!("Config synced..."); -// os::exec_bash(format!("wg syncconf wg0 <(wg-quick strip wg0)")).await; -// } - -// // async fn format_config(config: WireguardMemento, quick: WireguardSettings) -> String { -// // let result_vec = vec![ -// // format!("# Note: Do not edit this file directly.\n"), -// // format!("# Your changes will be overwritten!\n"), -// // format!("\n"), -// // format!("# Server\n"), -// // format!("[Interface]\n"), -// // format!("PrivateKey = {}\n", config.server.private_key), -// // format!("Address = {}/24\n", config.server.address), -// // format!("ListenPort = 51820\n"), -// // format!("PreUp = {}\n", multi_line(quick.wg_pre_up)), -// // format!("PostUp = {}\n", multi_line(quick.wg_post_up)), -// // format!("PreDown = {}\n", multi_line(quick.wg_pre_down)), -// // format!("PostDown = {}\n", multi_line(quick.wg_post_down)), -// // ]; -// // let mut result = misc::multi_line(&result_vec); - -// // for (uid, client) in config.clients { -// // if !client.enabled { -// // continue; -// // } -// // let result_inner = vec![ -// // format!("\n"), -// // format!("# Client: {} ({})\n", client.name, uid), -// // format!("[Peer]\n"), -// // format!("PublicKey = {}\n", client.public_key), -// // format!("PresharedKey = {}\n", client.pre_shared_key), -// // format!("AllowedIPs = {}/32\n", client.address), -// // ]; -// // result.push_str(multi_line(result_inner).as_str()); -// // } -// // result -// // } diff --git a/src/wireguard/config.rs b/src/wireguard/config.rs deleted file mode 100644 index 01652c49..00000000 --- a/src/wireguard/config.rs +++ /dev/null @@ -1,430 +0,0 @@ -use std::collections::HashMap; - -use chrono::{SecondsFormat, SubsecRound, Utc}; - - -use serde::{Deserialize, Serialize}; -use tokio::join; -use uuid::Uuid; - -use crate::utils::{misc, os}; - -#[derive(Serialize, Deserialize, Clone)] -pub struct WireguardSettings { - pub interface_name: String, - pub release: String, - pub api_port: i32, - pub password: String, - pub host: String, - pub wg_port: i32, - pub mtu: i32, - pub persistent_keepalive: i32, - pub default_address: String, - pub default_address_base: String, - pub default_dns: String, - pub allowed_ips: String, - pub pre_up: String, - pub post_up: String, - pub pre_down: String, - pub post_down: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all(serialize = "camelCase"))] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct WireguardMemento { - pub server: Server, - pub clients: HashMap, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all(serialize = "camelCase"))] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct Server { - pub private_key: String, - pub public_key: String, - pub address: String, -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all(serialize = "camelCase"))] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct Client { - pub uuid: Uuid, - pub name: String, - pub address: String, - pub private_key: String, - pub public_key: String, - pub pre_shared_key: String, - pub created_at: String, - pub updated_at: String, - pub enabled: bool, -} - -impl Client { - pub fn enable(&mut self) { - self.enabled = true; - self.updated_at = Utc::now() - .trunc_subsecs(3) - .to_rfc3339_opts(SecondsFormat::Millis, true) - } - pub fn disable(&mut self) { - self.enabled = false; - self.updated_at = Utc::now() - .trunc_subsecs(3) - .to_rfc3339_opts(SecondsFormat::Millis, true) - } -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all(serialize = "camelCase"))] -#[serde(rename_all(deserialize = "snake_case"))] -pub struct WebClient { - pub id: Uuid, - pub name: String, - pub enabled: bool, - pub address: String, - pub public_key: String, - pub created_at: String, - pub updated_at: String, - pub allowed_ips: String, - pub persistent_keepalive: String, - pub latest_handshake_at: String, - pub transfer_rx: i64, - pub transfer_tx: i64, -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all(serialize = "camelCase"))] -#[serde(rename_all(deserialize = "snake_case"))] -pub struct DumpClient { - pub public_key: String, - pub pre_shared_key: String, - pub endpoint: String, - pub allowed_ips: String, - pub latest_handshake_at: String, - pub transfer_rx: i64, - pub transfer_tx: i64, - pub persistent_keepalive: String, -} -#[derive(Clone)] -pub struct WireGuard { - pub name: String, - pub config_path: String, - pub memento_path: String, - pub settings: WireguardSettings, -} - -impl WireguardMemento { - pub fn new(server: Server) -> Self { - Self { - server, - clients: HashMap::new(), - } - } -} - -impl WireGuard { - pub fn new(name: &str, wg_path: String, settings: WireguardSettings) -> Self { - let path = format!("{}{}", wg_path, name); - let config_path = format!("{}.conf", path); - let memento_path = format!("{}.json", path); - let name = format!("{}", name); - Self { - name, - settings, - config_path, - memento_path, - } - } - - pub async fn quick_up(&self) { - let result = os::exec_sh(format!("wg-quick up {}", &self.name)) - .await - .expect("Error"); - result.log(); - } - - pub async fn quick_down(&self) { - let result = os::exec_sh(format!("wg-quick down {}", &self.name)) - .await - .expect("Error"); - result.log(); - } - - pub async fn sync_config(&self) { - let result = os::exec_sh(format!( - "wg syncconf {} <(wg-quick strip {})", - self.name, self.name - )) - .await - .expect("Error syncing"); - result.log(); - } - - pub async fn get_memento(&self) -> WireguardMemento { - let (_, memento_str) = os::load_and_read_file_unhandled(self.memento_path.as_str()).await; - if !memento_str.is_empty() { - serde_json::from_str::(memento_str.as_str()).unwrap() - } else { - log::info!("Creating new WireGuard configuration..."); - let private_key = os::exec_sh("wg genkey").await.expect("error genkey").stdout; - let public_key = os::exec_sh(format!("echo {} | wg pubkey", private_key)) - .await - .expect("error pubkey") - .stdout; - - let address = self.settings.default_address.repeat(1); - let memento = WireguardMemento::new(Server { - private_key, - public_key, - address, - }); - log::info!("WireGuard configuration generated..."); - memento - } - } - - pub async fn set_memento(&self, memento: &WireguardMemento) { - let data_memento = - serde_json::to_string_pretty(memento).expect("Could not serialize memento"); - let data_config = self.format_config(memento); - - join!( - os::write_to_path_unhandled(self.config_path.as_str(), data_config), - os::write_to_path_unhandled(self.memento_path.as_str(), data_memento.to_string()) - ); - } - - pub async fn start(&self) { - let memento = self.get_memento().await; - self.set_memento(&memento).await; - self.quick_down().await; - self.quick_up().await; - self.sync_config().await; - } - - pub async fn create_client<'a>( - &'a self, - memento: &'a mut WireguardMemento, - name: String, - ) -> &Client { - let private_key = os::exec_sh("wg genkey").await.expect("error genkey").stdout; - - let pub_cmd = format!("echo {} | wg pubkey", private_key); - let public_key = os::exec_sh(pub_cmd).await.expect("error pubkey").stdout; - - let pre_shared_key = os::exec_sh("wg genpsk").await.expect("error genpsk").stdout; - - let mut i: u8 = 2; - let address = loop { - match memento.clients.iter().find(|(_, client)| { - client.address == format!("{}.{}", self.settings.default_address_base, i) - }) { - Some(_) => { - if i == 255 { - break None; - } - i += 1; - } - None => break Some(format!("{}.{}", self.settings.default_address_base, i)), - } - } - .expect("Maximum number of clients reached"); - - let uuid = Uuid::new_v4(); - let now = Utc::now().trunc_subsecs(3); - - let client = Client { - uuid, - name, - address, - created_at: now.to_rfc3339_opts(SecondsFormat::Secs, true), - updated_at: now.to_rfc3339_opts(SecondsFormat::Secs, true), - enabled: true, - pre_shared_key, - private_key, - public_key, - }; - - memento.clients.insert(uuid, client.clone()); - - self.set_memento(&memento).await; - self.sync_config().await; - - memento.clients.get(&uuid).unwrap() - } - - pub fn get_client<'a>(&'a self, memento: &'a WireguardMemento, client_id: Uuid) -> &Client { - let client = memento - .clients - .get(&client_id) - .expect("Could not find user"); - client - } - - pub async fn get_clients(&self, memento: &WireguardMemento) -> Vec { - let res = os::exec_sh("wg show wg0 dump").await.unwrap(); - - let lines: Vec> = res - .stdout - .trim() - .split('\n') - .skip(1) - .map(|line| line.split('\t').map(|x| x.to_string()).collect()) - .collect(); - - let lines: Vec = lines - .iter() - .map(|x| DumpClient { - public_key: x[0].to_string(), - pre_shared_key: x[1].to_string(), - endpoint: x[2].to_string(), - allowed_ips: x[3].to_string(), - latest_handshake_at: x[4].to_string(), - transfer_rx: x[5].parse::().unwrap(), - transfer_tx: x[6].parse::().unwrap(), - persistent_keepalive: x[7].to_string(), - }) - .collect(); - - let clients: Vec = memento - .clients - .iter() - .map(|(uuid, value)| { - let uuid = uuid.to_owned(); - let value = value.to_owned(); - let client_conf; - - match lines.iter().find(|x| x.public_key.eq(&value.public_key)) { - Some(conf) => client_conf = conf, - None => return None, - }; - Some(WebClient { - id: uuid, - name: value.name, - enabled: value.enabled, - address: value.address, - public_key: value.public_key, - created_at: value.created_at, - updated_at: value.updated_at, - allowed_ips: client_conf.allowed_ips.to_string(), - persistent_keepalive: client_conf.persistent_keepalive.to_string(), - latest_handshake_at: client_conf.latest_handshake_at.to_string(), - transfer_rx: client_conf.transfer_rx, - transfer_tx: client_conf.transfer_tx, - }) - }) - .filter(|x| x.is_some()) - .map(|x| x.unwrap()) - .collect(); - - clients - } - - pub fn get_client_configuration( - &self, - memento: &WireguardMemento, - client_id: Uuid, - ) -> (String, String) { - let client = self.get_client(memento, client_id); - - let result_vec = vec![ - format!("[Interface]\n"), - format!("PrivateKey = {}\n", client.private_key), - format!("Address = {}/24\n", client.address), - format!("DNS = {}\n", self.settings.default_dns), - format!("MTU = {}\n", self.settings.mtu), - format!("\n"), - format!("[Peer]\n"), - format!("PublicKey = {}\n", memento.server.public_key), - format!("PresharedKey = {}\n", client.pre_shared_key), - format!("AllowedIPs = {}\n", self.settings.allowed_ips), - format!( - "PersistentKeepalive = {}\n", - self.settings.persistent_keepalive - ), - format!( - "Endpoint = {}:{}\n", - self.settings.host, self.settings.wg_port - ), - ]; - - (client.name.to_string(), misc::multi_line(&result_vec)) - } - - pub async fn delete_client(&self, memento: &mut WireguardMemento, client_id: Uuid) { - if memento.clients.remove(&client_id).is_some() { - self.set_memento(&memento).await; - } - } - - pub async fn enable_client(&self, memento: &mut WireguardMemento, client_id: Uuid) { - match memento.clients.get_mut(&client_id) { - Some(client) => { - client.enable(); - self.set_memento(&memento).await; - } - None => return, - } - } - - pub async fn disable_client(&self, memento: &mut WireguardMemento, client_id: Uuid) { - match memento.clients.get_mut(&client_id) { - Some(client) => { - client.disable(); - self.set_memento(&memento).await; - } - None => return, - } - } - - pub async fn get_client_qrcode_svg( - &self, - memento: &WireguardMemento, - client_id: Uuid, - ) -> String { - let (_, config) = self.get_client_configuration(memento, client_id); - return qrcode_generator::to_svg_to_string_from_str( - config, - qrcode_generator::QrCodeEcc::Low, - 512, - None::<&str>, - ) - .unwrap(); - } - - fn format_config(&self, config: &WireguardMemento) -> String { - let result_vec = vec![ - format!("# Note: Do not edit this file directly.\n"), - format!("# Your changes will be overwritten!\n"), - format!("\n"), - format!("# Server\n"), - format!("[Interface]\n"), - format!("PrivateKey = {}\n", config.server.private_key), - format!("Address = {}/24\n", config.server.address), - format!("ListenPort = 51820\n"), - format!("PreUp = {}\n", self.settings.pre_up), - format!("PostUp = {}\n", self.settings.post_up), - format!("PreDown = {}\n", self.settings.pre_down), - format!("PostDown = {}\n", self.settings.post_down), - ]; - let mut result = misc::multi_line(&result_vec); - - for (uid, client) in &config.clients { - if !client.enabled { - continue; - } - let result_inner = vec![ - format!("\n"), - format!("# Client: {} ({})\n", client.name, uid), - format!("[Peer]\n"), - format!("PublicKey = {}\n", client.public_key), - format!("PresharedKey = {}\n", client.pre_shared_key), - format!("AllowedIPs = {}/32\n", client.address), - ]; - result.push_str(misc::multi_line(&result_inner).as_str()); - } - result - } -}