mullvad: add patch for cgroups v1

This commit is contained in:
dkwo 2025-07-07 11:34:04 -04:00
parent b89a4eb48e
commit ed01e2f7df
4 changed files with 382 additions and 1 deletions

View file

@ -5,7 +5,6 @@ export MULLVAD_LOG_DIR=/var/log/mullvad-vpn
export MULLVAD_SETTINGS_DIR=/etc/mullvad-vpn export MULLVAD_SETTINGS_DIR=/etc/mullvad-vpn
export MULLVAD_CACHE_DIR=/var/cache/mullvad-vpn export MULLVAD_CACHE_DIR=/var/cache/mullvad-vpn
export MULLVAD_RPC_SOCKET_PATH=/run/mullvad-vpn/mullvad export MULLVAD_RPC_SOCKET_PATH=/run/mullvad-vpn/mullvad
export TALPID_NET_CLS_MOUNT_DIR=/run/mullvad-vpn/cgroup
export MULLVAD_MANAGEMENT_SOCKET_GROUP=_mullvad export MULLVAD_MANAGEMENT_SOCKET_GROUP=_mullvad
# needs cap_dac_override to write /etc/resolv.conf{,.mullvadbackup} # needs cap_dac_override to write /etc/resolv.conf{,.mullvadbackup}

View file

