diff --git a/Cargo.toml b/Cargo.toml index a235693d93..ed88c61fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ log = "0.4" maildir = { version = "0.6", optional = true } neli = { version = "0.6", features = ["async"] } neli-wifi = { version = "0.6", features = ["async"] } -nix = { version = "0.27", features = ["fs", "process"] } +nix = { version = "0.27", features = ["fs", "process", "user"] } nom = "7.1.2" notmuch = { version = "0.8", optional = true } once_cell = "1" diff --git a/src/blocks/vpn.rs b/src/blocks/vpn.rs index f7f9bd1f26..11aec238a4 100644 --- a/src/blocks/vpn.rs +++ b/src/blocks/vpn.rs @@ -12,6 +12,7 @@ //! `format_disconnected` | A string to customise the output in case the network is disconnected. See below for available placeholders. | `" VPN: $icon "` //! `state_connected` | The widgets state if the vpn network is connected. | `info` //! `state_disconnected` | The widgets state if the vpn network is disconnected | `idle` +//! `wireguard_interface` | The wireguard interface name | `wg0` //! //! Placeholder | Value | Type | Unit //! ------------|-----------------------------------------------------------|--------|------ @@ -32,6 +33,21 @@ //! ## Mullvad //! Behind the scenes the mullvad driver uses the `mullvad` command line binary. In order for this to work properly the binary should be executable and mullvad daemon should be running. //! +//! ## Wireguard +//! Behind the scenes the wireguard driver uses the `wg` and `wg-quick` command line binaries. +//! The binaries are executed through sudo, so you need to configure your sudoers file to allow password-less execution of these binaries. +//! +//! Sample sudoers file (`/etc/sudoers.d/wireguard`): +//! ```text +//! your_user ALL=(ALL:ALL) NOPASSWD: /usr/bin/wg-quick up wg0, \ +//! /usr/bin/wg-quick down wg0, \ +//! /usr/bin/wg show wg0 +//! ``` +//! Be careful to include the interface name, and make sure that the config file is owned by root and not writable by others. +//! Otherwise the PreUp and PostDown scripts can be used to run arbitrary commands as root. +//! +//! The country and flag placeholders are not available when connected to Wireguard. +//! //! # Example //! //! Shows the current vpn network state: @@ -70,6 +86,9 @@ mod nordvpn; use nordvpn::NordVpnDriver; mod mullvad; +mod wireguard; + +use crate::blocks::vpn::wireguard::WireguardDriver; use mullvad::MullvadDriver; use super::prelude::*; @@ -80,6 +99,7 @@ pub enum DriverType { #[default] Nordvpn, Mullvad, + Wireguard, } #[derive(Deserialize, Debug, SmartDefault)] @@ -92,12 +112,14 @@ pub struct Config { pub format_disconnected: FormatConfig, pub state_connected: State, pub state_disconnected: State, + #[default("wg0".into())] + pub wireguard_interface: String, } enum Status { Connected { - country: String, - country_flag: String, + country: Option, + country_flag: Option, }, Disconnected, Error, @@ -123,6 +145,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let driver: Box = match config.driver { DriverType::Nordvpn => Box::new(NordVpnDriver::new().await), DriverType::Mullvad => Box::new(MullvadDriver::new().await), + DriverType::Wireguard => { + Box::new(WireguardDriver::new(config.wireguard_interface.to_owned()).await) + } }; loop { @@ -136,10 +161,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { country_flag, } => { widget.set_values(map!( - "icon" => Value::icon(status.icon()), - "country" => Value::text(country.to_string()), - "flag" => Value::text(country_flag.to_string()), - + "icon" => Value::icon(status.icon()), + [if let Some(c) = country] "country" => Value::text(c.into()), + [if let Some(f) = country_flag] "flag" => Value::text(f.into()), )); widget.set_format(format_connected.clone()); config.state_connected diff --git a/src/blocks/vpn/mullvad.rs b/src/blocks/vpn/mullvad.rs index 7cf2685e57..2cc56ec6e0 100644 --- a/src/blocks/vpn/mullvad.rs +++ b/src/blocks/vpn/mullvad.rs @@ -67,8 +67,8 @@ impl Driver for MullvadDriver { .unwrap_or_default(); return Ok(Status::Connected { - country, - country_flag, + country: Some(country), + country_flag: Some(country_flag), }); } Ok(Status::Error) diff --git a/src/blocks/vpn/nordvpn.rs b/src/blocks/vpn/nordvpn.rs index 5856b8c9b1..79228686ec 100644 --- a/src/blocks/vpn/nordvpn.rs +++ b/src/blocks/vpn/nordvpn.rs @@ -77,8 +77,8 @@ impl Driver for NordVpnDriver { None => String::default(), }; return Ok(Status::Connected { - country, - country_flag, + country: Some(country), + country_flag: Some(country_flag), }); } Ok(Status::Error) diff --git a/src/blocks/vpn/wireguard.rs b/src/blocks/vpn/wireguard.rs new file mode 100644 index 0000000000..1b5305e1e5 --- /dev/null +++ b/src/blocks/vpn/wireguard.rs @@ -0,0 +1,95 @@ +use std::process::Stdio; + +use async_trait::async_trait; +use nix::unistd::getuid; +use tokio::process::Command; + +use crate::blocks::prelude::*; + +use super::{Driver, Status}; + +pub struct WireguardDriver { + interface: String, +} + +impl WireguardDriver { + pub async fn new(interface: String) -> WireguardDriver { + WireguardDriver { interface } + } +} + +const SUDO_CMD: &str = "/usr/bin/sudo"; +const WG_QUICK_CMD: &str = "/usr/bin/wg-quick"; +const WG_CMD: &str = "/usr/bin/wg"; + +#[async_trait] +impl Driver for WireguardDriver { + async fn get_status(&self) -> Result { + let status = run_wg(&["show", self.interface.as_str()]).await; + + match status { + Ok(status) => { + if status.contains(format!("interface: {}", self.interface).as_str()) { + Ok(Status::Connected { + country: None, + country_flag: None, + }) + } else { + Ok(Status::Disconnected) + } + } + Err(_) => Ok(Status::Error), + } + } + + async fn toggle_connection(&self, status: &Status) -> Result<()> { + match status { + Status::Connected { .. } => { + run_wg_quick(&["down", self.interface.as_str()]).await?; + } + Status::Disconnected => { + run_wg_quick(&["up", self.interface.as_str()]).await?; + } + Status::Error => (), + } + Ok(()) + } +} + +async fn run_wg(args: &[&str]) -> Result { + let stdout = make_command(should_use_sudo(), WG_CMD) + .args(args) + .output() + .await + .error(format!("Problem running wg command: {args:?}"))? + .stdout; + let stdout = + String::from_utf8(stdout).error(format!("wg produced non-UTF8 output: {args:?}"))?; + Ok(stdout) +} + +async fn run_wg_quick(args: &[&str]) -> Result<()> { + make_command(should_use_sudo(), WG_QUICK_CMD) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .spawn() + .error(format!("Problem running wg-quick command: {args:?}"))? + .wait() + .await + .error(format!("Problem running wg-quick command: {args:?}"))?; + Ok(()) +} + +fn make_command(use_sudo: bool, cmd: &str) -> Command { + let mut command = Command::new(if use_sudo { SUDO_CMD } else { cmd }); + + if use_sudo { + command.arg("-n").arg(cmd); + } + command +} + +fn should_use_sudo() -> bool { + !(getuid().is_root()) +}