summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml1
-rwxr-xr-xbuild.sh1
-rw-r--r--dist-assets/linux/after-install.sh2
-rw-r--r--gui/tasks/distribution.js2
-rw-r--r--mullvad-cli/src/cmds/mod.rs7
-rw-r--r--mullvad-cli/src/cmds/split_tunnel.rs70
-rw-r--r--mullvad-daemon/src/lib.rs62
-rw-r--r--mullvad-daemon/src/management_interface.rs76
-rw-r--r--mullvad-exclude/Cargo.toml13
-rw-r--r--mullvad-exclude/src/main.rs95
-rw-r--r--mullvad-ipc-client/src/lib.rs17
-rw-r--r--talpid-core/src/firewall/linux.rs155
-rw-r--r--talpid-core/src/lib.rs3
-rw-r--r--talpid-core/src/routing/linux.rs215
-rw-r--r--talpid-core/src/routing/unix.rs85
-rw-r--r--talpid-core/src/split_tunnel.rs162
-rw-r--r--talpid-core/src/tunnel_state_machine/connected_state.rs21
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs8
-rw-r--r--talpid-core/src/tunnel_state_machine/disconnected_state.rs7
-rw-r--r--talpid-core/src/tunnel_state_machine/mod.rs6
-rw-r--r--talpid-types/src/lib.rs3
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",
diff --git a/build.sh b/build.sh
index de58aff5ed..168f01502d 100755
--- a/build.sh
+++ b/build.sh
@@ -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 {