i3status_rs/blocks/vpn/
mullvad.rs1use itertools::Itertools as _;
2use std::process::Stdio;
3use tokio::process::Command;
4
5use crate::blocks::prelude::*;
6use crate::util::country_flag_from_iso_code;
7
8use super::{Driver, Status};
9
10pub struct MullvadDriver {}
11
12impl MullvadDriver {
13 pub async fn new() -> MullvadDriver {
14 MullvadDriver {}
15 }
16
17 async fn run_network_command(arg: &str) -> Result<()> {
18 let code = Command::new("mullvad")
19 .args([arg])
20 .stdin(Stdio::null())
21 .stdout(Stdio::null())
22 .spawn()
23 .error(format!("Problem running mullvad command: {arg}"))?
24 .wait()
25 .await
26 .error(format!("Problem running mullvad command: {arg}"))?;
27
28 if code.success() {
29 sleep(Duration::from_secs(1)).await;
32 Ok(())
33 } else {
34 Err(Error::new(format!(
35 "mullvad command failed with nonzero status: {code:?}"
36 )))
37 }
38 }
39}
40
41#[async_trait]
42impl Driver for MullvadDriver {
43 async fn get_status(&self) -> Result<Status> {
44 let stdout = Command::new("mullvad")
45 .args(["status", "-j"])
46 .output()
47 .await
48 .error("Problem running `mullvad status -j`")?
49 .stdout;
50
51 let json_status_output = String::from_utf8_lossy(&stdout);
52
53 let json_status = json_status_output
56 .lines()
57 .skip_while(|line| !line.starts_with("{"))
58 .join("\n");
59
60 let status: MullvadCliStatus =
61 serde_json::from_str(&json_status).error("`mullvad status -j` produced wrong JSON")?;
62
63 match status {
64 MullvadCliStatus::Disconnected | MullvadCliStatus::Disconnecting => {
65 Ok(Status::Disconnected { profile: None })
66 }
67 MullvadCliStatus::Connected { details } => {
68 let country_code = details
69 .location
70 .hostname
71 .map(|hostname| hostname[0..2].to_uppercase());
72
73 let country_flag = country_code
74 .as_ref()
75 .map(|code| country_flag_from_iso_code(code));
76
77 Ok(Status::Connected {
78 country: country_code,
79 country_flag,
80 profile: None,
81 })
82 }
83 MullvadCliStatus::Connecting => Ok(Status::Connecting { profile: None }),
84 MullvadCliStatus::Error { details } => {
85 let error = details.and_then(|details| details.cause).and_then(|cause| {
86 if let Some(reason) = cause.reason
87 && let Some(details) = cause.details
88 {
89 Some(format!("{}: {}", reason, details))
90 } else {
91 None
92 }
93 });
94
95 Ok(Status::Error(error))
96 }
97 }
98 }
99
100 async fn toggle_connection(&self, status: &Status) -> Result<()> {
101 match status {
102 Status::Connected { .. } | Status::Connecting { .. } => {
103 Self::run_network_command("disconnect").await?;
104 }
105 Status::Disconnected { .. } => Self::run_network_command("connect").await?,
106 Status::Error(_) => (),
107 }
108 Ok(())
109 }
110}
111
112#[derive(Deserialize, Debug, Clone)]
113#[serde(tag = "state")]
114#[serde(rename_all = "snake_case")]
115enum MullvadCliStatus {
116 Connected { details: Details },
117
118 Disconnected,
119
120 Connecting,
121
122 Disconnecting,
123
124 Error { details: Option<ErrorDetails> },
125}
126
127#[derive(Deserialize, Debug, Clone)]
128struct Details {
129 location: Location,
130}
131
132#[derive(Deserialize, Debug, Clone)]
133struct Location {
134 hostname: Option<String>,
135}
136
137#[derive(Deserialize, Debug, Clone)]
138struct ErrorDetails {
139 cause: Option<ErrorCause>,
140}
141
142#[derive(Deserialize, Debug, Clone)]
143struct ErrorCause {
144 reason: Option<String>,
145 details: Option<String>,
146}