diff options
| author | David Lönnhager <david.l@mullvad.net> | 2022-06-07 15:16:31 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2022-06-14 13:59:49 +0200 |
| commit | f3acffca0c5da48c854658a19d11eed4104036b0 (patch) | |
| tree | 8ca38b0a72ebc231b891c244faf52ebb11f9899f | |
| parent | 463042e83845efd09f61cd99feb65db58240bb32 (diff) | |
| download | mullvadvpn-f3acffca0c5da48c854658a19d11eed4104036b0.tar.xz mullvadvpn-f3acffca0c5da48c854658a19d11eed4104036b0.zip | |
Track excluded processes and add CLI command for listing them on Windows
| -rw-r--r-- | mullvad-cli/src/cmds/split_tunnel/windows.rs | 41 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 17 | ||||
| -rw-r--r-- | mullvad-daemon/src/management_interface.rs | 36 | ||||
| -rw-r--r-- | mullvad-management-interface/proto/management_interface.proto | 11 | ||||
| -rw-r--r-- | talpid-core/src/split_tunnel/windows/mod.rs | 60 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 5 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 5 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnected_state.rs | 5 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnecting_state.rs | 15 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/error_state.rs | 5 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 6 |
11 files changed, 203 insertions, 3 deletions
diff --git a/mullvad-cli/src/cmds/split_tunnel/windows.rs b/mullvad-cli/src/cmds/split_tunnel/windows.rs index 0bc8d468bc..9766607ce1 100644 --- a/mullvad-cli/src/cmds/split_tunnel/windows.rs +++ b/mullvad-cli/src/cmds/split_tunnel/windows.rs @@ -1,3 +1,5 @@ +use std::{ffi::OsStr, path::Path}; + use crate::{new_rpc_client, Command, Result}; pub struct SplitTunnel; @@ -23,11 +25,13 @@ impl Command for SplitTunnel { ), ) .subcommand(clap::App::new("get").about("Display the split tunnel status")) + .subcommand(create_pid_subcommand()) } async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { match matches.subcommand() { Some(("app", matches)) => Self::handle_app_subcommand(matches).await, + Some(("pid", matches)) => Self::handle_pid_subcommand(matches).await, Some(("get", _)) => self.get().await, Some(("set", matches)) => { let enabled = matches.value_of("policy").expect("missing policy"); @@ -50,6 +54,16 @@ fn create_app_subcommand() -> clap::App<'static> { .subcommand(clap::App::new("clear")) } +fn create_pid_subcommand() -> clap::App<'static> { + clap::App::new("pid") + .about("Manages processes (PIDs) excluded from the tunnel") + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .subcommand(clap::App::new("list") + .about("List processes that are currently being excluded, i.e. their PIDs, as well as whether \ + they are excluded because of their executable paths or because they're subprocesses of \ + such processes")) +} + impl SplitTunnel { async fn handle_app_subcommand(matches: &clap::ArgMatches) -> Result<()> { match matches.subcommand() { @@ -91,6 +105,33 @@ impl SplitTunnel { } } + async fn handle_pid_subcommand(matches: &clap::ArgMatches) -> Result<()> { + match matches.subcommand() { + Some(("list", _)) => { + let processes = new_rpc_client() + .await? + .get_excluded_processes(()) + .await? + .into_inner(); + + for process in &processes.processes { + let subproc = if process.inherited { "subprocess" } else { "" }; + println!( + "{:<7}{subproc:<12}{}", + process.pid, + Path::new(&process.image) + .file_name() + .unwrap_or(OsStr::new("unknown")) + .to_string_lossy() + ); + } + + Ok(()) + } + _ => unreachable!("unhandled subcommand"), + } + } + async fn set(&self, enabled: bool) -> Result<()> { let mut rpc = new_rpc_client().await?; rpc.set_split_tunnel_state(enabled).await?; diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index c013361925..8dfcf67ace 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -265,9 +265,12 @@ pub enum DaemonCommand { /// Clear list of apps to exclude from the tunnel #[cfg(windows)] ClearSplitTunnelApps(ResponseTx<(), Error>), - /// Disable split tunnel + /// Enable or disable split tunneling #[cfg(windows)] SetSplitTunnelState(ResponseTx<(), Error>, bool), + /// Returns all processes currently being excluded from the tunnel + #[cfg(windows)] + GetSplitTunnelProcesses(ResponseTx<Vec<split_tunnel::ExcludedProcess>, split_tunnel::Error>), /// Toggle wireguard-nt on or off #[cfg(target_os = "windows")] UseWireGuardNt(ResponseTx<(), Error>, bool), @@ -1013,6 +1016,8 @@ where ClearSplitTunnelApps(tx) => self.on_clear_split_tunnel_apps(tx).await, #[cfg(windows)] SetSplitTunnelState(tx, enabled) => self.on_set_split_tunnel_state(tx, enabled).await, + #[cfg(windows)] + GetSplitTunnelProcesses(tx) => self.on_get_split_tunnel_processes(tx), #[cfg(target_os = "windows")] UseWireGuardNt(tx, state) => self.on_use_wireguard_nt(tx, state).await, #[cfg(target_os = "windows")] @@ -1674,6 +1679,14 @@ where } #[cfg(windows)] + fn on_get_split_tunnel_processes( + &self, + tx: ResponseTx<Vec<split_tunnel::ExcludedProcess>, split_tunnel::Error>, + ) { + self.send_tunnel_command(TunnelCommand::GetExcludedProcesses(tx)); + } + + #[cfg(windows)] async fn on_use_wireguard_nt(&mut self, tx: ResponseTx<(), Error>, state: bool) { let save_result = self .settings @@ -2198,7 +2211,7 @@ where } } - fn send_tunnel_command(&mut self, command: TunnelCommand) { + fn send_tunnel_command(&self, command: TunnelCommand) { self.tunnel_command_tx .unbounded_send(command) .expect("Tunnel state machine has stopped"); diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index a7478eb048..7999fd9c55 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -768,6 +768,42 @@ impl ManagementService for ManagementServiceImpl { } #[cfg(windows)] + async fn get_excluded_processes( + &self, + _: Request<()>, + ) -> ServiceResult<types::ExcludedProcessList> { + log::debug!("get_excluded_processes"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetSplitTunnelProcesses(tx))?; + self.wait_for_result(rx) + .await? + .map_err(map_split_tunnel_error) + .map(|processes| { + Response::new(types::ExcludedProcessList { + processes: processes + .into_iter() + .map(|process| types::ExcludedProcess { + // FIXME: This is necessarily 32 bits or less + pid: u32::try_from(process.pid).unwrap(), + image: process.image.into_os_string().to_string_lossy().to_string(), + inherited: process.inherited, + }) + .collect(), + }) + }) + } + + #[cfg(not(windows))] + async fn get_excluded_processes( + &self, + _: Request<()>, + ) -> ServiceResult<types::ExcludedProcessList> { + Ok(Response::new(types::ExcludedProcessList { + processes: vec![], + })) + } + + #[cfg(windows)] async fn set_use_wireguard_nt(&self, request: Request<bool>) -> ServiceResult<()> { log::debug!("set_use_wireguard_nt"); let state = request.into_inner(); diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 9148a10150..2f39f496d6 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -79,6 +79,7 @@ service ManagementService { rpc RemoveSplitTunnelApp(google.protobuf.StringValue) returns (google.protobuf.Empty) {} rpc ClearSplitTunnelApps(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc SetSplitTunnelState(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc GetExcludedProcesses(google.protobuf.Empty) returns (ExcludedProcessList) {} rpc SetUseWireguardNt(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} @@ -476,6 +477,16 @@ message PublicKey { google.protobuf.Timestamp created = 2; } +message ExcludedProcess { + uint32 pid = 1; + string image = 2; + bool inherited = 3; +} + +message ExcludedProcessList { + repeated ExcludedProcess processes = 1; +} + message AppVersionInfo { bool supported = 1; string latest_stable = 2; diff --git a/talpid-core/src/split_tunnel/windows/mod.rs b/talpid-core/src/split_tunnel/windows/mod.rs index 5b5b29e0cc..8b679106ea 100644 --- a/talpid-core/src/split_tunnel/windows/mod.rs +++ b/talpid-core/src/split_tunnel/windows/mod.rs @@ -12,15 +12,17 @@ use crate::{ }; use futures::channel::{mpsc, oneshot}; use std::{ + collections::HashMap, convert::TryFrom, ffi::{OsStr, OsString}, io, mem, net::{IpAddr, Ipv4Addr, Ipv6Addr}, os::windows::io::{AsRawHandle, RawHandle}, + path::{Path, PathBuf}, ptr, sync::{ atomic::{AtomicBool, Ordering}, - mpsc as sync_mpsc, Arc, Mutex, Weak, + mpsc as sync_mpsc, Arc, Mutex, RwLock, Weak, }, time::Duration, }; @@ -102,6 +104,7 @@ pub struct SplitTunnel { request_tx: RequestTx, event_thread: Option<std::thread::JoinHandle<()>>, quit_event: Arc<QuitEvent>, + excluded_pids: Arc<RwLock<HashMap<usize, ExcludedProcess>>>, _route_change_callback: Option<WinNetCallbackHandle>, daemon_tx: Weak<mpsc::UnboundedSender<TunnelCommand>>, async_path_update_in_progress: Arc<AtomicBool>, @@ -148,10 +151,23 @@ struct InterfaceAddresses { internet_ipv6: Option<Ipv6Addr>, } +/// Represents a process that is being excluded from the tunnel. +#[derive(Debug, Clone)] +pub struct ExcludedProcess { + /// Process identifier. + pub pid: u32, + /// Path to the image that this process is an instance of. + pub image: PathBuf, + /// If true, then the process is split because its parent was split, + /// not due to its path being in the config. + pub inherited: bool, +} + struct EventThreadContext { handle: Arc<driver::DeviceHandle>, event_overlapped: OVERLAPPED, quit_event: Arc<QuitEvent>, + excluded_pids: Arc<RwLock<HashMap<usize, ExcludedProcess>>>, } unsafe impl Send for EventThreadContext {} @@ -172,11 +188,13 @@ impl SplitTunnel { } let quit_event = Arc::new(QuitEvent::new()); + let excluded_pids = Arc::new(RwLock::new(HashMap::new())); let event_context = EventThreadContext { handle: handle.clone(), event_overlapped, quit_event: quit_event.clone(), + excluded_pids: excluded_pids.clone(), }; let event_thread = std::thread::spawn(move || { @@ -291,6 +309,34 @@ impl SplitTunnel { reason, image, } => { + let mut pids = event_context.excluded_pids.write().unwrap(); + match event_id { + EventId::StartSplittingProcess => { + if let Some(prev_entry) = pids.get(&process_id) { + log::error!("PID collision: {process_id} is already in the list of excluded processes. New image: {:?}. Current image: {:?}", image, prev_entry); + } + pids.insert( + process_id, + ExcludedProcess { + pid: u32::try_from(process_id) + .expect("PID should be containable in a DWORD"), + image: Path::new(&image).to_path_buf(), + inherited: reason.contains( + driver::SplittingChangeReason::BY_INHERITANCE, + ), + }, + ); + } + EventId::StopSplittingProcess => { + if pids.remove(&process_id).is_none() { + log::error!( + "Inconsistent process tree: {process_id} was not found" + ); + } + } + _ => (), + } + log::trace!( "{}:\n\tpid: {}\n\treason: {:?}\n\timage: {:?}", event_str, @@ -326,6 +372,7 @@ impl SplitTunnel { _route_change_callback: None, daemon_tx, async_path_update_in_progress: Arc::new(AtomicBool::new(false)), + excluded_pids, }) } @@ -562,6 +609,17 @@ impl SplitTunnel { self._route_change_callback = None; self.send_request(Request::RegisterIps(InterfaceAddresses::default())) } + + /// Return processes that are currently being excluded. + pub fn get_processes(&self) -> Result<Vec<ExcludedProcess>, Error> { + Ok(self + .excluded_pids + .read() + .unwrap() + .values() + .cloned() + .collect()) + } } impl Drop for SplitTunnel { diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 80e5e28957..beb29590c1 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -277,6 +277,11 @@ impl ConnectedState { shared_values.split_tunnel.set_paths(&paths, result_tx); SameState(self.into()) } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + SameState(self.into()) + } } } diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 7536b26b09..64d21086cd 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -390,6 +390,11 @@ impl ConnectingState { shared_values.split_tunnel.set_paths(&paths, result_tx); SameState(self.into()) } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + SameState(self.into()) + } } } diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index fa91284f6b..281b22b8f2 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -208,6 +208,11 @@ impl TunnelState for DisconnectedState { shared_values.split_tunnel.set_paths(&paths, result_tx); SameState(self.into()) } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + SameState(self.into()) + } None => { Self::reset_dns(shared_values); Finished diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index 2a14d881d6..2c42a1a864 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -57,6 +57,11 @@ impl DisconnectingState { shared_values.split_tunnel.set_paths(&paths, result_tx); AfterDisconnect::Nothing } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + AfterDisconnect::Nothing + } }, AfterDisconnect::Block(reason) => match command { Some(TunnelCommand::AllowLan(allow_lan)) => { @@ -97,6 +102,11 @@ impl DisconnectingState { shared_values.split_tunnel.set_paths(&paths, result_tx); AfterDisconnect::Block(reason) } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + AfterDisconnect::Block(reason) + } None => AfterDisconnect::Block(reason), }, AfterDisconnect::Reconnect(retry_attempt) => match command { @@ -138,6 +148,11 @@ impl DisconnectingState { shared_values.split_tunnel.set_paths(&paths, result_tx); AfterDisconnect::Reconnect(retry_attempt) } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + AfterDisconnect::Reconnect(retry_attempt) + } }, }; diff --git a/talpid-core/src/tunnel_state_machine/error_state.rs b/talpid-core/src/tunnel_state_machine/error_state.rs index a11e49d859..d64393a432 100644 --- a/talpid-core/src/tunnel_state_machine/error_state.rs +++ b/talpid-core/src/tunnel_state_machine/error_state.rs @@ -209,6 +209,11 @@ impl TunnelState for ErrorState { shared_values.split_tunnel.set_paths(&paths, result_tx); SameState(self.into()) } + #[cfg(windows)] + Some(TunnelCommand::GetExcludedProcesses(result_tx)) => { + let _ = result_tx.send(shared_values.split_tunnel.get_processes()); + SameState(self.into()) + } } } } diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 6c388a4680..42d20181a4 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -189,6 +189,12 @@ pub enum TunnelCommand { oneshot::Sender<Result<(), split_tunnel::Error>>, Vec<OsString>, ), + /// Return a list of processes that are currently being split, as well as their + /// paths. + #[cfg(windows)] + GetExcludedProcesses( + oneshot::Sender<Result<Vec<split_tunnel::ExcludedProcess>, split_tunnel::Error>>, + ), } type TunnelCommandReceiver = stream::Fuse<mpsc::UnboundedReceiver<TunnelCommand>>; |
