diff --git a/Cargo.lock b/Cargo.lock index f0e1f19..6f68c94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,16 +115,39 @@ dependencies = [ [[package]] name = "async-executor" -version = "0.1.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f47c78ea98277cb1f5e6f60ba4fc762f5eafe9f6511bc2f7dfd8b75c225650" +checksum = "d373d78ded7d0b3fa8039375718cde0aace493f2e34fb60f51cbf567562ca801" dependencies = [ - "async-io 0.1.11", - "futures-lite 0.1.11", - "multitask", - "parking 1.0.6", - "scoped-tls", - "waker-fn", + "async-task 4.0.0", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "vec-arena", +] + +[[package]] +name = "async-fs" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3572236ba37147ca2b674a0bd5afd20aec0cd925ab125ab6fad6543960f9002" +dependencies = [ + "blocking", + "futures-lite", +] + +[[package]] +name = "async-global-executor" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd4f132a18f3fe7329c7b907047684f1b06174a900c559b661b2da8bb9cad5f" +dependencies = [ + "async-executor", + "async-io", + "futures-lite", + "num_cpus", + "once_cell", ] [[package]] @@ -145,43 +168,20 @@ dependencies = [ [[package]] name = "async-io" -version = "0.1.11" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae22a338d28c75b53702b66f77979062cb29675db376d99e451af4fa79dedb3" +checksum = "64c629684e697f58c0e99e5e2d84a840e3b336afbcfdbac7b44c3b1e222c2fd8" dependencies = [ - "cfg-if", - "concurrent-queue", - "futures-lite 0.1.11", - "libc", - "once_cell", - "parking 2.0.0", - "polling 0.1.9", - "socket2", - "vec-arena 0.5.2", - "wepoll-sys-stjepang", - "winapi", -] - -[[package]] -name = "async-io" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38628c78a34f111c5a6b98fc87dfc056cd1590b61afe748b145be4623c56d194" -dependencies = [ - "cfg-if", "concurrent-queue", "fastrand", - "futures-lite 1.4.0", - "libc", + "futures-lite", "log", + "nb-connect", "once_cell", - "parking 2.0.0", - "polling 1.0.1", - "socket2", - "vec-arena 1.0.0", + "parking", + "polling", + "vec-arena", "waker-fn", - "wepoll-sys-stjepang", - "winapi", ] [[package]] @@ -199,11 +199,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb915df28b8309139bd9c9c700d84c20e5c21385d05378caa84912332d0f6a1" dependencies = [ - "async-io 1.1.0", - "blocking 1.0.0", + "async-io", + "blocking", "cfg-if", "event-listener", - "futures-lite 1.4.0", + "futures-lite", "once_cell", "signal-hook", "winapi", @@ -246,21 +246,21 @@ dependencies = [ [[package]] name = "async-std" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c8da367da62b8ff2313c406c9ac091c1b31d67a165becdd2de380d846260f7" +checksum = "3c92085acfce8b32e5b261d0b59b8f3309aee69fea421ea3f271f8b93225754f" dependencies = [ - "async-executor", - "async-io 0.1.11", + "async-global-executor", + "async-io", "async-mutex", - "async-task", - "blocking 0.5.2", + "async-task 3.0.0", + "blocking", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 0.1.11", - "futures-timer", + "futures-lite", + "gloo-timers", "kv-log-macro", "log", "memchr", @@ -278,6 +278,12 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" +[[package]] +name = "async-task" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c37ba09c1b5185eb9897a5cef32770031f58fa92d9a5f79eb50cae5030b39c1" + [[package]] name = "async-trait" version = "0.1.40" @@ -397,19 +403,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5800d29218fea137b0880387e5948694a23c93fcdde157006966693a865c7c" -dependencies = [ - "async-channel", - "atomic-waker", - "futures-lite 0.1.11", - "once_cell", - "waker-fn", -] - [[package]] name = "blocking" version = "1.0.0" @@ -419,7 +412,7 @@ dependencies = [ "async-channel", "atomic-waker", "fastrand", - "futures-lite 1.4.0", + "futures-lite", "once_cell", "waker-fn", ] @@ -471,9 +464,9 @@ checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" [[package]] name = "cc" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" +checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" [[package]] name = "cfg-if" @@ -552,7 +545,7 @@ dependencies = [ "percent-encoding", "rand", "sha2", - "time 0.2.19", + "time 0.2.20", "version_check", ] @@ -647,9 +640,9 @@ checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" [[package]] name = "either" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "error-chain" @@ -722,30 +715,15 @@ checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" [[package]] name = "futures-lite" -version = "0.1.11" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97999970129b808f0ccba93211201d431fcc12d7e1ffae03a61b5cedd1a7ced2" +checksum = "5b77e08e656f472d8ea84c472fa8b0a7a917883048e1cf2d4e34a323cd0aaf63" dependencies = [ "fastrand", "futures-core", "futures-io", "memchr", - "parking 2.0.0", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba659a67bd5ac00896b31e1f4095174134a31e448893d73256f1d51b81abbd62" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking 2.0.0", + "parking", "pin-project-lite", "waker-fn", ] @@ -771,16 +749,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" -dependencies = [ - "gloo-timers", - "send_wrapper", -] - [[package]] name = "futures-util" version = "0.3.5" @@ -928,9 +896,12 @@ checksum = "6854dd77ddc4f9ba1a448f487e27843583d407648150426a30c2ea3a2c39490a" [[package]] name = "instant" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485" +checksum = "63312a18f7ea8760cdd0a7c5aac1a619752a246b833545e3e36d1f81f7cd9e66" +dependencies = [ + "cfg-if", +] [[package]] name = "itertools" @@ -1050,9 +1021,9 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memoffset" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" dependencies = [ "autocfg", ] @@ -1074,14 +1045,13 @@ dependencies = [ ] [[package]] -name = "multitask" -version = "0.2.0" +name = "nb-connect" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c09c35271e7dcdb5f709779111f2c8e8ab8e06c1b587c1c6a9e179d865aaa5b4" +checksum = "e847c76b390f44529c2071ef06d0b52fbb4bdb04cc8987a5cfa63954c000abca" dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", + "libc", + "winapi", ] [[package]] @@ -1168,12 +1138,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "parking" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb300f271742d4a2a66c01b6b2fa0c83dfebd2e0bf11addb879a3547b4ed87c" - [[package]] name = "parking" version = "2.0.0" @@ -1246,22 +1210,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "polling" -version = "0.1.9" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fffa183f6bd5f1a8a3e1f60ce2f8d5621e350eed84a62d6daaa5b9d1aaf6fbd" -dependencies = [ - "cfg-if", - "libc", - "log", - "wepoll-sys-stjepang", - "winapi", -] - -[[package]] -name = "polling" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0835fa5f9af34c170eb38638ae6bc88e1b11ecdd0b968c9d9de8e343450385eb" +checksum = "e0720e0b9ea9d52451cf29d3413ba8a9303f8815d9d9653ef70e03ff73e65566" dependencies = [ "cfg-if", "libc", @@ -1392,13 +1343,14 @@ name = "router" version = "0.1.0" dependencies = [ "anyhow", + "async-fs", "async-process", "async-trait", "base64", "bcrypt", - "blocking 1.0.0", + "blocking", "config", - "futures-lite 1.4.0", + "futures-lite", "mime", "once_cell", "rand", @@ -1468,12 +1420,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - [[package]] name = "scopeguard" version = "1.1.0" @@ -1495,12 +1441,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -[[package]] -name = "send_wrapper" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" - [[package]] name = "serde" version = "0.8.23" @@ -1684,18 +1624,6 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" -[[package]] -name = "socket2" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "winapi", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1846,9 +1774,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c1a1fd93112fc50b11c43a1def21f926be3c18884fad676ea879572da070a1" +checksum = "0d4953c513c9bf1b97e9cdd83f11d60c4b0a83462880a360d80d96953a953fee" dependencies = [ "const_fn", "libc", @@ -1949,12 +1877,6 @@ dependencies = [ "serde 1.0.116", ] -[[package]] -name = "vec-arena" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb18268690309760d59ee1a9b21132c126ba384f374c59a94db4bc03adeb561" - [[package]] name = "vec-arena" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index df51090..d3a4dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ build = "src/build.rs" [dependencies] anyhow = "1.0" +async-fs = "1.3.0" async-process = "1.0.0" async-trait = "0.1.40" base64 = "0.12.3" diff --git a/src/main.rs b/src/main.rs index fce0685..7ada06d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,12 +15,14 @@ mod middleware; mod rules; mod session; mod startup; +mod tunnels; use self::{ middleware::{AuthMiddleware, UserInfo}, rules::Rule, startup::{Config, Interfaces, ServerState}, templates::statics::StaticFile, + tunnels::{NewPeer, NewTunnel}, }; static CONFIG: Lazy = Lazy::new(|| Config::read().unwrap()); @@ -81,6 +83,41 @@ async fn delete_rule(req: tide::Request<()>) -> tide::Result { Ok(to_rules_page()) } +async fn tunnels_page(_: tide::Request<()>) -> tide::Result { + Ok(tide::Response::builder(200).build()) +} + +async fn save_tunnel(mut req: tide::Request<()>) -> tide::Result { + let body_string = req.body_string().await?; + + let new_tunnel: NewTunnel = QS.deserialize_str(&body_string)?; + + let tunnel = tunnels::create(new_tunnel).await?; + + // TODO: add firewall rule for tunnel + + tunnels::save(&DB, &tunnel).await?; + + Ok(to_tunnels_page()) +} + +async fn peers_page(_: tide::Request<()>) -> tide::Result { + Ok(tide::Response::builder(200).build()) +} + +async fn save_peer(mut req: tide::Request<()>) -> tide::Result { + let body_string = req.body_string().await?; + + let new_peer: NewPeer = QS.deserialize_str(&body_string)?; + + let peer = tunnels::add_peer(&DB, new_peer).await?; + let _ = peer; + + // TODO: add firewall rule for peer + + Ok(to_tunnels_page()) +} + #[derive(serde::Deserialize)] struct LoginForm { username: String, @@ -227,6 +264,10 @@ fn to_rules_page() -> tide::Response { to_custom("/rules") } +fn to_tunnels_page() -> tide::Response { + to_custom("/tunnnels") +} + async fn statics(req: tide::Request<()>) -> tide::Result { let file: String = req.param("file")?; @@ -261,6 +302,15 @@ fn main() -> Result<(), anyhow::Error> { app.at("/static/:file").get(statics); app.at("/login").get(login_page).post(login); app.at("/account").get(account_page).post(update_account); + app.at("/tunnels") + .with(AuthMiddleware) + .get(tunnels_page) + .post(save_tunnel) + .nest({ + let mut app = tide::new(); + app.at("/:id/peers").get(peers_page).post(save_peer); + app + }); app.at("/rules") .with(AuthMiddleware) .get(rules_page) diff --git a/src/tunnels/mod.rs b/src/tunnels/mod.rs new file mode 100644 index 0000000..2d197f7 --- /dev/null +++ b/src/tunnels/mod.rs @@ -0,0 +1,134 @@ +use once_cell::sync::OnceCell; +use sled::{Db, Tree}; + +static TUNNELS_TREE: OnceCell = OnceCell::new(); + +fn tunnels_tree(db: &Db) -> &'static Tree { + TUNNELS_TREE.get_or_init(|| db.open_tree("tunnels").unwrap()) +} + +mod wireguard; + +#[derive(serde::Deserialize)] +#[serde(tag = "type")] +pub(crate) enum NewTunnel { + Wireguard(wireguard::NewInterface), +} + +#[derive(serde::Deserialize)] +pub(crate) struct NewPeer { + tunnel_id: String, + peer: NewPeerKind, +} + +#[derive(serde::Deserialize)] +#[serde(tag = "type")] +pub(crate) enum NewPeerKind { + Wireguard(wireguard::Peer), +} + +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub(crate) enum Tunnel { + Wireguard(wireguard::Interface), +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub(crate) struct Peer { + tunnel_id: String, + peer: PeerKind, +} + +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub(crate) enum PeerKind { + Wireguard(wireguard::Peer), +} + +pub(crate) async fn add_peer(db: &Db, new_peer: NewPeer) -> Result { + let tree = tunnels_tree(db); + + let tunnel_bytes = tree + .get(new_peer.tunnel_id.as_bytes())? + .ok_or_else(|| anyhow::anyhow!("Missing tunnel"))?; + let tunnel: Tunnel = serde_json::from_slice(&tunnel_bytes)?; + + let peer = match (new_peer.peer, tunnel) { + (NewPeerKind::Wireguard(peer), Tunnel::Wireguard(interface)) => { + wireguard::add_peer(&interface, &peer).await?; + + Peer { + tunnel_id: new_peer.tunnel_id, + peer: PeerKind::Wireguard(peer), + } + } // _ => return Err(anyhow::anyhow!("Peer kind mismatch")), + }; + + let v = serde_json::to_vec(&peer)?; + + loop { + let id = db.generate_id()?; + let peer_id = peer_id(&peer.tunnel_id, id); + + if tree + .compare_and_swap( + peer_id.as_bytes(), + None as Option>, + Some(v.as_slice()), + )? + .is_ok() + { + break; + } + } + + tree.flush_async().await?; + + Ok(peer) +} + +pub(crate) async fn create(new_tunnel: NewTunnel) -> Result { + match new_tunnel { + NewTunnel::Wireguard(new_interface) => { + let interface = wireguard::generate_config(new_interface).await?; + + wireguard::apply(&interface).await?; + + Ok(Tunnel::Wireguard(interface)) + } + } +} + +pub(crate) async fn save(db: &Db, tunnel: &Tunnel) -> Result<(), anyhow::Error> { + let tree = tunnels_tree(db); + + let v = serde_json::to_vec(tunnel)?; + + loop { + let id = db.generate_id()?; + let tunnel_id = tunnel_id(id); + + if tree + .compare_and_swap( + tunnel_id.as_bytes(), + None as Option>, + Some(v.as_slice()), + )? + .is_ok() + { + break; + } + } + + tree.flush_async().await?; + + Ok(()) +} + +fn tunnel_id(id: u64) -> String { + format!("tunnel-{}", id) +} + +fn peer_id(tunnel_id: &str, id: u64) -> String { + format!("{}/peers/{}", tunnel_id, id) +} diff --git a/src/tunnels/wireguard.rs b/src/tunnels/wireguard.rs new file mode 100644 index 0000000..4659d35 --- /dev/null +++ b/src/tunnels/wireguard.rs @@ -0,0 +1,144 @@ +use async_fs::File; +use async_process::{Command, Stdio}; +use futures_lite::AsyncWriteExt; +use std::{net::Ipv4Addr, path::PathBuf}; + +#[derive(serde::Deserialize)] +pub(crate) struct NewInterface { + name: String, + address: Ipv4Addr, + port: u16, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub(crate) struct Interface { + name: String, + address: Ipv4Addr, + port: u16, + private_key: String, + public_key: String, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub(crate) struct Peer { + public_key: String, + address: Ipv4Addr, +} + +impl Interface { + fn config(&self) -> String { + format!( + r#"[Interface] +PrivateKey = {private_key} +Address = {address}/24 +ListenPort = {port} +SaveConfig = true +"#, + private_key = self.private_key, + address = self.address, + port = self.port, + ) + } +} + +impl Peer { + fn config(&self) -> String { + format!( + r#"[Peer] +PublicKey = {public_key} +AllowedIPs = {allowed_ip}/32 +"#, + public_key = self.public_key, + allowed_ip = self.address, + ) + } +} + +pub(crate) async fn add_peer(interface: &Interface, peer: &Peer) -> Result<(), anyhow::Error> { + use rand::Rng; + + let filename = rand::thread_rng() + .sample_iter(rand::distributions::Alphanumeric) + .take(8) + .collect::(); + + let filename = format!("{}.conf", filename); + + let mut tmp_file = PathBuf::new(); + tmp_file.push("/tmp"); + tmp_file.push(&filename); + + let mut file = File::create(&tmp_file).await?; + file.write_all(peer.config().as_bytes()).await?; + file.flush().await?; + drop(file); + + let status = Command::new("wg") + .args(&[ + &"addconf".as_ref(), + &interface.name.as_ref(), + &tmp_file.as_os_str(), + ]) + .status() + .await?; + + async_fs::remove_file(&tmp_file).await?; + + if !status.success() { + return Err(anyhow::anyhow!("Failed to add a peer")); + } + + Ok(()) +} + +pub(crate) async fn generate_config( + new_interface: NewInterface, +) -> Result { + let output = Command::new("wg").arg("genkey").output().await?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Error generating private key")); + } + + let private_key = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + let child = Command::new("wg") + .arg("pubkey") + .stdin(Stdio::piped()) + .spawn()?; + + let output = child.output().await?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Error generating public key")); + } + + let public_key = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + Ok(Interface { + name: new_interface.name, + address: new_interface.address, + port: new_interface.port, + private_key, + public_key, + }) +} + +pub(crate) async fn apply(interface: &Interface) -> Result<(), anyhow::Error> { + let config = interface.config(); + + let mut file = File::create(format!("{}.conf", interface.name)).await?; + file.write_all(config.as_bytes()).await?; + file.flush().await?; + + let status = Command::new("systemctl") + .args(&["enable", "--now", &interface.name]) + .status() + .await?; + + if !status.success() { + return Err(anyhow::anyhow!("Failed to enable wireguard service")); + } + + Ok(()) +}