@ -0,0 +1,72 @@
From 267087b3255e2748693d52728dadfdd69c6cb761 Mon Sep 17 00:00:00 2001
From: Joakim Hulthe <joakim.hulthe@mullvad.net>
Date: Thu, 19 Jun 2025 15:22:53 +0200
Subject: [PATCH] Do not add split-tunneling fw rules if no net_cls
---
talpid-core/src/firewall/linux.rs | 31 +++++++++++++++++++++++++++----
1 file changed, 27 insertions(+), 4 deletions(-)
diff --git a/talpid-core/src/firewall/linux.rs b/talpid-core/src/firewall/linux.rs
index 26d3a5cb5a68..b72b4c12d6ea 100644
--- a/talpid-core/src/firewall/linux.rs
+++ b/talpid-core/src/firewall/linux.rs
@@ -12,9 +12,12 @@ use std::{
net::{IpAddr, Ipv4Addr},
sync::LazyLock,
};
-use talpid_types::net::{
- AllowedEndpoint, AllowedTunnelTraffic, Endpoint, TransportProtocol, ALLOWED_LAN_MULTICAST_NETS,
- ALLOWED_LAN_NETS,
+use talpid_types::{
+ cgroup::find_net_cls_mount,
+ net::{
+ AllowedEndpoint, AllowedTunnelTraffic, Endpoint, TransportProtocol,
+ ALLOWED_LAN_MULTICAST_NETS, ALLOWED_LAN_NETS,
+ },
};
/// Priority for rules that tag split tunneling packets. Equals NF_IP_PRI_MANGLE.
@@ -52,6 +55,10 @@ pub enum Error {
/// Unable to translate network interface name into index.
#[error("Unable to translate network interface name \"{0}\" into index")]
LookupIfaceIndexError(String, #[source] crate::linux::IfaceIndexLookupError),
+
+ /// Failed to check if the net_cls mount exists.
+ #[error("An error occurred when checking for net_cls")]
+ FindNetClsMount(#[source] io::Error),
}
/// TODO(linus): This crate is not supposed to be Mullvad-aware. So at some point this should be
@@ -303,7 +310,19 @@ impl<'a> PolicyBatch<'a> {
/// policy.
pub fn finalize(mut self, policy: &FirewallPolicy, fwmark: u32) -> Result<FinalizedBatch> {
self.add_loopback_rules()?;
- self.add_split_tunneling_rules(policy, fwmark)?;
+
+ // if cgroups v1 doesn't exist, split tunneling won't work.
+ // checking if the `net_cls` mount exists is a cheeky way of checking this.
+ if find_net_cls_mount()
+ .map_err(Error::FindNetClsMount)?
+ .is_some()
+ {
+ self.add_split_tunneling_rules(policy, fwmark)?;
+ } else {
+ // skipping add_split_tunneling_rules as it won't cause traffic to leak
+ log::warn!("net_cls mount not found, skipping add_split_tunneling_rules");
+ }
+
self.add_dhcp_client_rules();
self.add_ndp_rules();
self.add_policy_specific_rules(policy, fwmark)?;
@@ -311,6 +330,10 @@ impl<'a> PolicyBatch<'a> {
Ok(self.batch.finalize())
}
+ /// Allow split-tunneled traffic outside the tunnel.
+ ///
+ /// This is acheived by setting `fwmark` on connections initated by processes in the cgroup
+ /// defined by [split_tunnel::NET_CLS_CLASSID].
fn add_split_tunneling_rules(&mut self, policy: &FirewallPolicy, fwmark: u32) -> Result<()> {
// Send select DNS requests in the tunnel
if let FirewallPolicy::Connected {

View file

@ -0,0 +1,167 @@
From 69ebc3b8f16d1427c197b2951690bd1f4682362b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20L=C3=B6nnhager?= <david.l@mullvad.net>
Date: Mon, 9 Jun 2025 16:53:47 +0200
Subject: [PATCH] Make daemon start without split tunneling
---
mullvad-daemon/src/lib.rs | 2 +-
talpid-core/src/split_tunnel/linux.rs | 64 +++++++++++++++++++--------
2 files changed, 47 insertions(+), 19 deletions(-)
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index 79fbe1bb7cf0..edb770ec5809 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -945,7 +945,7 @@ impl Daemon {
},
target_state,
#[cfg(target_os = "linux")]
- exclude_pids: split_tunnel::PidManager::new().map_err(Error::InitSplitTunneling)?,
+ exclude_pids: split_tunnel::PidManager::default(),
rx: internal_event_rx,
tx: internal_event_tx,
reconnection_job: None,
diff --git a/talpid-core/src/split_tunnel/linux.rs b/talpid-core/src/split_tunnel/linux.rs
index e96a0293defc..dfbf8fc479a4 100644
--- a/talpid-core/src/split_tunnel/linux.rs
+++ b/talpid-core/src/split_tunnel/linux.rs
@@ -1,7 +1,7 @@
use std::{
env, fs,
io::{self, BufRead, BufReader, Write},
- path::PathBuf,
+ path::{Path, PathBuf},
};
use talpid_types::cgroup::{find_net_cls_mount, SPLIT_TUNNEL_CGROUP_NAME};
@@ -26,6 +26,10 @@ pub enum Error {
#[error("Unable to create cgroup for excluded processes")]
CreateCGroup(#[source] io::Error),
+ /// Split tunneling is unavailable
+ #[error("Failed to set up split tunneling")]
+ Unavailable,
+
/// Unable to set class ID for cgroup.
#[error("Unable to set cgroup class ID")]
SetCGroupClassId(#[source] io::Error),
@@ -49,20 +53,36 @@ pub enum Error {
/// Manages PIDs in the Linux Cgroup excluded from the VPN tunnel.
pub struct PidManager {
- net_cls_path: PathBuf,
+ inner: Inner,
}
-impl PidManager {
+enum Inner {
+ Ok { net_cls_path: PathBuf },
+ Failed { err: Error },
+}
+
+impl Default for PidManager {
/// Creates a new PID Cgroup manager.
///
/// Finds the corresponding Cgroup to use. Will mount a `net_cls` filesystem
/// if none exists.
- pub fn new() -> Result<PidManager, Error> {
- let manager = PidManager {
- net_cls_path: Self::create_cgroup()?,
+ fn default() -> Self {
+ let inner = match Self::new_inner() {
+ Ok(net_cls_path) => Inner::Ok { net_cls_path },
+ Err(err) => {
+ log::error!("{}", err.display_chain_with_msg("Failed to enable split tunneling"));
+ Inner::Failed { err }
+ }
};
- manager.setup_exclusion_group()?;
- Ok(manager)
+ PidManager { inner }
+ }
+}
+
+impl PidManager {
+ fn new_inner() -> Result<PathBuf, Error> {
+ let net_cls_path = Self::create_cgroup()?;
+ Self::setup_exclusion_group(&net_cls_path)?;
+ Ok(net_cls_path)
}
/// Set up cgroup used to track PIDs for split tunneling.
@@ -92,8 +112,8 @@ impl PidManager {
Ok(net_cls_dir)
}
- fn setup_exclusion_group(&self) -> Result<(), Error> {
- let exclusions_dir = self.net_cls_path.join(SPLIT_TUNNEL_CGROUP_NAME);
+ fn setup_exclusion_group(net_cls_path: &Path) -> Result<(), Error> {
+ let exclusions_dir = net_cls_path.join(SPLIT_TUNNEL_CGROUP_NAME);
if !exclusions_dir.exists() {
fs::create_dir(exclusions_dir.clone()).map_err(Error::CreateCGroup)?;
}
@@ -103,10 +123,20 @@ impl PidManager {
.map_err(Error::SetCGroupClassId)
}
+ fn get_net_cls_path(&self) -> Result<&Path, Error> {
+ match &self.inner {
+ Inner::Ok { net_cls_path } => Ok(net_cls_path),
+ Inner::Failed { err } => {
+ log::error!("Failed to get netcls path: {err}");
+ Err(Error::Unavailable)
+ }
+ }
+ }
+
/// Add a PID to the Cgroup to have it excluded from the tunnel.
pub fn add(&self, pid: i32) -> Result<(), Error> {
let exclusions_path = self
- .net_cls_path
+ .get_net_cls_path()?
.join(SPLIT_TUNNEL_CGROUP_NAME)
.join("cgroup.procs");
@@ -125,8 +155,7 @@ impl PidManager {
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 mut file = self
- .open_parent_cgroup_handle()
+ let mut file = Self::open_parent_cgroup_handle(self.get_net_cls_path()?)
.map_err(Error::RemoveCGroupPid)?;
file.write_all(pid.to_string().as_bytes())
@@ -136,7 +165,7 @@ impl PidManager {
/// Return a list of all PIDs currently in the Cgroup excluded from the tunnel.
pub fn list(&self) -> Result<Vec<i32>, Error> {
let exclusions_path = self
- .net_cls_path
+ .get_net_cls_path()?
.join(SPLIT_TUNNEL_CGROUP_NAME)
.join("cgroup.procs");
@@ -158,8 +187,7 @@ impl PidManager {
pub fn clear(&self) -> Result<(), Error> {
let pids = self.list()?;
- let mut file = self
- .open_parent_cgroup_handle()
+ let mut file = Self::open_parent_cgroup_handle(self.get_net_cls_path()?)
.map_err(Error::RemoveCGroupPid)?;
for pid in pids {
file.write_all(pid.to_string().as_bytes())
@@ -169,11 +197,11 @@ impl PidManager {
Ok(())
}
- fn open_parent_cgroup_handle(&self) -> io::Result<fs::File> {
+ fn open_parent_cgroup_handle(net_cls_path: &Path) -> io::Result<fs::File> {
fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
- .open(self.net_cls_path.join("cgroup.procs"))
+ .open(net_cls_path.join("cgroup.procs"))
}
}

View file

@ -0,0 +1,143 @@
From a33f5961780f4d3831fc4a1ddf72b433e3fb4740 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20L=C3=B6nnhager?= <david.l@mullvad.net>
Date: Tue, 10 Jun 2025 09:36:48 +0200
Subject: [PATCH] Add RPC for checking if split tunneling is available on Linux
---
.../packages/mullvad-vpn/src/main/daemon-rpc.ts | 5 +++++
mullvad-daemon/src/lib.rs | 11 +++++++++++
mullvad-daemon/src/management_interface.rs | 15 +++++++++++++++
.../proto/management_interface.proto | 1 +
talpid-core/src/split_tunnel/linux.rs | 15 +++++++++++++--
5 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
index d0d424418616..206070598c25 100644
--- a/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
+++ b/desktop/packages/mullvad-vpn/src/main/daemon-rpc.ts
@@ -510,6 +510,11 @@ export class DaemonRpc extends GrpcClient {
await this.callBool(this.client.setSplitTunnelState, enabled);
}
+ public async splitTunnelIsEnabled(): Promise<boolean> {
+ const isEnabled = await this.callEmpty<BoolValue>(this.client.splitTunnelIsEnabled);
+ return isEnabled.getValue();
+ }
+
public async needFullDiskPermissions(): Promise<boolean> {
const needFullDiskPermissions = await this.callEmpty<BoolValue>(
this.client.needFullDiskPermissions,
diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs
index edb770ec5809..70db828aa3aa 100644
--- a/mullvad-daemon/src/lib.rs
+++ b/mullvad-daemon/src/lib.rs
@@ -344,6 +344,9 @@ pub enum DaemonCommand {
/// Remove settings and clear the cache
#[cfg(not(target_os = "android"))]
FactoryReset(ResponseTx<(), Error>),
+ /// Return whether split tunneling is available
+ #[cfg(target_os = "linux")]
+ SplitTunnelIsEnabled(oneshot::Sender<bool>),
/// Request list of processes excluded from the tunnel
#[cfg(target_os = "linux")]
GetSplitTunnelProcesses(ResponseTx<Vec<i32>, split_tunnel::Error>),
@@ -1454,6 +1457,8 @@ impl Daemon {
#[cfg(not(target_os = "android"))]
FactoryReset(tx) => self.on_factory_reset(tx).await,
#[cfg(target_os = "linux")]
+ SplitTunnelIsEnabled(tx) => self.on_split_tunnel_is_enabled(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),
@@ -2029,6 +2034,12 @@ impl Daemon {
}));
}
+ #[cfg(target_os = "linux")]
+ fn on_split_tunnel_is_enabled(&mut self, tx: oneshot::Sender<bool>) {
+ let enabled = self.exclude_pids.is_enabled();
+ Self::oneshot_send(tx, enabled, "split_tunnel_is_enabled response");
+ }
+
#[cfg(target_os = "linux")]
fn on_get_split_tunnel_processes(&mut self, tx: ResponseTx<Vec<i32>, split_tunnel::Error>) {
let result = self.exclude_pids.list().inspect_err(|error| {
diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs
index 7beadb3ddd38..5695683fcf15 100644
--- a/mullvad-daemon/src/management_interface.rs
+++ b/mullvad-daemon/src/management_interface.rs
@@ -833,6 +833,21 @@ impl ManagementService for ManagementServiceImpl {
// Split tunneling
//
+ async fn split_tunnel_is_enabled(&self, _: Request<()>) -> ServiceResult<bool> {
+ #[cfg(target_os = "linux")]
+ {
+ log::debug!("split_tunnel_is_enabled");
+ let (tx, rx) = oneshot::channel();
+ self.send_command_to_daemon(DaemonCommand::SplitTunnelIsEnabled(tx))?;
+ Ok(self.wait_for_result(rx).await.map(Response::new)?)
+ }
+ #[cfg(not(target_os = "linux"))]
+ {
+ log::error!("split_tunnel_is_enabled is only available on Linux");
+ Ok(Response::new(false))
+ }
+ }
+
async fn get_split_tunnel_processes(
&self,
_: Request<()>,
diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto
index f3be6708220e..ab9883c1efdd 100644
--- a/mullvad-management-interface/proto/management_interface.proto
+++ b/mullvad-management-interface/proto/management_interface.proto
@@ -97,6 +97,7 @@ service ManagementService {
rpc TestApiAccessMethodById(UUID) returns (google.protobuf.BoolValue) {}
// Split tunneling (Linux)
+ rpc SplitTunnelIsEnabled(google.protobuf.Empty) returns (google.protobuf.BoolValue) {}
rpc GetSplitTunnelProcesses(google.protobuf.Empty) returns (stream google.protobuf.Int32Value) {}
rpc AddSplitTunnelProcess(google.protobuf.Int32Value) returns (google.protobuf.Empty) {}
rpc RemoveSplitTunnelProcess(google.protobuf.Int32Value) returns (google.protobuf.Empty) {}
diff --git a/talpid-core/src/split_tunnel/linux.rs b/talpid-core/src/split_tunnel/linux.rs
index dfbf8fc479a4..1efb1dc60bf0 100644
--- a/talpid-core/src/split_tunnel/linux.rs
+++ b/talpid-core/src/split_tunnel/linux.rs
@@ -3,7 +3,10 @@ use std::{
io::{self, BufRead, BufReader, Write},
path::{Path, PathBuf},
};
-use talpid_types::cgroup::{find_net_cls_mount, SPLIT_TUNNEL_CGROUP_NAME};
+use talpid_types::{
+ cgroup::{find_net_cls_mount, SPLIT_TUNNEL_CGROUP_NAME},
+ ErrorExt,
+};
const DEFAULT_NET_CLS_DIR: &str = "/sys/fs/cgroup/net_cls";
const NET_CLS_DIR_OVERRIDE_ENV_VAR: &str = "TALPID_NET_CLS_MOUNT_DIR";
@@ -70,7 +73,10 @@ impl Default for PidManager {
let inner = match Self::new_inner() {
Ok(net_cls_path) => Inner::Ok { net_cls_path },
Err(err) => {
- log::error!("{}", err.display_chain_with_msg("Failed to enable split tunneling"));
+ log::error!(
+ "{}",
+ err.display_chain_with_msg("Failed to enable split tunneling")
+ );
Inner::Failed { err }
}
};
@@ -197,6 +203,11 @@ impl PidManager {
Ok(())
}
+ /// Return whether it is enabled
+ pub fn is_enabled(&self) -> bool {
+ matches!(self.inner, Inner::Ok { .. })
+ }
+
fn open_parent_cgroup_handle(net_cls_path: &Path) -> io::Result<fs::File> {
fs::OpenOptions::new()
.write(true)