Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wireguard support to vpn block #1981

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 25 additions & 0 deletions src/blocks/vpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//! ------------|-----------------------------------------------------------|--------|------
Expand All @@ -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:
Expand Down Expand Up @@ -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::*;
Expand All @@ -80,6 +99,7 @@ pub enum DriverType {
#[default]
Nordvpn,
Mullvad,
Wireguard,
}

#[derive(Deserialize, Debug, SmartDefault)]
Expand All @@ -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 {
Expand Down Expand Up @@ -123,6 +145,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let driver: Box<dyn Driver> = 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 {
Expand Down
95 changes: 95 additions & 0 deletions src/blocks/vpn/wireguard.rs
Original file line number Diff line number Diff line change
@@ -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<Status> {
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<String> {
ppx17 marked this conversation as resolved.
Show resolved Hide resolved
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<()> {
ppx17 marked this conversation as resolved.
Show resolved Hide resolved
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())
}