From 0a46b52531ccb8594cc91ef98e425c4ad7c1d5b8 Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:32:03 +0100 Subject: [PATCH 1/7] Add Wireguard support to vpn block --- Cargo.toml | 2 +- src/blocks/vpn.rs | 25 ++++++++++ src/blocks/vpn/wireguard.rs | 95 +++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/blocks/vpn/wireguard.rs 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..c1700a04ba 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 empty strings 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,6 +112,8 @@ pub struct Config { pub format_disconnected: FormatConfig, pub state_connected: State, pub state_disconnected: State, + #[default("wg0".into())] + pub wireguard_interface: String, } enum Status { @@ -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 { diff --git a/src/blocks/vpn/wireguard.rs b/src/blocks/vpn/wireguard.rs new file mode 100644 index 0000000000..ae55c00a6f --- /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: &'static str = "/usr/bin/sudo"; +const WG_QUICK_CMD: &'static str = "/usr/bin/wg-quick"; +const WG_CMD: &'static str = "/usr/bin/wg"; + +#[async_trait] +impl Driver for WireguardDriver { + async fn get_status(&self) -> Result { + let status = run_wg(vec!["show", self.interface.as_str()]).await; + + match status { + Ok(status) => { + if status.contains(format!("interface: {}", self.interface).as_str()) { + Ok(Status::Connected { + country: "".to_owned(), + country_flag: "".to_owned(), + }) + } else { + Ok(Status::Disconnected) + } + } + Err(_) => Ok(Status::Error), + } + } + + async fn toggle_connection(&self, status: &Status) -> Result<()> { + match status { + Status::Connected { .. } => { + run_wg_quick(vec!["down", self.interface.as_str()]).await?; + } + Status::Disconnected => { + run_wg_quick(vec!["up", self.interface.as_str()]).await?; + } + Status::Error => (), + } + Ok(()) + } +} + +async fn run_wg(args: Vec<&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: Vec<&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()) +} From 954f445db8f90107627f7aa31c42d2dc0e9c0d1f Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 7 Jan 2024 17:57:30 +0100 Subject: [PATCH 2/7] Remove redundant static lifetime. --- src/blocks/vpn/wireguard.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blocks/vpn/wireguard.rs b/src/blocks/vpn/wireguard.rs index ae55c00a6f..8d9fcd44ad 100644 --- a/src/blocks/vpn/wireguard.rs +++ b/src/blocks/vpn/wireguard.rs @@ -18,9 +18,9 @@ impl WireguardDriver { } } -const SUDO_CMD: &'static str = "/usr/bin/sudo"; -const WG_QUICK_CMD: &'static str = "/usr/bin/wg-quick"; -const WG_CMD: &'static str = "/usr/bin/wg"; +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 { From 037b773f9c090adbc603347a31d0fe2193b1c33a Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 21 Jan 2024 09:58:38 +0100 Subject: [PATCH 3/7] Use borrowed slice instead of owned Vector for command arguments. --- src/blocks/vpn/wireguard.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/blocks/vpn/wireguard.rs b/src/blocks/vpn/wireguard.rs index 8d9fcd44ad..fae211d1d9 100644 --- a/src/blocks/vpn/wireguard.rs +++ b/src/blocks/vpn/wireguard.rs @@ -25,7 +25,7 @@ const WG_CMD: &str = "/usr/bin/wg"; #[async_trait] impl Driver for WireguardDriver { async fn get_status(&self) -> Result { - let status = run_wg(vec!["show", self.interface.as_str()]).await; + let status = run_wg(&["show", self.interface.as_str()]).await; match status { Ok(status) => { @@ -45,10 +45,10 @@ impl Driver for WireguardDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { Status::Connected { .. } => { - run_wg_quick(vec!["down", self.interface.as_str()]).await?; + run_wg_quick(&["down", self.interface.as_str()]).await?; } Status::Disconnected => { - run_wg_quick(vec!["up", self.interface.as_str()]).await?; + run_wg_quick(&["up", self.interface.as_str()]).await?; } Status::Error => (), } @@ -56,9 +56,9 @@ impl Driver for WireguardDriver { } } -async fn run_wg(args: Vec<&str>) -> Result { +async fn run_wg(args: &[&str]) -> Result { let stdout = make_command(should_use_sudo(), WG_CMD) - .args(&args) + .args(args) .output() .await .error(format!("Problem running wg command: {args:?}"))? @@ -68,9 +68,9 @@ async fn run_wg(args: Vec<&str>) -> Result { Ok(stdout) } -async fn run_wg_quick(args: Vec<&str>) -> Result<()> { +async fn run_wg_quick(args: &[&str]) -> Result<()> { make_command(should_use_sudo(), WG_QUICK_CMD) - .args(&args) + .args(args) .stdin(Stdio::null()) .stdout(Stdio::null()) .spawn() From f66c1134b0d4553ae3165e15eca54a51056b6fd8 Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 21 Jan 2024 10:09:09 +0100 Subject: [PATCH 4/7] Separate state for connections to a specific country. --- src/blocks/vpn.rs | 17 +++++++++++++---- src/blocks/vpn/mullvad.rs | 6 ++++-- src/blocks/vpn/nordvpn.rs | 6 ++++-- src/blocks/vpn/wireguard.rs | 7 ++----- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/blocks/vpn.rs b/src/blocks/vpn.rs index c1700a04ba..a8607ce8db 100644 --- a/src/blocks/vpn.rs +++ b/src/blocks/vpn.rs @@ -46,7 +46,7 @@ //! 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 empty strings when connected to Wireguard. +//! The country and flag placeholders are not available when connected to Wireguard. //! //! # Example //! @@ -117,7 +117,8 @@ pub struct Config { } enum Status { - Connected { + Connected, + ConnectedToCountry { country: String, country_flag: String, }, @@ -128,7 +129,8 @@ enum Status { impl Status { fn icon(&self) -> Cow<'static, str> { match self { - Status::Connected { .. } => "net_vpn".into(), + Status::Connected => "net_vpn".into(), + Status::ConnectedToCountry { .. } => "net_vpn".into(), Status::Disconnected => "net_wired".into(), Status::Error => "net_down".into(), } @@ -156,7 +158,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut widget = Widget::new(); widget.state = match &status { - Status::Connected { + Status::ConnectedToCountry { country, country_flag, } => { @@ -169,6 +171,13 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { widget.set_format(format_connected.clone()); config.state_connected } + Status::Connected => { + widget.set_values(map!( + "icon" => Value::icon(status.icon()), + )); + widget.set_format(format_connected.clone()); + config.state_connected + } Status::Disconnected => { widget.set_values(map!( "icon" => Value::icon(status.icon()), diff --git a/src/blocks/vpn/mullvad.rs b/src/blocks/vpn/mullvad.rs index 7cf2685e57..a955f2e3b9 100644 --- a/src/blocks/vpn/mullvad.rs +++ b/src/blocks/vpn/mullvad.rs @@ -66,7 +66,7 @@ impl Driver for MullvadDriver { }) .unwrap_or_default(); - return Ok(Status::Connected { + return Ok(Status::ConnectedToCountry { country, country_flag, }); @@ -76,7 +76,9 @@ impl Driver for MullvadDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected { .. } => Self::run_network_command("disconnect").await?, + Status::Connected | Status::ConnectedToCountry { .. } => { + Self::run_network_command("disconnect").await? + } Status::Disconnected => Self::run_network_command("connect").await?, Status::Error => (), } diff --git a/src/blocks/vpn/nordvpn.rs b/src/blocks/vpn/nordvpn.rs index 5856b8c9b1..c7ecc56d5c 100644 --- a/src/blocks/vpn/nordvpn.rs +++ b/src/blocks/vpn/nordvpn.rs @@ -76,7 +76,7 @@ impl Driver for NordVpnDriver { .unwrap_or_default(), None => String::default(), }; - return Ok(Status::Connected { + return Ok(Status::ConnectedToCountry { country, country_flag, }); @@ -86,7 +86,9 @@ impl Driver for NordVpnDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected { .. } => Self::run_network_command("disconnect").await?, + Status::Connected | Status::ConnectedToCountry { .. } => { + Self::run_network_command("disconnect").await? + } Status::Disconnected => Self::run_network_command("connect").await?, Status::Error => (), } diff --git a/src/blocks/vpn/wireguard.rs b/src/blocks/vpn/wireguard.rs index fae211d1d9..1b085eb4a5 100644 --- a/src/blocks/vpn/wireguard.rs +++ b/src/blocks/vpn/wireguard.rs @@ -30,10 +30,7 @@ impl Driver for WireguardDriver { match status { Ok(status) => { if status.contains(format!("interface: {}", self.interface).as_str()) { - Ok(Status::Connected { - country: "".to_owned(), - country_flag: "".to_owned(), - }) + Ok(Status::Connected) } else { Ok(Status::Disconnected) } @@ -44,7 +41,7 @@ impl Driver for WireguardDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected { .. } => { + Status::Connected | Status::ConnectedToCountry { .. } => { run_wg_quick(&["down", self.interface.as_str()]).await?; } Status::Disconnected => { From 6dc6286786e5fbef3a96fe6ce916d00d32d5df4a Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 21 Jan 2024 10:46:37 +0100 Subject: [PATCH 5/7] Fix clippy issues --- src/blocks/vpn.rs | 3 +-- src/blocks/vpn/mullvad.rs | 2 +- src/blocks/vpn/nordvpn.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/blocks/vpn.rs b/src/blocks/vpn.rs index a8607ce8db..f2bcb0ed7f 100644 --- a/src/blocks/vpn.rs +++ b/src/blocks/vpn.rs @@ -129,8 +129,7 @@ enum Status { impl Status { fn icon(&self) -> Cow<'static, str> { match self { - Status::Connected => "net_vpn".into(), - Status::ConnectedToCountry { .. } => "net_vpn".into(), + Status::Connected | Status::ConnectedToCountry { .. } => "net_vpn".into(), Status::Disconnected => "net_wired".into(), Status::Error => "net_down".into(), } diff --git a/src/blocks/vpn/mullvad.rs b/src/blocks/vpn/mullvad.rs index a955f2e3b9..ecfc86495e 100644 --- a/src/blocks/vpn/mullvad.rs +++ b/src/blocks/vpn/mullvad.rs @@ -77,7 +77,7 @@ impl Driver for MullvadDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { Status::Connected | Status::ConnectedToCountry { .. } => { - Self::run_network_command("disconnect").await? + Self::run_network_command("disconnect").await?; } Status::Disconnected => Self::run_network_command("connect").await?, Status::Error => (), diff --git a/src/blocks/vpn/nordvpn.rs b/src/blocks/vpn/nordvpn.rs index c7ecc56d5c..e1f2c98b71 100644 --- a/src/blocks/vpn/nordvpn.rs +++ b/src/blocks/vpn/nordvpn.rs @@ -87,7 +87,7 @@ impl Driver for NordVpnDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { Status::Connected | Status::ConnectedToCountry { .. } => { - Self::run_network_command("disconnect").await? + Self::run_network_command("disconnect").await?; } Status::Disconnected => Self::run_network_command("connect").await?, Status::Error => (), From 46c5518b83f1288be9ecd9b0504a23c075d803ad Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 21 Jan 2024 11:02:12 +0100 Subject: [PATCH 6/7] Single connect status with optional country attributes. --- src/blocks/vpn.rs | 25 ++++++++----------------- src/blocks/vpn/mullvad.rs | 8 ++++---- src/blocks/vpn/nordvpn.rs | 8 ++++---- src/blocks/vpn/wireguard.rs | 7 +++++-- 4 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/blocks/vpn.rs b/src/blocks/vpn.rs index f2bcb0ed7f..11aec238a4 100644 --- a/src/blocks/vpn.rs +++ b/src/blocks/vpn.rs @@ -117,10 +117,9 @@ pub struct Config { } enum Status { - Connected, - ConnectedToCountry { - country: String, - country_flag: String, + Connected { + country: Option, + country_flag: Option, }, Disconnected, Error, @@ -129,7 +128,7 @@ enum Status { impl Status { fn icon(&self) -> Cow<'static, str> { match self { - Status::Connected | Status::ConnectedToCountry { .. } => "net_vpn".into(), + Status::Connected { .. } => "net_vpn".into(), Status::Disconnected => "net_wired".into(), Status::Error => "net_down".into(), } @@ -157,22 +156,14 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut widget = Widget::new(); widget.state = match &status { - Status::ConnectedToCountry { + Status::Connected { country, country_flag, } => { widget.set_values(map!( - "icon" => Value::icon(status.icon()), - "country" => Value::text(country.to_string()), - "flag" => Value::text(country_flag.to_string()), - - )); - widget.set_format(format_connected.clone()); - config.state_connected - } - Status::Connected => { - widget.set_values(map!( - "icon" => Value::icon(status.icon()), + "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 ecfc86495e..8819b5aa55 100644 --- a/src/blocks/vpn/mullvad.rs +++ b/src/blocks/vpn/mullvad.rs @@ -66,9 +66,9 @@ impl Driver for MullvadDriver { }) .unwrap_or_default(); - return Ok(Status::ConnectedToCountry { - country, - country_flag, + return Ok(Status::Connected { + country: Some(country), + country_flag: Some(country_flag), }); } Ok(Status::Error) @@ -76,7 +76,7 @@ impl Driver for MullvadDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected | Status::ConnectedToCountry { .. } => { + Status::Connected { .. } => { Self::run_network_command("disconnect").await?; } Status::Disconnected => Self::run_network_command("connect").await?, diff --git a/src/blocks/vpn/nordvpn.rs b/src/blocks/vpn/nordvpn.rs index e1f2c98b71..06aac2369f 100644 --- a/src/blocks/vpn/nordvpn.rs +++ b/src/blocks/vpn/nordvpn.rs @@ -76,9 +76,9 @@ impl Driver for NordVpnDriver { .unwrap_or_default(), None => String::default(), }; - return Ok(Status::ConnectedToCountry { - country, - country_flag, + return Ok(Status::Connected { + country: Some(country), + country_flag: Some(country_flag), }); } Ok(Status::Error) @@ -86,7 +86,7 @@ impl Driver for NordVpnDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected | Status::ConnectedToCountry { .. } => { + Status::Connected { .. } => { Self::run_network_command("disconnect").await?; } Status::Disconnected => Self::run_network_command("connect").await?, diff --git a/src/blocks/vpn/wireguard.rs b/src/blocks/vpn/wireguard.rs index 1b085eb4a5..1b5305e1e5 100644 --- a/src/blocks/vpn/wireguard.rs +++ b/src/blocks/vpn/wireguard.rs @@ -30,7 +30,10 @@ impl Driver for WireguardDriver { match status { Ok(status) => { if status.contains(format!("interface: {}", self.interface).as_str()) { - Ok(Status::Connected) + Ok(Status::Connected { + country: None, + country_flag: None, + }) } else { Ok(Status::Disconnected) } @@ -41,7 +44,7 @@ impl Driver for WireguardDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected | Status::ConnectedToCountry { .. } => { + Status::Connected { .. } => { run_wg_quick(&["down", self.interface.as_str()]).await?; } Status::Disconnected => { From d1558536b7e08dd73b512a8793283a608bc87bb7 Mon Sep 17 00:00:00 2001 From: Niels <1229068+ppx17@users.noreply.github.com> Date: Sun, 21 Jan 2024 11:04:35 +0100 Subject: [PATCH 7/7] Little clean-up. --- src/blocks/vpn/mullvad.rs | 4 +--- src/blocks/vpn/nordvpn.rs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/blocks/vpn/mullvad.rs b/src/blocks/vpn/mullvad.rs index 8819b5aa55..2cc56ec6e0 100644 --- a/src/blocks/vpn/mullvad.rs +++ b/src/blocks/vpn/mullvad.rs @@ -76,9 +76,7 @@ impl Driver for MullvadDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected { .. } => { - Self::run_network_command("disconnect").await?; - } + Status::Connected { .. } => Self::run_network_command("disconnect").await?, Status::Disconnected => Self::run_network_command("connect").await?, Status::Error => (), } diff --git a/src/blocks/vpn/nordvpn.rs b/src/blocks/vpn/nordvpn.rs index 06aac2369f..79228686ec 100644 --- a/src/blocks/vpn/nordvpn.rs +++ b/src/blocks/vpn/nordvpn.rs @@ -86,9 +86,7 @@ impl Driver for NordVpnDriver { async fn toggle_connection(&self, status: &Status) -> Result<()> { match status { - Status::Connected { .. } => { - Self::run_network_command("disconnect").await?; - } + Status::Connected { .. } => Self::run_network_command("disconnect").await?, Status::Disconnected => Self::run_network_command("connect").await?, Status::Error => (), }