From ed01e2f7df0ebfc08651697ae4428d793db85f08 Mon Sep 17 00:00:00 2001 From: dkwo Date: Mon, 7 Jul 2025 11:34:04 -0400 Subject: [PATCH] mullvad: add patch for cgroups v1 --- srcpkgs/mullvadvpn/files/mullvad/run | 1 - ...87b3255e2748693d52728dadfdd69c6cb761.patch | 72 ++++++++ ...c3b8f16d1427c197b2951690bd1f4682362b.patch | 167 ++++++++++++++++++ ...5961780f4d3831fc4a1ddf72b433e3fb4740.patch | 143 +++++++++++++++ 4 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 srcpkgs/mullvadvpn/patches/267087b3255e2748693d52728dadfdd69c6cb761.patch create mode 100644 srcpkgs/mullvadvpn/patches/69ebc3b8f16d1427c197b2951690bd1f4682362b.patch create mode 100644 srcpkgs/mullvadvpn/patches/a33f5961780f4d3831fc4a1ddf72b433e3fb4740.patch diff --git a/srcpkgs/mullvadvpn/files/mullvad/run b/srcpkgs/mullvadvpn/files/mullvad/run index 7246e20c905..49ffecc7735 100644 --- a/srcpkgs/mullvadvpn/files/mullvad/run +++ b/srcpkgs/mullvadvpn/files/mullvad/run @@ -5,7 +5,6 @@ export MULLVAD_LOG_DIR=/var/log/mullvad-vpn export MULLVAD_SETTINGS_DIR=/etc/mullvad-vpn export MULLVAD_CACHE_DIR=/var/cache/mullvad-vpn 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 # needs cap_dac_override to write /etc/resolv.conf{,.mullvadbackup} diff --git a/srcpkgs/mullvadvpn/patches/267087b3255e2748693d52728dadfdd69c6cb761.patch b/srcpkgs/mullvadvpn/patches/267087b3255e2748693d52728dadfdd69c6cb761.patch new file mode 100644 index 00000000000..ea6d05c5ed3 --- /dev/null +++ b/srcpkgs/mullvadvpn/patches/267087b3255e2748693d52728dadfdd69c6cb761.patch @@ -0,0 +1,72 @@ +From 267087b3255e2748693d52728dadfdd69c6cb761 Mon Sep 17 00:00:00 2001 +From: Joakim Hulthe +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 { + 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 { diff --git a/srcpkgs/mullvadvpn/patches/69ebc3b8f16d1427c197b2951690bd1f4682362b.patch b/srcpkgs/mullvadvpn/patches/69ebc3b8f16d1427c197b2951690bd1f4682362b.patch new file mode 100644 index 00000000000..a6da00f95a8 --- /dev/null +++ b/srcpkgs/mullvadvpn/patches/69ebc3b8f16d1427c197b2951690bd1f4682362b.patch @@ -0,0 +1,167 @@ +From 69ebc3b8f16d1427c197b2951690bd1f4682362b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?David=20L=C3=B6nnhager?= +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 { +- 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 { ++ 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, 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 { ++ fn open_parent_cgroup_handle(net_cls_path: &Path) -> io::Result { + fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(false) +- .open(self.net_cls_path.join("cgroup.procs")) ++ .open(net_cls_path.join("cgroup.procs")) + } + } diff --git a/srcpkgs/mullvadvpn/patches/a33f5961780f4d3831fc4a1ddf72b433e3fb4740.patch b/srcpkgs/mullvadvpn/patches/a33f5961780f4d3831fc4a1ddf72b433e3fb4740.patch new file mode 100644 index 00000000000..a481a7490bd --- /dev/null +++ b/srcpkgs/mullvadvpn/patches/a33f5961780f4d3831fc4a1ddf72b433e3fb4740.patch @@ -0,0 +1,143 @@ +From a33f5961780f4d3831fc4a1ddf72b433e3fb4740 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?David=20L=C3=B6nnhager?= +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 { ++ const isEnabled = await this.callEmpty(this.client.splitTunnelIsEnabled); ++ return isEnabled.getValue(); ++ } ++ + public async needFullDiskPermissions(): Promise { + const needFullDiskPermissions = await this.callEmpty( + 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), + /// Request list of processes excluded from the tunnel + #[cfg(target_os = "linux")] + GetSplitTunnelProcesses(ResponseTx, 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) { ++ 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, 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 { ++ #[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::OpenOptions::new() + .write(true)