Browse Source

Add client state switching

pull/438/head
Yamzik 4 years ago
parent
commit
19b67c4b28
  1. 2
      Cargo.toml
  2. BIN
      assets/screenshot.png
  3. BIN
      assets/wg-easy.sketch
  4. 0
      build_install.sh
  5. 0
      build_release.sh
  6. 13
      data/wg0.conf
  7. 8
      data/wg0.json
  8. 4
      docker-compose.deploy.yml
  9. 78
      src/main.rs
  10. 39
      src/utils/os.rs
  11. 2
      src/wg_rs.rs
  12. 231
      src/wg_rs/config.rs
  13. 250
      src/wg_rs/wireguard.rs
  14. 2
      src/wireguard.rs
  15. 108
      src/wireguard/commands.rs
  16. 430
      src/wireguard/config.rs

2
Cargo.toml

@ -15,7 +15,7 @@ actix-web-lab = "0"
actix-session = { version = "0", features = ["cookie-session"] } actix-session = { version = "0", features = ["cookie-session"] }
futures-util = { version = "0", default-features = false, features = ["std"] } futures-util = { version = "0", default-features = false, features = ["std"] }
serde = { version = "1.0", features = ["derive"] } 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"] } uuid = {version = "1", features = ["serde", "v4", "fast-rng", "macro-diagnostics"] }
chrono = {version = "0", features = ["serde"] } chrono = {version = "0", features = ["serde"] }
qrcode-generator = "4" qrcode-generator = "4"

BIN
assets/screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

BIN
assets/wg-easy.sketch

Binary file not shown.

0
install.sh → build_install.sh

0
release.sh → build_release.sh

13
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 =

8
data/wg0.json

@ -0,0 +1,8 @@
{
"server": {
"privateKey": "aCjYU+CBAkIerzXXFj8hzc4ZmMDLNGrhsxqJjOwtPH0=",
"publicKey": "Gi9QI2VgCJK1ziPaE2gE/jXbiXj+Rx3mUbIq8RcHPEE=",
"address": "10.8.0.1"
},
"clients": {}
}

4
docker-compose.deploy.yml

@ -13,8 +13,8 @@ services:
# - INTERFACE_NAME=wg0 # - INTERFACE_NAME=wg0
# - RELEASE=rs0 # - RELEASE=rs0
# - API_PORT=8080 # - API_PORT=8080
- PASSWORD=123321 # - PASSWORD=123321
- HOST= - HOST=localhost
# - WG_PORT=51820 # - WG_PORT=51820
# - MTU=0 # - MTU=0
# - PERSISTENT_KEEPALIVE=25 # - PERSISTENT_KEEPALIVE=25

78
src/main.rs

