Skip to main content

i3status_rs/blocks/
vpn.rs

1//! Shows the current connection status for VPN networks
2//!
3//! This widget toggles the connection on left click.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `driver` | Which vpn should be used . Available drivers are: `"nordvpn"`, `"mullvad"`, `"tailscale"` | `"nordvpn"`
10//! `interval` | Update interval in seconds. | `10`
11//! `format_connected` | A string to customise the output in case the network is connected. See below for available placeholders. | `" VPN: $icon "`
12//! `format_disconnected` | A string to customise the output in case the network is disconnected. See below for available placeholders. | `" VPN: $icon "`
13//! `format_connecting` | A string to customise the output in case the network is in a connecting state. See below for available placeholders. | `" VPN: $icon "`
14//! `state_connected` | The widgets state if the vpn network is connected. | `info`
15//! `state_disconnected` | The widgets state if the vpn network is disconnected | `idle`
16//!
17//! Placeholder | Value                                                     | Type   | Unit
18//! ------------|-----------------------------------------------------------|--------|------
19//! `icon`      | A static icon                                             | Icon   | -
20//! `country`   | Country currently connected to                            | Text   | -
21//! `flag`      | Country specific flag (depends on a font supporting them) | Text   | -
22//! `profile`   | Currently selected profile configuration (tailnet)        | Text   | -
23//! `error`     | Error message if any                                      | Text   | -
24//!
25//! Action    | Default button | Description
26//! ----------|----------------|-----------------------------------
27//! `toggle`  | Left           | toggles the vpn network connection
28//!
29//! # Drivers
30//!
31//! ## Mullvad
32//! 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.
33//!
34//! ## nordvpn
35//! Behind the scenes the nordvpn driver uses the `nordvpn` command line binary. In order for this to work
36//! properly the binary should be executable without root privileges.
37//!
38//! ## Tailscale
39//! Behind the scenes the tailscale driver uses the `tailscale` command line binary.
40//! In order for this to work properly the tailscale daemon should be running and the user must be configured as operator:
41//! ```sh
42//! sudo tailscale set --operator=$USER
43//! ```
44//!
45//! ## Cloudflare WARP
46//! Behind the scenes the WARP driver uses the `warp-cli` command line binary. Just ensure the binary is executable without root privileges.
47//!
48//! # Example
49//!
50//! Shows the current vpn network state:
51//!
52//! ```toml
53//! [[block]]
54//! block = "vpn"
55//! driver = "nordvpn"
56//! interval = 10
57//! format_connected = "VPN: $icon "
58//! format_disconnected = "VPN: $icon "
59//! format_connecting = "VPN: $icon "
60//! state_connected = "good"
61//! state_disconnected = "warning"
62//! ```
63//!
64//! Possible values for `state_connected` and `state_disconnected`:
65//!
66//! ```text
67//! warning
68//! critical
69//! good
70//! info
71//! idle
72//! ```
73//!
74//! # Icons Used
75//!
76//! - `net_vpn`
77//! - `net_wired`
78//! - `net_wireless` (for connecting state)
79//! - `net_down`
80//! - country code flags (if supported by font)
81//!
82//! Flags: They are not icons but unicode glyphs. You will need a font that
83//! includes them. Tested with: <https://www.babelstone.co.uk/Fonts/Flags.html>
84
85mod mullvad;
86use mullvad::MullvadDriver;
87mod nordvpn;
88use nordvpn::NordVpnDriver;
89mod tailscale;
90use tailscale::TailscaleDriver;
91mod warp;
92use warp::WarpDriver;
93
94use super::prelude::*;
95
96#[derive(Deserialize, Debug, SmartDefault)]
97#[serde(rename_all = "snake_case")]
98pub enum DriverType {
99    Mullvad,
100    #[default]
101    Nordvpn,
102    Tailscale,
103    Warp,
104}
105
106#[derive(Deserialize, Debug, SmartDefault)]
107#[serde(deny_unknown_fields, default)]
108pub struct Config {
109    pub driver: DriverType,
110    #[default(10.into())]
111    pub interval: Seconds,
112    pub format_connected: FormatConfig,
113    pub format_disconnected: FormatConfig,
114    pub format_connecting: FormatConfig,
115    pub state_connected: State,
116    pub state_disconnected: State,
117}
118
119enum Status {
120    Connected {
121        country: Option<String>,
122        country_flag: Option<String>,
123        profile: Option<String>,
124    },
125    Connecting {
126        profile: Option<String>,
127    },
128    Disconnected {
129        profile: Option<String>,
130    },
131    Error(Option<String>),
132}
133
134impl Status {
135    fn icon(&self) -> Cow<'static, str> {
136        match self {
137            Status::Connected { .. } => "net_vpn".into(),
138            Status::Disconnected { .. } => "net_wired".into(),
139            Status::Connecting { .. } => "net_wireless".into(),
140            Status::Error(_) => "net_down".into(),
141        }
142    }
143}
144
145pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
146    let mut actions = api.get_actions()?;
147    api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;
148
149    let format_connected = config.format_connected.with_default(" VPN: $icon ")?;
150    let format_disconnected = config.format_disconnected.with_default(" VPN: $icon ")?;
151    let format_connecting = config.format_connecting.with_default(" VPN: $icon ")?;
152
153    let driver: Box<dyn Driver> = match config.driver {
154        DriverType::Mullvad => Box::new(MullvadDriver::new().await),
155        DriverType::Nordvpn => Box::new(NordVpnDriver::new().await),
156        DriverType::Tailscale => Box::new(TailscaleDriver::new().await),
157        DriverType::Warp => Box::new(WarpDriver::new().await),
158    };
159
160    loop {
161        let status = driver.get_status().await?;
162
163        let mut widget = Widget::new();
164
165        widget.state = match &status {
166            Status::Connected {
167                country,
168                country_flag,
169                profile,
170            } => {
171                widget.set_values(map!(
172                        "icon" => Value::icon(status.icon()),
173                        [if let Some(country) = country] "country" => Value::text(country.into()),
174                        [if let Some(flag) = country_flag] "flag" => Value::text(flag.into()),
175                        [if let Some(profile) = profile] "profile" => Value::text(profile.into()),
176                ));
177                widget.set_format(format_connected.clone());
178                config.state_connected
179            }
180            Status::Disconnected { profile } => {
181                widget.set_values(map! {
182                    "icon" => Value::icon(status.icon()),
183                    [if let Some(profile) = profile] "profile" => Value::text(profile.into()),
184                });
185                widget.set_format(format_disconnected.clone());
186                config.state_disconnected
187            }
188            Status::Connecting { profile } => {
189                widget.set_values(map!(
190                        "icon" => Value::icon(status.icon()),
191                        [if let Some(profile) = profile] "profile" => Value::text(profile.into()),
192                ));
193                widget.set_format(format_connecting.clone());
194                State::Info
195            }
196            Status::Error(error) => {
197                widget.set_values(map!(
198                        "icon" => Value::icon(status.icon()),
199                        [if let Some(error) = error] "error" => Value::text(error.into())
200                ));
201                widget.set_format(format_disconnected.clone());
202                State::Critical
203            }
204        };
205
206        api.set_widget(widget)?;
207
208        select! {
209            _ = sleep(config.interval.0) => (),
210            _ = api.wait_for_update_request() => (),
211            Some(action) = actions.recv() => match action.as_ref() {
212                "toggle" => driver.toggle_connection(&status).await?,
213                _ => (),
214            }
215        }
216    }
217}
218
219#[async_trait]
220trait Driver {
221    async fn get_status(&self) -> Result<Status>;
222    async fn toggle_connection(&self, status: &Status) -> Result<()>;
223}