diff options
| author | David Lönnhager <david.l@mullvad.net> | 2020-06-02 11:03:12 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2020-06-02 11:03:12 +0200 |
| commit | bae528b847e13faf0ea745e29c8d119687241e5c (patch) | |
| tree | 69768eac1f8d27c0359cfa316365da371480bc99 | |
| parent | 3b59a0ae1ca0cd725b166a843364712956bf3335 (diff) | |
| parent | 9ae5ed301218c649b304e31327df2d687ffe6866 (diff) | |
| download | mullvadvpn-bae528b847e13faf0ea745e29c8d119687241e5c.tar.xz mullvadvpn-bae528b847e13faf0ea745e29c8d119687241e5c.zip | |
Merge branch 'split-tunnel-linux'
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 10 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rwxr-xr-x | build.sh | 1 | ||||
| -rw-r--r-- | dist-assets/linux/after-install.sh | 2 | ||||
| -rw-r--r-- | gui/tasks/distribution.js | 2 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/mod.rs | 7 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/split_tunnel.rs | 70 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 62 | ||||
| -rw-r--r-- | mullvad-daemon/src/management_interface.rs | 76 | ||||
| -rw-r--r-- | mullvad-exclude/Cargo.toml | 13 | ||||
| -rw-r--r-- | mullvad-exclude/src/main.rs | 95 | ||||
| -rw-r--r-- | mullvad-ipc-client/src/lib.rs | 17 | ||||
| -rw-r--r-- | talpid-core/src/firewall/linux.rs | 155 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/routing/linux.rs | 215 | ||||
| -rw-r--r-- | talpid-core/src/routing/unix.rs | 85 | ||||
| -rw-r--r-- | talpid-core/src/split_tunnel.rs | 162 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 21 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 8 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnected_state.rs | 7 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 6 | ||||
| -rw-r--r-- | talpid-types/src/lib.rs | 3 |
23 files changed, 976 insertions, 46 deletions
diff --git a/.gitignore b/.gitignore index b6e4ca74d3..bb585e09e2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /dist-assets/mullvad.exe /dist-assets/mullvad-daemon /dist-assets/mullvad-daemon.exe +/dist-assets/mullvad-exclude /dist-assets/mullvad-setup /dist-assets/mullvad-setup.exe /dist-assets/mullvad-problem-report diff --git a/Cargo.lock b/Cargo.lock index 92246ebd57..2635701b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,6 +1410,16 @@ dependencies = [ ] [[package]] +name = "mullvad-exclude" +version = "0.1.0" +dependencies = [ + "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "nix 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "talpid-types 0.1.0", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "mullvad-ipc-client" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml index bbaaee503a..2debcc071c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "mullvad-types", "mullvad-rpc", "mullvad-tests", + "mullvad-exclude", "talpid-openvpn-plugin", "talpid-core", "talpid-ipc", @@ -167,6 +167,7 @@ elif [[ ("$(uname -s)" == "Linux") ]]; then mullvad-problem-report libtalpid_openvpn_plugin.so mullvad-setup + mullvad-exclude ) elif [[ ("$(uname -s)" == "MINGW"*) ]]; then binaries=( diff --git a/dist-assets/linux/after-install.sh b/dist-assets/linux/after-install.sh index 0abe49cbe5..b75b72dc31 100644 --- a/dist-assets/linux/after-install.sh +++ b/dist-assets/linux/after-install.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -eu +chmod u+s "/usr/bin/mullvad-exclude" + if which systemctl &> /dev/null; then systemctl enable "/opt/Mullvad VPN/resources/mullvad-daemon.service" systemctl start mullvad-daemon.service diff --git a/gui/tasks/distribution.js b/gui/tasks/distribution.js index 30c0fe1f03..4e8061a7fa 100644 --- a/gui/tasks/distribution.js +++ b/gui/tasks/distribution.js @@ -126,6 +126,7 @@ const config = { '--config-files', '/opt/Mullvad VPN/resources/mullvad-daemon.conf', distAssets('mullvad') + '=/usr/bin/', + distAssets('mullvad-exclude') + '=/usr/bin/', distAssets('linux/problem-report-link') + '=/usr/bin/mullvad-problem-report', distAssets('shell-completions/mullvad.bash') + '=/usr/share/bash-completion/completions/mullvad', @@ -149,6 +150,7 @@ const config = { '--config-files', '/opt/Mullvad VPN/resources/mullvad-daemon.conf', distAssets('mullvad') + '=/usr/bin/', + distAssets('mullvad-exclude') + '=/usr/bin/', distAssets('linux/problem-report-link') + '=/usr/bin/mullvad-problem-report', distAssets('shell-completions/mullvad.bash') + '=/usr/share/bash-completion/completions/mullvad', diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 699b62ba1d..9cdefd19c5 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -34,6 +34,11 @@ pub use self::relay::Relay; mod reset; pub use self::reset::Reset; +#[cfg(target_os = "linux")] +mod split_tunnel; +#[cfg(target_os = "linux")] +pub use self::split_tunnel::SplitTunnel; + mod status; pub use self::status::Status; @@ -57,6 +62,8 @@ pub fn get_commands() -> HashMap<&'static str, Box<dyn Command>> { Box::new(Lan), Box::new(Relay), Box::new(Reset), + #[cfg(target_os = "linux")] + Box::new(SplitTunnel), Box::new(Status), Box::new(Tunnel), Box::new(Version), diff --git a/mullvad-cli/src/cmds/split_tunnel.rs b/mullvad-cli/src/cmds/split_tunnel.rs new file mode 100644 index 0000000000..95b172eb6b --- /dev/null +++ b/mullvad-cli/src/cmds/split_tunnel.rs @@ -0,0 +1,70 @@ +use crate::{new_rpc_client, Command, Result}; +use clap::value_t_or_exit; + +pub struct SplitTunnel; + +impl Command for SplitTunnel { + fn name(&self) -> &'static str { + "split-tunnel" + } + + fn clap_subcommand(&self) -> clap::App<'static, 'static> { + clap::SubCommand::with_name(self.name()) + .about("Manage split tunneling") + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .subcommand(create_pid_subcommand()) + } + + fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + match matches.subcommand() { + ("pid", Some(pid_matches)) => Self::handle_pid_cmd(pid_matches), + _ => unreachable!("unhandled comand"), + } + } +} + +fn create_pid_subcommand() -> clap::App<'static, 'static> { + clap::SubCommand::with_name("pid") + .about("Manage processes to exclude from the tunnel") + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .subcommand( + clap::SubCommand::with_name("add").arg(clap::Arg::with_name("pid").required(true)), + ) + .subcommand( + clap::SubCommand::with_name("delete").arg(clap::Arg::with_name("pid").required(true)), + ) + .subcommand(clap::SubCommand::with_name("clear")) + .subcommand(clap::SubCommand::with_name("list")) +} + +impl SplitTunnel { + fn handle_pid_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { + match matches.subcommand() { + ("add", Some(matches)) => { + let pid = value_t_or_exit!(matches.value_of("pid"), i32); + new_rpc_client()?.add_split_tunnel_process(pid)?; + Ok(()) + } + ("delete", Some(matches)) => { + let pid = value_t_or_exit!(matches.value_of("pid"), i32); + new_rpc_client()?.remove_split_tunnel_process(pid)?; + Ok(()) + } + ("clear", Some(_)) => { + new_rpc_client()?.clear_split_tunnel_processes()?; + Ok(()) + } + ("list", Some(_)) => { + let pids = new_rpc_client()?.get_split_tunnel_processes()?; + println!("Excluded PIDs:"); + + for pid in pids.iter() { + println!(" {}", pid); + } + + Ok(()) + } + _ => unreachable!("unhandled command"), + } + } +} diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index cd9ddd39a1..2b202deab9 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -55,6 +55,8 @@ use std::{ thread, time::Duration, }; +#[cfg(target_os = "linux")] +use talpid_core::split_tunnel; use talpid_core::{ mpsc::Sender, tunnel_state_machine::{self, TunnelCommand, TunnelParametersGenerator}, @@ -95,6 +97,10 @@ pub enum Error { #[error(display = "Unable to load account history with wireguard key cache")] LoadAccountHistory(#[error(source)] account_history::Error), + #[cfg(target_os = "linux")] + #[error(display = "Unable to initialize split tunneling")] + InitSplitTunneling(#[error(source)] split_tunnel::Error), + #[error(display = "No wireguard private key available")] NoKeyAvailable, @@ -212,6 +218,18 @@ pub enum DaemonCommand { /// Remove settings and clear the cache #[cfg(not(target_os = "android"))] FactoryReset(oneshot::Sender<()>), + /// Request list of processes excluded from the tunnel + #[cfg(target_os = "linux")] + GetSplitTunnelProcesses(oneshot::Sender<Vec<i32>>), + /// Exclude traffic of a process (PID) from the tunnel + #[cfg(target_os = "linux")] + AddSplitTunnelProcess(oneshot::Sender<()>, i32), + /// Remove process (PID) from list of processes excluded from the tunnel + #[cfg(target_os = "linux")] + RemoveSplitTunnelProcess(oneshot::Sender<()>, i32), + /// Clear list of processes excluded from the tunnel + #[cfg(target_os = "linux")] + ClearSplitTunnelProcesses(oneshot::Sender<()>), /// Makes the daemon exit the main loop and quit. Shutdown, /// Saves the target tunnel state and enters a blocking state. The state is restored @@ -431,6 +449,8 @@ pub struct Daemon<L: EventListener> { tunnel_state: TunnelState, target_state: TargetState, state: DaemonExecutionState, + #[cfg(target_os = "linux")] + exclude_pids: split_tunnel::PidManager, rx: Wait<UnboundedReceiver<InternalDaemonEvent>>, tx: DaemonEventSender, reconnection_loop_tx: Option<mpsc::Sender<()>>, @@ -574,6 +594,8 @@ where tunnel_state: TunnelState::Disconnected, target_state: initial_target_state, state: DaemonExecutionState::Running, + #[cfg(target_os = "linux")] + exclude_pids: split_tunnel::PidManager::new().map_err(Error::InitSplitTunneling)?, rx: internal_event_rx.wait(), tx: internal_event_tx, reconnection_loop_tx: None, @@ -992,6 +1014,14 @@ where GetCurrentVersion(tx) => self.on_get_current_version(tx), #[cfg(not(target_os = "android"))] FactoryReset(tx) => self.on_factory_reset(tx), + #[cfg(target_os = "linux")] + GetSplitTunnelProcesses(tx) => self.on_get_split_tunnel_processes(tx), + #[cfg(target_os = "linux")] + AddSplitTunnelProcess(tx, pid) => self.on_add_split_tunnel_process(tx, pid), + #[cfg(target_os = "linux")] + RemoveSplitTunnelProcess(tx, pid) => self.on_remove_split_tunnel_process(tx, pid), + #[cfg(target_os = "linux")] + ClearSplitTunnelProcesses(tx) => self.on_clear_split_tunnel_processes(tx), Shutdown => self.trigger_shutdown_event(), PrepareRestart => self.on_prepare_restart(), } @@ -1355,6 +1385,38 @@ where })); } + #[cfg(target_os = "linux")] + fn on_get_split_tunnel_processes(&mut self, tx: oneshot::Sender<Vec<i32>>) { + match self.exclude_pids.list() { + Ok(pids) => Self::oneshot_send(tx, pids, "get_split_tunnel_processes response"), + Err(e) => error!("{}", e.display_chain_with_msg("Unable to obtain PIDs")), + } + } + + #[cfg(target_os = "linux")] + fn on_add_split_tunnel_process(&mut self, tx: oneshot::Sender<()>, pid: i32) { + match self.exclude_pids.add(pid) { + Ok(()) => Self::oneshot_send(tx, (), "add_split_tunnel_process response"), + Err(e) => error!("{}", e.display_chain_with_msg("Unable to add PID")), + } + } + + #[cfg(target_os = "linux")] + fn on_remove_split_tunnel_process(&mut self, tx: oneshot::Sender<()>, pid: i32) { + match self.exclude_pids.remove(pid) { + Ok(()) => Self::oneshot_send(tx, (), "remove_split_tunnel_process response"), + Err(e) => error!("{}", e.display_chain_with_msg("Unable to remove PID")), + } + } + + #[cfg(target_os = "linux")] + fn on_clear_split_tunnel_processes(&mut self, tx: oneshot::Sender<()>) { + match self.exclude_pids.clear() { + Ok(()) => Self::oneshot_send(tx, (), "clear_split_tunnel_processes response"), + Err(e) => error!("{}", e.display_chain_with_msg("Unable to clear PIDs")), + } + } + fn on_update_relay_settings(&mut self, tx: oneshot::Sender<()>, update: RelaySettingsUpdate) { let save_result = self.settings.update_relay_settings(update); match save_result { diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 84c5db8570..75ea62168c 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -178,6 +178,22 @@ build_rpc_trait! { #[rpc(meta, name = "factory_reset")] fn factory_reset(&self, Self::Metadata) -> BoxFuture<(), Error>; + /// Retrieve PIDs to exclude from the tunnel + #[rpc(meta, name = "get_split_tunnel_processes")] + fn get_split_tunnel_processes(&self, Self::Metadata) -> BoxFuture<Vec<i32>, Error>; + + /// Add a process to exclude from the tunnel + #[rpc(meta, name = "add_split_tunnel_process")] + fn add_split_tunnel_process(&self, Self::Metadata, i32) -> BoxFuture<(), Error>; + + /// Remove a process excluded from the tunnel + #[rpc(meta, name = "remove_split_tunnel_process")] + fn remove_split_tunnel_process(&self, Self::Metadata, i32) -> BoxFuture<(), Error>; + + /// Clear list of processes to exclude from the tunnel + #[rpc(meta, name = "clear_split_tunnel_processes")] + fn clear_split_tunnel_processes(&self, Self::Metadata) -> BoxFuture<(), Error>; + #[pubsub(name = "daemon_event")] { /// Subscribes to events from the daemon. #[rpc(name = "daemon_event_subscribe")] @@ -739,6 +755,66 @@ impl ManagementInterfaceApi for ManagementInterface { } } + fn get_split_tunnel_processes(&self, _: Self::Metadata) -> BoxFuture<Vec<i32>, Error> { + #[cfg(target_os = "linux")] + { + log::debug!("get_split_tunnel_processes"); + let (tx, rx) = sync::oneshot::channel(); + let future = self + .send_command_to_daemon(DaemonCommand::GetSplitTunnelProcesses(tx)) + .and_then(|_| rx.map_err(|_| Error::internal_error())); + Box::new(future) + } + #[cfg(not(target_os = "linux"))] + { + Box::new(future::ok(Vec::with_capacity(0))) + } + } + + #[cfg(target_os = "linux")] + fn add_split_tunnel_process(&self, _: Self::Metadata, pid: i32) -> BoxFuture<(), Error> { + log::debug!("add_split_tunnel_process"); + let (tx, rx) = sync::oneshot::channel(); + let future = self + .send_command_to_daemon(DaemonCommand::AddSplitTunnelProcess(tx, pid)) + .and_then(|_| rx.map_err(|_| Error::internal_error())); + Box::new(future) + } + #[cfg(not(target_os = "linux"))] + fn add_split_tunnel_process(&self, _: Self::Metadata, _: i32) -> BoxFuture<(), Error> { + Box::new(future::ok(())) + } + + #[cfg(target_os = "linux")] + fn remove_split_tunnel_process(&self, _: Self::Metadata, pid: i32) -> BoxFuture<(), Error> { + log::debug!("remove_split_tunnel_process"); + let (tx, rx) = sync::oneshot::channel(); + let future = self + .send_command_to_daemon(DaemonCommand::RemoveSplitTunnelProcess(tx, pid)) + .and_then(|_| rx.map_err(|_| Error::internal_error())); + Box::new(future) + } + #[cfg(not(target_os = "linux"))] + fn remove_split_tunnel_process(&self, _: Self::Metadata, _: i32) -> BoxFuture<(), Error> { + Box::new(future::ok(())) + } + + fn clear_split_tunnel_processes(&self, _: Self::Metadata) -> BoxFuture<(), Error> { + #[cfg(target_os = "linux")] + { + log::debug!("clear_split_tunnel_processes"); + let (tx, rx) = sync::oneshot::channel(); + let future = self + .send_command_to_daemon(DaemonCommand::ClearSplitTunnelProcesses(tx)) + .and_then(|_| rx.map_err(|_| Error::internal_error())); + Box::new(future) + } + #[cfg(not(target_os = "linux"))] + { + Box::new(future::ok(())) + } + } + fn daemon_event_subscribe( &self, diff --git a/mullvad-exclude/Cargo.toml b/mullvad-exclude/Cargo.toml new file mode 100644 index 0000000000..340cdf1c9e --- /dev/null +++ b/mullvad-exclude/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mullvad-exclude" +version = "0.1.0" +authors = ["Mullvad VPN"] +license = "GPL-3.0" +edition = "2018" +publish = false + +[target.'cfg(target_os = "linux")'.dependencies] +nix = "0.17" +err-derive = "0.2.1" +void = "1.0.2" +talpid-types = { path = "../talpid-types" } diff --git a/mullvad-exclude/src/main.rs b/mullvad-exclude/src/main.rs new file mode 100644 index 0000000000..cfa180532a --- /dev/null +++ b/mullvad-exclude/src/main.rs @@ -0,0 +1,95 @@ +#[cfg(target_os = "linux")] +use nix::unistd::{execvp, getgid, getpid, getuid, setgid, setuid}; +#[cfg(target_os = "linux")] +use std::{ + env, + error::Error as StdError, + ffi::{CStr, CString, NulError}, + fs, io, + os::unix::ffi::OsStrExt, + path::Path, +}; + +#[cfg(target_os = "linux")] +use talpid_types::SPLIT_TUNNEL_CGROUP_NAME; + +#[cfg(target_os = "linux")] +const NETCLS_DIR: &str = "/sys/fs/cgroup/net_cls/"; + +#[cfg(target_os = "linux")] +const PROGRAM_NAME: &str = "mullvad-exclude"; + +#[cfg(target_os = "linux")] +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +enum Error { + #[error(display = "Invalid arguments")] + InvalidArguments, + + #[error(display = "Cannot set the cgroup")] + AddProcToCGroup(#[error(source)] io::Error), + + #[error(display = "Failed to drop root user privileges for the process")] + DropRootUid(#[error(source)] nix::Error), + + #[error(display = "Failed to drop root group privileges for the process")] + DropRootGid(#[error(source)] nix::Error), + + #[error(display = "Failed to launch the process")] + Exec(#[error(source)] nix::Error), + + #[error(display = "An argument contains interior nul bytes")] + ArgumentNulError(#[error(source)] NulError), +} + +fn main() { + #[cfg(target_os = "linux")] + match run() { + Err(Error::InvalidArguments) => { + let mut args = env::args(); + let program = args.next().unwrap_or(PROGRAM_NAME.to_string()); + eprintln!("Usage {} COMMAND [ARGS]", program); + std::process::exit(1); + } + Err(e) => { + let mut s = format!("{}", e); + let mut source = e.source(); + while let Some(error) = source { + s.push_str(&format!("\nCaused by: {}", error)); + source = error.source(); + } + eprintln!("{}", s); + + std::process::exit(1); + } + _ => unreachable!("execv returned unexpectedly"), + } +} + +#[cfg(target_os = "linux")] +fn run() -> Result<void::Void, Error> { + let mut args_iter = env::args_os().skip(1); + let program = args_iter.next().ok_or(Error::InvalidArguments)?; + let program = CString::new(program.as_bytes()).map_err(Error::ArgumentNulError)?; + + let args: Vec<CString> = env::args_os() + .skip(1) + .map(|arg| CString::new(arg.as_bytes())) + .collect::<Result<Vec<CString>, NulError>>() + .map_err(Error::ArgumentNulError)?; + let args: Vec<&CStr> = args.iter().map(|arg| &**arg).collect(); + + // Set the cgroup of this process + let cgroup_dir = Path::new(NETCLS_DIR).join(SPLIT_TUNNEL_CGROUP_NAME); + let procs_path = cgroup_dir.join("cgroup.procs"); + fs::write(procs_path, getpid().to_string().as_bytes()).map_err(Error::AddProcToCGroup)?; + + // Drop root privileges + let real_uid = getuid(); + setuid(real_uid).map_err(Error::DropRootUid)?; + let real_gid = getgid(); + setgid(real_gid).map_err(Error::DropRootGid)?; + + // Launch the process + execvp(&program, &args).map_err(Error::Exec) +} diff --git a/mullvad-ipc-client/src/lib.rs b/mullvad-ipc-client/src/lib.rs index 511f68ae3e..93817c7328 100644 --- a/mullvad-ipc-client/src/lib.rs +++ b/mullvad-ipc-client/src/lib.rs @@ -235,6 +235,23 @@ impl DaemonRpcClient { self.call("update_relay_settings", &[update]) } + pub fn get_split_tunnel_processes(&mut self) -> Result<Vec<i32>> { + self.call("get_split_tunnel_processes", &NO_ARGS) + } + + pub fn add_split_tunnel_process(&mut self, pid: i32) -> Result<()> { + self.call("add_split_tunnel_process", &[pid]) + } + + pub fn remove_split_tunnel_process(&mut self, pid: i32) -> Result<()> { + self.call("remove_split_tunnel_process", &[pid]) + } + + pub fn clear_split_tunnel_processes(&mut self) -> Result<()> { + self.call("clear_split_tunnel_processes", &NO_ARGS) + } + + pub fn call<A, O>(&mut self, method: &'static str, args: &A) -> Result<O> where A: Serialize + Send + 'static, diff --git a/talpid-core/src/firewall/linux.rs b/talpid-core/src/firewall/linux.rs index e291cc9050..abe542d4b3 100644 --- a/talpid-core/src/firewall/linux.rs +++ b/talpid-core/src/firewall/linux.rs @@ -1,5 +1,5 @@ use super::{FirewallArguments, FirewallPolicy, FirewallT}; -use crate::tunnel; +use crate::{split_tunnel, tunnel}; use ipnetwork::IpNetwork; use lazy_static::lazy_static; use libc; @@ -16,6 +16,9 @@ use std::{ }; use talpid_types::net::{Endpoint, TransportProtocol}; +/// Priority for rules that tag split tunneling packets. Equals NF_IP_PRI_MANGLE. +const MANGLE_CHAIN_PRIORITY: i32 = libc::NF_IP_PRI_MANGLE; + pub type Result<T> = std::result::Result<T, Error>; /// Errors that can happen when interacting with Linux netfilter. @@ -55,8 +58,16 @@ lazy_static! { /// TODO(linus): This crate is not supposed to be Mullvad-aware. So at some point this should be /// replaced by allowing the table name to be configured from the public API of this crate. static ref TABLE_NAME: CString = CString::new("mullvad").unwrap(); - static ref IN_CHAIN_NAME: CString = CString::new("in").unwrap(); - static ref OUT_CHAIN_NAME: CString = CString::new("out").unwrap(); + static ref IN_CHAIN_NAME: CString = CString::new("input").unwrap(); + static ref OUT_CHAIN_NAME: CString = CString::new("output").unwrap(); + + /// We need two separate tables for compatibility with older kernels (holds true for kernel + /// version 4.19 but not 5.6), where the base filter type may not be `nftnl::ChainType::Route` + /// or `nftnl::ChainType::Nat` for inet tables. + static ref MANGLE_TABLE_NAME_V4: CString = CString::new("mullvadmangle4").unwrap(); + static ref MANGLE_TABLE_NAME_V6: CString = CString::new("mullvadmangle6").unwrap(); + static ref MANGLE_CHAIN_NAME: CString = CString::new("mangle").unwrap(); + static ref NAT_CHAIN_NAME: CString = CString::new("nat").unwrap(); /// Allows controlling whether firewall rules should have packet counters or not from an env /// variable. Useful for debugging the rules. @@ -78,39 +89,50 @@ enum End { } /// The Linux implementation for the firewall and DNS. -pub struct Firewall { - table_name: CString, +pub struct Firewall(()); + +struct FirewallTables { + main: Table, + mangle_v4: Table, + mangle_v6: Table, } impl FirewallT for Firewall { type Error = Error; fn new(_args: FirewallArguments) -> Result<Self> { - Ok(Firewall { - table_name: TABLE_NAME.clone(), - }) + Ok(Firewall(())) } fn apply_policy(&mut self, policy: FirewallPolicy) -> Result<()> { - let table = Table::new(&self.table_name, ProtoFamily::Inet); - let batch = PolicyBatch::new(&table).finalize(&policy)?; + let tables = FirewallTables { + main: Table::new(&*TABLE_NAME, ProtoFamily::Inet), + mangle_v4: Table::new(&*MANGLE_TABLE_NAME_V4, ProtoFamily::Ipv4), + mangle_v6: Table::new(&*MANGLE_TABLE_NAME_V6, ProtoFamily::Ipv6), + }; + let batch = PolicyBatch::new(&tables).finalize(&policy)?; self.send_and_process(&batch)?; - self.verify_tables(&[&TABLE_NAME]) + self.verify_tables(&[&TABLE_NAME, &MANGLE_TABLE_NAME_V4, &MANGLE_TABLE_NAME_V6]) } fn reset_policy(&mut self) -> Result<()> { - let table = Table::new(&self.table_name, ProtoFamily::Inet); - let batch = { - let mut batch = Batch::new(); - // Our batch will add and remove the table even though the goal is just to remove it. - // This because only removing it throws a strange error if the table does not exist. - batch.add(&table, nftnl::MsgType::Add); - batch.add(&table, nftnl::MsgType::Del); - batch.finalize() - }; - + let tables = [ + Table::new(&*TABLE_NAME, ProtoFamily::Inet), + Table::new(&*MANGLE_TABLE_NAME_V4, ProtoFamily::Ipv4), + Table::new(&*MANGLE_TABLE_NAME_V6, ProtoFamily::Ipv6), + ]; + let mut batch = Batch::new(); + for table in &tables { + // Our batch will add and remove the table even though the goal is just to remove + // it. This because only removing it throws a strange error if the + // table does not exist. + batch.add(table, nftnl::MsgType::Add); + batch.add(table, nftnl::MsgType::Del); + } + let batch = batch.finalize(); log::debug!("Removing table and chain from netfilter"); - self.send_and_process(&batch) + self.send_and_process(&batch)?; + Ok(()) } } @@ -187,44 +209,119 @@ struct PolicyBatch<'a> { batch: Batch, in_chain: Chain<'a>, out_chain: Chain<'a>, + mangle_chain_v4: Chain<'a>, + mangle_chain_v6: Chain<'a>, + nat_chain_v4: Chain<'a>, + nat_chain_v6: Chain<'a>, } impl<'a> PolicyBatch<'a> { /// Bootstrap a new nftnl message batch object and add the initial messages creating the /// table and chains. - pub fn new(table: &'a Table) -> Self { + pub fn new(tables: &'a FirewallTables) -> Self { let mut batch = Batch::new(); - let mut out_chain = Chain::new(&*OUT_CHAIN_NAME, table); - let mut in_chain = Chain::new(&*IN_CHAIN_NAME, table); + let mut out_chain = Chain::new(&*OUT_CHAIN_NAME, &tables.main); + let mut in_chain = Chain::new(&*IN_CHAIN_NAME, &tables.main); out_chain.set_hook(nftnl::Hook::Out, 0); in_chain.set_hook(nftnl::Hook::In, 0); out_chain.set_policy(nftnl::Policy::Drop); in_chain.set_policy(nftnl::Policy::Drop); - // A little dance that will make sure the table exists, but is cleared. - batch.add(table, nftnl::MsgType::Add); - batch.add(table, nftnl::MsgType::Del); - batch.add(table, nftnl::MsgType::Add); + Self::flush_table(&mut batch, &tables.main); batch.add(&out_chain, nftnl::MsgType::Add); batch.add(&in_chain, nftnl::MsgType::Add); + Self::flush_table(&mut batch, &tables.mangle_v4); + Self::flush_table(&mut batch, &tables.mangle_v6); + + let mut add_mangle_chain = |table| { + let mut chain = Chain::new(&*MANGLE_CHAIN_NAME, table); + chain.set_hook(nftnl::Hook::Out, MANGLE_CHAIN_PRIORITY); + chain.set_type(nftnl::ChainType::Route); + chain.set_policy(nftnl::Policy::Accept); + batch.add(&chain, nftnl::MsgType::Add); + + chain + }; + let mangle_chain_v4 = add_mangle_chain(&tables.mangle_v4); + let mangle_chain_v6 = add_mangle_chain(&tables.mangle_v6); + + let mut add_nat_chain = |table| { + let mut chain = Chain::new(&*NAT_CHAIN_NAME, table); + chain.set_hook(nftnl::Hook::PostRouting, libc::NF_IP_PRI_NAT_SRC); + chain.set_type(nftnl::ChainType::Nat); + chain.set_policy(nftnl::Policy::Accept); + batch.add(&chain, nftnl::MsgType::Add); + + chain + }; + let nat_chain_v4 = add_nat_chain(&tables.mangle_v4); + let nat_chain_v6 = add_nat_chain(&tables.mangle_v6); + PolicyBatch { batch, in_chain, out_chain, + mangle_chain_v4, + mangle_chain_v6, + nat_chain_v4, + nat_chain_v6, } } + /// Creates the table if it does not exist and clears it otherwise. + fn flush_table(batch: &mut Batch, table: &'a Table) { + batch.add(table, nftnl::MsgType::Add); + batch.add(table, nftnl::MsgType::Del); + batch.add(table, nftnl::MsgType::Add); + } + /// Finalize the nftnl message batch by adding every firewall rule needed to satisfy the given /// policy. pub fn finalize(mut self, policy: &FirewallPolicy) -> Result<FinalizedBatch> { self.add_loopback_rules()?; + self.add_split_tunneling_rules(); self.add_dhcp_client_rules(); self.add_policy_specific_rules(policy)?; Ok(self.batch.finalize()) } + fn add_split_tunneling_rules(&mut self) { + let mangle_chains = [&self.mangle_chain_v4, &self.mangle_chain_v6]; + for chain in &mangle_chains { + let mut rule = Rule::new(chain); + rule.add_expr(&nft_expr!(meta cgroup)); + rule.add_expr(&nft_expr!(cmp == split_tunnel::NETCLS_CLASSID)); + rule.add_expr(&nft_expr!(immediate data split_tunnel::MARK)); + rule.add_expr(&nft_expr!(ct mark set)); + rule.add_expr(&nft_expr!(meta mark set)); + self.batch.add(&rule, nftnl::MsgType::Add); + } + + let mut rule = Rule::new(&self.in_chain); + rule.add_expr(&nft_expr!(ct mark)); + rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK)); + add_verdict(&mut rule, &Verdict::Accept); + self.batch.add(&rule, nftnl::MsgType::Add); + + let mut rule = Rule::new(&self.out_chain); + rule.add_expr(&nft_expr!(meta mark)); + rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK)); + add_verdict(&mut rule, &Verdict::Accept); + self.batch.add(&rule, nftnl::MsgType::Add); + + let nat_chains = [&self.nat_chain_v4, &self.nat_chain_v6]; + for chain in &nat_chains { + let mut rule = Rule::new(chain); + rule.add_expr(&nft_expr!(ct mark)); + rule.add_expr(&nft_expr!(cmp == split_tunnel::MARK)); + rule.add_expr(&nft_expr!(masquerade)); + add_verdict(&mut rule, &Verdict::Accept); + self.batch.add(&rule, nftnl::MsgType::Add); + } + } + fn add_loopback_rules(&mut self) -> Result<()> { const LOOPBACK_IFACE_NAME: &str = "lo"; self.batch.add( diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 9ab260eb9a..5df4f2f0aa 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -20,6 +20,9 @@ pub mod routing; mod offline; +/// Split tunneling +pub mod split_tunnel; + /// Working with processes. pub mod process; diff --git a/talpid-core/src/routing/linux.rs b/talpid-core/src/routing/linux.rs index 7a7bfa4865..71becd2a63 100644 --- a/talpid-core/src/routing/linux.rs +++ b/talpid-core/src/routing/linux.rs @@ -1,12 +1,18 @@ -use crate::routing::{imp::RouteManagerCommand, NetNode, Node, RequiredRoute, Route}; +use crate::{ + routing::{imp::RouteManagerCommand, NetNode, Node, RequiredRoute, Route}, + split_tunnel, +}; use talpid_types::ErrorExt; use ipnetwork::IpNetwork; +use regex::Regex; use std::{ collections::{BTreeMap, HashSet}, - io, - net::IpAddr, + fs, + io::{self, BufRead, BufReader, Read, Seek, Write}, + net::{IpAddr, Ipv4Addr}, + process::Command, thread, }; @@ -36,6 +42,9 @@ use rtnetlink::{ use libc::{AF_INET, AF_INET6}; +const ROUTING_TABLE_NAME: &str = "mullvad_exclusions"; +const RT_TABLES_PATH: &str = "/etc/iproute2/rt_tables"; + pub type Result<T> = std::result::Result<T, Error>; @@ -67,8 +76,24 @@ pub enum Error { #[error(display = "Unknown device index - {}", _0)] UnknownDeviceIndex(u32), + /// Unable to create routing table for tagged connections and packets. + #[error(display = "Unable to create routing table for split tunneling")] + ExclusionsRoutingTableSetup(#[error(source)] io::Error), + + /// Unable to create routing table for tagged connections and packets. + #[error(display = "Cannot find a free routing table ID in rt_tables")] + NoFreeRoutingTableId, + #[error(display = "Shutting down route manager")] Shutdown, + + /// Failed to run the process. + #[error(display = "Unable to execute process")] + ExecFailed(#[error(source)] io::Error), + + /// ip command returned an error status. + #[error(display = "ip command failed")] + IpFailed, } pub struct RouteManagerImpl { @@ -146,6 +171,8 @@ pub struct RouteManagerImplInner { default_routes: HashSet<Route>, best_default_node_v4: Option<Node>, best_default_node_v6: Option<Node>, + + split_table_id: i32, } impl RouteManagerImplInner { @@ -163,6 +190,7 @@ impl RouteManagerImplInner { tokio02::spawn(connection); let iface_map = Self::initialize_link_map(&handle).await?; + let split_table_id = Self::initialize_exclusions_table().await?; let mut monitor = Self { iface_map, @@ -175,6 +203,8 @@ impl RouteManagerImplInner { default_routes: HashSet::new(), best_default_node_v4: None, best_default_node_v6: None, + + split_table_id, }; monitor.default_routes = monitor.get_default_routes().await?; @@ -188,6 +218,155 @@ impl RouteManagerImplInner { Ok(monitor) } + /// Set up policy-based routing table for marked packets. + /// Returns the routing table id. + async fn initialize_exclusions_table() -> Result<i32> { + // Add routing table to /etc/iproute2/rt_tables, if it does not exist + + let file = fs::OpenOptions::new() + .read(true) + .open(RT_TABLES_PATH) + .map_err(Error::ExclusionsRoutingTableSetup)?; + let buf_reader = BufReader::new(file); + let expression = Regex::new(r"^\s*(\d+)\s+(\w+)").unwrap(); + + let mut used_ids = Vec::<i32>::new(); + + for line in buf_reader.lines() { + let line = line.map_err(Error::ExclusionsRoutingTableSetup)?; + if let Some(captures) = expression.captures(&line) { + let table_id = captures + .get(1) + .unwrap() + .as_str() + .parse::<i32>() + .expect("Table ID does not fit i32"); + let table_name = captures.get(2).unwrap().as_str(); + + if table_name == ROUTING_TABLE_NAME { + // The table has already been added + return Ok(table_id); + } + + used_ids.push(table_id); + } + } + + used_ids.sort_unstable(); + + // Assign a free id to the table + let mut table_id = 1; + loop { + if used_ids.binary_search(&table_id).is_err() { + break; + } + + table_id += 1; + + if table_id >= 256 { + return Err(Error::NoFreeRoutingTableId); + } + } + + let mut file = fs::OpenOptions::new() + .read(true) + .append(true) + .open(RT_TABLES_PATH) + .map_err(Error::ExclusionsRoutingTableSetup)?; + + if let Ok(_) = file.seek(io::SeekFrom::End(-1)) { + // Append newline if necessary + let mut buffer = [0u8]; + let _ = file.read_exact(&mut buffer); + if buffer[0] != b'\n' { + writeln!(file).map_err(Error::ExclusionsRoutingTableSetup)?; + } + } + + writeln!(file, "{} {}", table_id, ROUTING_TABLE_NAME) + .map_err(Error::ExclusionsRoutingTableSetup)?; + Ok(table_id) + } + + /// Route PID-associated packets through the physical interface. + async fn enable_exclusions_routes(&mut self) -> Result<()> { + // TODO: IPv6 + + let table_id_str = &self.split_table_id.to_string(); + + // Create the rule if it does not exist + let mut cmd = Command::new("ip"); + cmd.args(&["-4", "rule", "list", "table", table_id_str]); + log::trace!("running cmd - {:?}", &cmd); + let out = cmd.output().map_err(Error::ExecFailed)?; + + let missing_rule = + !out.status.success() || String::from_utf8_lossy(&out.stdout).trim().is_empty(); + if missing_rule { + exec_ip(&[ + "-4", + "rule", + "add", + "from", + "all", + "fwmark", + &split_tunnel::MARK.to_string(), + "lookup", + table_id_str, + ])?; + } + + // Add default route for the exclusions table + let zero_network = + ipnetwork::IpNetwork::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0).unwrap(); + let mut required_routes = HashSet::new(); + required_routes.insert( + RequiredRoute::new(zero_network, NetNode::DefaultNode).table(self.split_table_id as u8), + ); + self.add_required_routes(required_routes).await + } + + /// Stop routing PID-associated packets through the physical interface. + async fn disable_exclusions_routes(&self) { + // TODO: IPv6 + + if let Err(e) = exec_ip(&[ + "-4", + "rule", + "del", + "from", + "all", + "fwmark", + &split_tunnel::MARK.to_string(), + "lookup", + &self.split_table_id.to_string(), + ]) { + log::warn!("Failed to delete routing policy: {}", e); + } + } + + /// Route DNS requests through the tunnel interface. + #[cfg(target_os = "linux")] + async fn route_exclusions_dns( + &mut self, + tunnel_alias: &str, + dns_servers: &[IpAddr], + ) -> Result<()> { + let mut dns_routes = HashSet::new(); + + for server in dns_servers { + dns_routes.insert( + RequiredRoute::new( + IpNetwork::from(*server), + Node::device(tunnel_alias.to_string()), + ) + .table(self.split_table_id as u8), + ); + } + + self.add_required_routes(dns_routes).await + } + async fn add_required_default_routes( &mut self, required_default_routes: HashSet<RequiredDefaultRoute>, @@ -477,11 +656,17 @@ impl RouteManagerImplInner { } RouteManagerCommand::AddRoutes(routes, result_rx) => { log::debug!("Adding routes: {:?}", routes); - if let Err(error) = self.add_required_routes(routes.clone()).await { - let _ = result_rx.send(Err(error)); - } else { - let _ = result_rx.send(Ok(())); - } + let _ = result_rx.send(self.add_required_routes(routes.clone()).await); + } + RouteManagerCommand::EnableExclusionsRoutes(result_rx) => { + let _ = result_rx.send(self.enable_exclusions_routes().await); + } + RouteManagerCommand::DisableExclusionsRoutes => { + self.disable_exclusions_routes().await; + } + RouteManagerCommand::RouteExclusionsDns(tunnel_alias, dns_servers, result_rx) => { + let _ = + result_rx.send(self.route_exclusions_dns(&tunnel_alias, &dns_servers).await); } RouteManagerCommand::ClearRoutes => { log::debug!("Clearing routes"); @@ -760,6 +945,20 @@ fn ip_to_bytes(addr: IpAddr) -> Vec<u8> { } } +fn exec_ip(args: &[&str]) -> Result<()> { + let mut cmd = Command::new("ip"); + cmd.args(args); + + log::trace!("running cmd - {:?}", &cmd); + + let status = cmd.status().map_err(Error::ExecFailed)?; + if status.success() { + Ok(()) + } else { + Err(Error::IpFailed) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/talpid-core/src/routing/unix.rs b/talpid-core/src/routing/unix.rs index 3f50ef3db5..e7193facb1 100644 --- a/talpid-core/src/routing/unix.rs +++ b/talpid-core/src/routing/unix.rs @@ -12,6 +12,9 @@ use futures01::{ use std::{collections::HashSet, sync::mpsc::sync_channel}; use talpid_types::ErrorExt; +#[cfg(target_os = "linux")] +use std::net::IpAddr; + #[cfg(target_os = "macos")] #[path = "macos.rs"] mod imp; @@ -51,6 +54,16 @@ pub enum RouteManagerCommand { ), ClearRoutes, Shutdown(oneshot::Sender<()>), + #[cfg(target_os = "linux")] + EnableExclusionsRoutes(oneshot::Sender<Result<(), PlatformError>>), + #[cfg(target_os = "linux")] + DisableExclusionsRoutes, + #[cfg(target_os = "linux")] + RouteExclusionsDns( + String, + Vec<IpAddr>, + oneshot::Sender<Result<(), PlatformError>>, + ), } /// RouteManager applies a set of routes to the route table. @@ -147,6 +160,78 @@ impl RouteManager { Err(Error::RouteManagerDown) } } + + /// Route PID-associated packets through the physical interface. + #[cfg(target_os = "linux")] + pub fn enable_exclusions_routes(&self) -> Result<(), Error> { + if let Some(tx) = &self.manage_tx { + let (result_tx, result_rx) = oneshot::channel(); + if tx + .unbounded_send(RouteManagerCommand::EnableExclusionsRoutes(result_tx)) + .is_err() + { + return Err(Error::RouteManagerDown); + } + + match result_rx.wait() { + Ok(result) => result.map_err(Error::PlatformError), + Err(error) => { + log::trace!("{}", error.display_chain_with_msg("channel is closed")); + Ok(()) + } + } + } else { + Err(Error::RouteManagerDown) + } + } + + /// Stop routing PID-associated packets through the physical interface. + #[cfg(target_os = "linux")] + pub fn disable_exclusions_routes(&self) -> Result<(), Error> { + if let Some(tx) = &self.manage_tx { + if tx + .unbounded_send(RouteManagerCommand::DisableExclusionsRoutes) + .is_err() + { + return Err(Error::RouteManagerDown); + } + Ok(()) + } else { + Err(Error::RouteManagerDown) + } + } + + /// Route DNS requests through the tunnel interface. + #[cfg(target_os = "linux")] + pub fn route_exclusions_dns( + &mut self, + tunnel_alias: &str, + dns_servers: &[IpAddr], + ) -> Result<(), Error> { + if let Some(tx) = &self.manage_tx { + let (result_tx, result_rx) = oneshot::channel(); + if tx + .unbounded_send(RouteManagerCommand::RouteExclusionsDns( + tunnel_alias.to_string(), + dns_servers.to_vec(), + result_tx, + )) + .is_err() + { + return Err(Error::RouteManagerDown); + } + + match result_rx.wait() { + Ok(result) => result.map_err(Error::PlatformError), + Err(error) => { + log::trace!("{}", error.display_chain_with_msg("channel is closed")); + Ok(()) + } + } + } else { + Err(Error::RouteManagerDown) + } + } } impl Drop for RouteManager { diff --git a/talpid-core/src/split_tunnel.rs b/talpid-core/src/split_tunnel.rs new file mode 100644 index 0000000000..d9054ad4d8 --- /dev/null +++ b/talpid-core/src/split_tunnel.rs @@ -0,0 +1,162 @@ +#![cfg(target_os = "linux")] +use std::{ + fs, + io::{self, BufRead, BufReader, BufWriter, Write}, + path::Path, +}; +use talpid_types::SPLIT_TUNNEL_CGROUP_NAME; + +const NETCLS_DIR: &str = "/sys/fs/cgroup/net_cls/"; + +/// Identifies packets coming from the cgroup. +/// This should be an arbitrary but unique integer. +pub const NETCLS_CLASSID: u32 = 0x4d9f41; +/// Value used to mark packets and associated connections. +/// This should be an arbitrary but unique integer. +pub const MARK: i32 = 0xf41; + +/// Errors related to split tunneling. +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +pub enum Error { + /// Unable to create cgroup. + #[error(display = "Unable to initialize net_cls cgroup instance")] + InitNetClsCGroup(#[error(source)] nix::Error), + + /// Unable to create cgroup. + #[error(display = "Unable to create cgroup for excluded processes")] + CreateCGroup(#[error(source)] io::Error), + + /// Unable to set class ID for cgroup. + #[error(display = "Unable to set cgroup class ID")] + SetCGroupClassId(#[error(source)] io::Error), + + /// Unable to add PID to cgroup.procs. + #[error(display = "Unable to add PID to cgroup.procs")] + AddCGroupPid(#[error(source)] io::Error), + + /// Unable to remove PID to cgroup.procs. + #[error(display = "Unable to remove PID from cgroup")] + RemoveCGroupPid(#[error(source)] io::Error), + + /// Unable to read cgroup.procs. + #[error(display = "Unable to obtain PIDs from cgroup.procs")] + ListCGroupPids(#[error(source)] io::Error), +} + +/// Manages PIDs to exclude from the tunnel. +pub struct PidManager; + +impl PidManager { + /// Create object to manage split-tunnel PIDs. + pub fn new() -> Result<PidManager, Error> { + Self::create_cgroup()?; + Ok(PidManager {}) + } + + /// Set up cgroup used to track PIDs for split tunneling. + fn create_cgroup() -> Result<(), Error> { + let netcls_dir = Path::new(NETCLS_DIR); + if !netcls_dir.exists() { + fs::create_dir(netcls_dir.clone()).map_err(Error::CreateCGroup)?; + + // https://www.kernel.org/doc/Documentation/cgroup-v1/net_cls.txt + nix::mount::mount( + Some("net_cls"), + netcls_dir, + Some("cgroup"), + nix::mount::MsFlags::empty(), + Some("net_cls"), + ) + .map_err(Error::InitNetClsCGroup)?; + } + + let exclusions_dir = netcls_dir.join(SPLIT_TUNNEL_CGROUP_NAME); + + if !exclusions_dir.exists() { + fs::create_dir(exclusions_dir.clone()).map_err(Error::CreateCGroup)?; + } + + let classid_path = exclusions_dir.join("net_cls.classid"); + fs::write(classid_path, NETCLS_CLASSID.to_string().as_bytes()) + .map_err(Error::SetCGroupClassId) + } + + /// Add a PID to exclude from the tunnel. + pub fn add(&self, pid: i32) -> Result<(), Error> { + self.add_list(&[pid]) + } + + /// Add PIDs to exclude from the tunnel. + pub fn add_list(&self, pids: &[i32]) -> Result<(), Error> { + let exclusions_path = Path::new(NETCLS_DIR) + .join(SPLIT_TUNNEL_CGROUP_NAME) + .join("cgroup.procs"); + + let file = fs::OpenOptions::new() + .write(true) + .create(true) + .open(exclusions_path) + .map_err(Error::AddCGroupPid)?; + + let mut writer = BufWriter::new(file); + + for pid in pids { + writer + .write_all(pid.to_string().as_bytes()) + .map_err(Error::AddCGroupPid)?; + } + + Ok(()) + } + + /// Remove a PID from processes to exclude from the tunnel. + pub fn remove(&self, pid: i32) -> Result<(), Error> { + // FIXME: We remove PIDs from our cgroup here by adding + // them to the parent cgroup. This seems wrong. + let exclusions_path = Path::new(NETCLS_DIR).join("cgroup.procs"); + + let mut file = fs::OpenOptions::new() + .write(true) + .create(true) + .open(exclusions_path) + .map_err(Error::RemoveCGroupPid)?; + + file.write_all(pid.to_string().as_bytes()) + .map_err(Error::RemoveCGroupPid) + } + + /// Return a list of PIDs that are excluded from the tunnel. + pub fn list(&self) -> Result<Vec<i32>, Error> { + // TODO: manage child PIDs somehow? + + let exclusions_path = Path::new(NETCLS_DIR) + .join(SPLIT_TUNNEL_CGROUP_NAME) + .join("cgroup.procs"); + + let file = fs::File::open(exclusions_path).map_err(Error::ListCGroupPids)?; + + let result: Result<Vec<i32>, io::Error> = BufReader::new(file) + .lines() + .map(|line| { + line.and_then(|v| { + v.parse() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) + }) + }) + .collect(); + result.map_err(Error::ListCGroupPids) + } + + /// Clear list of PIDs to exclude from the tunnel. + pub fn clear(&self) -> Result<(), Error> { + // TODO: reuse file handle + let pids = self.list()?; + + for pid in pids { + self.remove(pid)?; + } + + Ok(()) + } +} diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index deb7e265a4..6ba26007b7 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -13,7 +13,7 @@ use futures01::{ use talpid_types::{ net::{Endpoint, TunnelParameters}, tunnel::ErrorStateCause, - ErrorExt, + BoxedError, ErrorExt, }; pub struct ConnectedStateBootstrap { @@ -69,10 +69,7 @@ impl ConnectedState { } } - fn set_dns( - &self, - shared_values: &mut SharedTunnelStateValues, - ) -> Result<(), crate::dns::Error> { + fn set_dns(&self, shared_values: &mut SharedTunnelStateValues) -> Result<(), BoxedError> { let mut dns_ips = vec![self.metadata.ipv4_gateway.into()]; if let Some(ipv6_gateway) = self.metadata.ipv6_gateway { dns_ips.push(ipv6_gateway.into()); @@ -81,6 +78,15 @@ impl ConnectedState { shared_values .dns_monitor .set(&self.metadata.interface, &dns_ips) + .map_err(BoxedError::new)?; + + #[cfg(target_os = "linux")] + shared_values + .route_manager + .route_exclusions_dns(&self.metadata.interface, &dns_ips) + .map_err(BoxedError::new)?; + + Ok(()) } fn reset_dns(shared_values: &mut SharedTunnelStateValues) { @@ -235,10 +241,7 @@ impl TunnelState for ConnectedState { ), ) } else if let Err(error) = connected_state.set_dns(shared_values) { - log::error!( - "{}", - error.display_chain_with_msg("Failed to set system DNS settings") - ); + log::error!("{}", error.display_chain_with_msg("Failed to set DNS")); DisconnectingState::enter( shared_values, ( diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index b9859a12dc..13a7104aa1 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -358,6 +358,14 @@ impl TunnelState for ConnectingState { ); ErrorState::enter(shared_values, ErrorStateCause::StartTunnelError) } else { + #[cfg(target_os = "linux")] + if let Err(error) = shared_values.route_manager.enable_exclusions_routes() { + error!( + "{}", + error.display_chain_with_msg("Failed to set up split tunneling") + ); + } + #[cfg(target_os = "android")] { if retry_attempt > 0 && retry_attempt % MAX_ATTEMPTS_WITH_SAME_TUN == 0 { diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index 0ad48366d9..0b1005a355 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -39,6 +39,13 @@ impl TunnelState for DisconnectedState { shared_values: &mut SharedTunnelStateValues, _: Self::Bootstrap, ) -> (TunnelStateWrapper, TunnelStateTransition) { + #[cfg(target_os = "linux")] + if let Err(error) = shared_values.route_manager.disable_exclusions_routes() { + log::error!( + "{}", + error.display_chain_with_msg("Failed to disable exclusions routes") + ); + } Self::set_firewall_policy(shared_values); #[cfg(target_os = "android")] shared_values.tun_provider.close_tun(); diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 2ffce2bec9..b0222ade67 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -50,6 +50,11 @@ pub enum Error { #[error(display = "Unable to spawn offline state monitor")] OfflineMonitorError(#[error(source)] crate::offline::Error), + /// Unable to set up split tunneling + #[cfg(target_os = "linux")] + #[error(display = "Failed to initialize split tunneling")] + InitSplitTunneling(#[error(source)] crate::split_tunnel::Error), + /// Failed to initialize the system firewall integration. #[error(display = "Failed to initialize the system firewall integration")] InitFirewallError(#[error(source)] crate::firewall::Error), @@ -235,6 +240,7 @@ impl TunnelStateMachine { allow_lan: None, } }; + let firewall = Firewall::new(args).map_err(Error::InitFirewallError)?; let dns_monitor = DnsMonitor::new(cache_dir).map_err(Error::InitDnsMonitorError)?; let route_manager = diff --git a/talpid-types/src/lib.rs b/talpid-types/src/lib.rs index d5b71fd46d..2ee772a756 100644 --- a/talpid-types/src/lib.rs +++ b/talpid-types/src/lib.rs @@ -7,6 +7,9 @@ pub mod android; pub mod net; pub mod tunnel; +#[cfg(target_os = "linux")] +pub const SPLIT_TUNNEL_CGROUP_NAME: &str = "mullvad-exclusions"; + /// Used to generate string representations of error chains. pub trait ErrorExt { |