@ -1,5 +1,5 @@
mod utils; mod utils;
mod wireguard; mod wg_rs;
use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware}; use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware};
use actix_web::{ use actix_web::{
@ -10,9 +10,12 @@ use actix_web::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use utils::{misc, os}; use utils::{misc, os};
use uuid::Uuid; use uuid::Uuid;
use wireguard::config::{WireGuard, WireguardSettings}; use wg_rs::config::Settings;
use wg_rs::wireguard::WireGuard;
#[derive(Deserialize)] #[derive(Deserialize)]
struct Name { struct Name {
@ -131,23 +134,26 @@ async fn session_logout(session: Session) -> impl Responder {
} }
#[get("/api/wireguard/client")] #[get("/api/wireguard/client")]
async fn all_clients(wireguard: web::Data<WireGuard>) -> impl Responder { async fn all_clients(wireguard: web::Data<RwLock<WireGuard>>) -> impl Responder {
let memento = wireguard.get_memento().await; let wireguard = wireguard.read().await;
let clients = wireguard.get_clients(&memento).await; let clients = wireguard.get_clients().await;
HttpResponse::Ok().body(serde_json::to_string_pretty(&clients).unwrap()) HttpResponse::Ok().body(serde_json::to_string_pretty(&clients).unwrap())
} }
#[get("/api/wireguard/client/{id}/qrcode.svg")] #[get("/api/wireguard/client/{id}/qrcode.svg")]
async fn client_qr(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder { async fn client_qr(id: web::Path<Uuid>, wireguard: web::Data<RwLock<WireGuard>>) -> impl Responder {
let memento = wireguard.get_memento().await; let wireguard = wireguard.read().await;
let qr = wireguard.get_client_qrcode_svg(&memento, *id).await; let qr = wireguard.get_client_qrcode_svg(*id).await;
HttpResponse::Ok().body(qr) HttpResponse::Ok().body(qr)
} }
#[get("/api/wireguard/client/{id}/configuration")] #[get("/api/wireguard/client/{id}/configuration")]
async fn client_config(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder { async fn client_config(
let memento = wireguard.get_memento().await; id: web::Path<Uuid>,
let (name, config) = wireguard.get_client_configuration(&memento, *id); wireguard: web::Data<RwLock<WireGuard>>,
) -> impl Responder {
let wireguard = wireguard.read().await;
let (name, config) = wireguard.get_client_configuration(*id);
HttpResponse::Ok() HttpResponse::Ok()
.content_type("text/plain") .content_type("text/plain")
.append_header(ContentDisposition { .append_header(ContentDisposition {
@ -161,33 +167,42 @@ async fn client_config(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) ->
} }
#[post("/api/wireguard/client")] #[post("/api/wireguard/client")]
async fn create_client(data: web::Json<Name>, wireguard: web::Data<WireGuard>) -> impl Responder { async fn create_client(
let mut memento = wireguard.get_memento().await; data: web::Json<Name>,
let client = wireguard wireguard: web::Data<RwLock<WireGuard>>,
.create_client(&mut memento, data.name.to_string()) ) -> impl Responder {
.await; let mut wireguard = wireguard.write().await;
HttpResponse::Ok().body(serde_json::to_string_pretty(client).unwrap()) 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}")] #[delete("/api/wireguard/client/{id}")]
async fn delete_client(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder { async fn delete_client(
let mut memento = wireguard.get_memento().await; id: web::Path<Uuid>,
wireguard.delete_client(&mut memento, *id).await; wireguard: web::Data<RwLock<WireGuard>>,
) -> impl Responder {
let mut wireguard = wireguard.write().await;
wireguard.delete_client(*id).await;
HttpResponse::NoContent() HttpResponse::NoContent()
} }
#[post("/api/wireguard/client/{id}/enable")] #[post("/api/wireguard/client/{id}/enable")]
async fn enable_client(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder { async fn enable_client(
// todo here is a bug that kills whole app id: web::Path<Uuid>,
// let mut memento = wireguard.get_memento().await; wireguard: web::Data<RwLock<WireGuard>>,
// wireguard.enable_client(&mut memento, *id).await; ) -> impl Responder {
let mut wireguard = wireguard.write().await;
wireguard.enable_client(*id).await;
HttpResponse::NoContent() HttpResponse::NoContent()
} }
#[post("/api/wireguard/client/{id}/disable")] #[post("/api/wireguard/client/{id}/disable")]
async fn disable_client(id: web::Path<Uuid>, wireguard: web::Data<WireGuard>) -> impl Responder { async fn disable_client(
// let mut memento = wireguard.get_memento().await; id: web::Path<Uuid>,
// wireguard.disable_client(&mut memento, *id).await; wireguard: web::Data<RwLock<WireGuard>>,
) -> impl Responder {
let mut wireguard = wireguard.write().await;
wireguard.disable_client(*id).await;
HttpResponse::NoContent() HttpResponse::NoContent()
} }
@ -206,8 +221,7 @@ async fn main() -> std::io::Result<()> {
let password = get_env("PASSWORD", "123321"); let password = get_env("PASSWORD", "123321");
let settings = get_wireguard_settings(); 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; wireguard.start().await;
let index_path = format!("{}/index.html", static_dir); 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 (_, manifest_file) = os::load_and_read_file_unhandled(mainfest_path.as_str()).await;
let security = web::Data::new(Security { password }); 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 { let static_files = web::Data::new(Static {
dir: static_dir, dir: static_dir,
index_page, index_page,
@ -261,7 +275,7 @@ async fn main() -> std::io::Result<()> {
.await .await
} }
fn get_wireguard_settings() -> WireguardSettings { fn get_wireguard_settings() -> Settings {
let interface_name = get_env("INTERFACE_NAME", "wg0"); let interface_name = get_env("INTERFACE_NAME", "wg0");
let release_ = get_env("RELEASE", "rs0"); let release_ = get_env("RELEASE", "rs0");
let api_port = get_env("API_PORT", "8080"); 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 pre_down = get_env("PRE_DOWN", "");
let post_down = get_env("POST_DOWN", ""); let post_down = get_env("POST_DOWN", "");
WireguardSettings { Settings {
interface_name, interface_name,
release: release_, release: release_,
api_port: api_port.parse::<i32>().expect("Invalid api port was set"), api_port: api_port.parse::<i32>().expect("Invalid api port was set"),

39
src/utils/os.rs

@ -2,10 +2,7 @@ use std::{ffi::OsStr, string::FromUtf8Error};
use log::{info, warn}; use log::{info, warn};
use serde::Serialize; use serde::Serialize;
use tokio::{ use tokio::{fs::File, io::AsyncReadExt, process::Command};
io::{AsyncReadExt, AsyncWriteExt},
process::Command,
};
#[derive(Serialize)] #[derive(Serialize)]
pub struct StdResult { 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<S: Into<String>>(path: S) -> Result<File, std::io::Error> {
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<String, std::io::Error> {
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() match tokio::fs::OpenOptions::new()
.append(false) .append(false)
.read(true) .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 buffer = String::new();
let mut file = load_file_unhandled(path).await; let mut file = load_file_unhandled(path).await;
match file.read_to_string(&mut buffer).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 { pub async fn exec_sh<S: Into<String>>(command: S) -> Result<StdResult, String> {
let mut file = load_file_unhandled(path).await; let command = command.into();
match file.write(data.as_bytes()).await { info!("$ {}", command.clone());
Ok(_) => file,
Err(error) => {
log::error!("Could not write to file: {}", error);
panic!()
}
}
}
pub async fn exec_sh<S: AsRef<OsStr>>(command: S) -> Result<StdResult, String> {
let res = match Command::new("sh").arg("-c").arg(command).output().await { let res = match Command::new("sh").arg("-c").arg(command).output().await {
Ok(val) => val, Ok(val) => val,
Err(err) => return Err(err.to_string()), Err(err) => return Err(err.to_string()),

2
src/wg_rs.rs

@ -0,0 +1,2 @@
pub mod wireguard;
pub mod config;

231
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<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
}
}

250
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<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();
}
}

2
src/wireguard.rs

@ -1,2 +0,0 @@
pub mod commands;
pub mod config;

108
src/wireguard/commands.rs

@ -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
// // }

430
src/wireguard/config.rs

@ -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…
Cancel
Save