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) .map(char::from) .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(()) }