mirror of https://github.com/wg-easy/wg-easy
committed by
GitHub
16 changed files with 574 additions and 593 deletions
|
Before Width: | Height: | Size: 105 KiB |
Binary file not shown.
@ -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 = |
|||
|
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"server": { |
|||
"privateKey": "aCjYU+CBAkIerzXXFj8hzc4ZmMDLNGrhsxqJjOwtPH0=", |
|||
"publicKey": "Gi9QI2VgCJK1ziPaE2gE/jXbiXj+Rx3mUbIq8RcHPEE=", |
|||
"address": "10.8.0.1" |
|||
}, |
|||
"clients": {} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
pub mod wireguard; |
|||
pub mod config; |
|||
@ -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<uuid::Uuid, Client>, |
|||
} |
|||
|
|||
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<Settings>, |
|||
memento: Memento, |
|||
} |
|||
|
|||
impl Accessor { |
|||
pub async fn new(memento_path: String, config_path: String, settings: Arc<Settings>) -> 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 |
|||
} |
|||
} |
|||
@ -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<Settings>, |
|||
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<WebClient> { |
|||
let res = os::exec_sh("wg show wg0 dump").await.unwrap(); |
|||
|
|||
let lines: Vec<Vec<String>> = res |
|||
.stdout |
|||
.trim() |
|||
.split('\n') |
|||
.skip(1) |
|||
.map(|line| line.split('\t').map(|x| x.to_string()).collect()) |
|||
.collect(); |
|||
|
|||
let lines: Vec<DumpClient> = 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::<i64>().unwrap(), |
|||
transfer_tx: x[6].parse::<i64>().unwrap(), |
|||
persistent_keepalive: x[7].to_string(), |
|||
}) |
|||
.collect(); |
|||
|
|||
let memento = self.memento_accessor.get(); |
|||
let clients: Vec<WebClient> = 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(); |
|||
} |
|||
} |
|||
@ -1,2 +0,0 @@ |
|||
pub mod commands; |
|||
pub mod config; |
|||
@ -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
|
|||
// // }
|
|||
@ -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<uuid::Uuid, Client>, |
|||
} |
|||
|
|||
#[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::<WireguardMemento>(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<WebClient> { |
|||
let res = os::exec_sh("wg show wg0 dump").await.unwrap(); |
|||
|
|||
let lines: Vec<Vec<String>> = res |
|||
.stdout |
|||
.trim() |
|||
.split('\n') |
|||
.skip(1) |
|||
.map(|line| line.split('\t').map(|x| x.to_string()).collect()) |
|||
.collect(); |
|||
|
|||
let lines: Vec<DumpClient> = 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::<i64>().unwrap(), |
|||
transfer_tx: x[6].parse::<i64>().unwrap(), |
|||
persistent_keepalive: x[7].to_string(), |
|||
}) |
|||
.collect(); |
|||
|
|||
let clients: Vec<WebClient> = 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 |
|||
} |
|||
} |
|||
Loading…
Reference in new issue