From 495ba5b0036cf3c2033edf81105be8d57e88608a Mon Sep 17 00:00:00 2001 From: Sebastian Rugina Date: Sat, 31 Jan 2026 07:41:47 +0000 Subject: [PATCH] feat: Reduce kill switch auth requirements To mitigate CVE-2025-9615, NetworkManager from 1.57.1 (at least) will remove the modify_system build option (a new option is available for backwards compatibility but is discouraged). Debian, NixOS, & Tumbleweed already do not rely on this, but Arch, Fedora, and Alpine do (for now). See https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/merge_requests/2324 If a non-permanent kill switch were enabled ("Standard" and/or IPv6) without modify_system, a polkit prompt would appear for every manual (dis)connection of the VPN (except within auth timeout of previous prompt). This is because editing system connections uses the `org.freedesktop.NetworkManager.settings.modify.system` polkit action, which without modify_system defaults to `auth_admin_keep`. To fix this, a user connection is sufficient as on boot it is acceptable to wait for login like the VPN connection (also a user connection). When the user's regular connection is also a user connection, there will be no polkit prompt to manually (dis)connect the VPN. Only the permanent ("Advanced") kill switch needs to be a system connection so that there is no leak before user login on boot, and since it's permanent (written to disk) the polkit prompt is only required when enabling/disabling the permanent kill switch setting and not on every manual (dis)connection of the VPN. --- .../killswitch/default/killswitch_connection.py | 7 +++++++ .../killswitch/default/killswitch_connection_handler.py | 9 ++++++--- .../killswitch/wireguard/killswitch_connection.py | 7 +++++++ .../wireguard/killswitch_connection_handler.py | 6 ++++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection.py b/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection.py index 57ad6fa..49a21aa 100644 --- a/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection.py +++ b/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection.py @@ -24,6 +24,7 @@ from dataclasses import dataclass, field import uuid +from getpass import getuser import gi # pylint: disable=C0411 gi.require_version("NM", "1.0") @@ -37,6 +38,7 @@ class KillSwitchGeneralConfig: # pylint: disable=missing-class-docstring human_readable_id: str interface_name: str + permanent: bool @dataclass @@ -86,6 +88,11 @@ def _create_connection_profile(self): s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4())) s_con.set_property(NM.SETTING_CONNECTION_TYPE, NM.SETTING_DUMMY_SETTING_NAME) + # Only permanent kill switch needs to be a system connection; + # weaker kill switches can be user connections to avoid polkit auth. + if not self._general_settings.permanent: + s_con.add_permission(NM.SETTING_USER_SETTING_NAME, getuser(), None) + s_dummy = NM.SettingDummy.new() s_ipv4 = self._generate_ipv4_settings() diff --git a/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection_handler.py b/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection_handler.py index 831d962..425adf5 100644 --- a/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection_handler.py +++ b/proton/vpn/backend/networkmanager/killswitch/default/killswitch_connection_handler.py @@ -144,7 +144,8 @@ async def add_full_killswitch_connection(self, permanent: bool): interface_name = _get_interface_name(permanent) general_config = KillSwitchGeneralConfig( human_readable_id=connection_id, - interface_name=interface_name + interface_name=interface_name, + permanent=permanent ) kill_switch = KillSwitchConnection( @@ -174,7 +175,8 @@ async def add_routed_killswitch_connection(self, server_ip: str, permanent: bool general_config = KillSwitchGeneralConfig( human_readable_id=_get_connection_id(self._connection_prefix, permanent, routed=True), - interface_name=_get_interface_name(permanent, routed=True) + interface_name=_get_interface_name(permanent, routed=True), + permanent=permanent ) kill_switch = KillSwitchConnection( general_config, @@ -204,7 +206,8 @@ async def add_ipv6_leak_protection(self): interface_name = _get_interface_name(permanent=False, ipv6=True) general_config = KillSwitchGeneralConfig( human_readable_id=connection_id, - interface_name=interface_name + interface_name=interface_name, + permanent=False ) kill_switch = KillSwitchConnection( diff --git a/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection.py b/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection.py index 4813fbc..59cf700 100644 --- a/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection.py +++ b/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection.py @@ -24,6 +24,7 @@ from dataclasses import dataclass, field import uuid +from getpass import getuser import gi gi.require_version("NM", "1.0") @@ -37,6 +38,7 @@ class KillSwitchGeneralConfig: # pylint: disable=missing-class-docstring human_readable_id: str interface_name: str + permanent: bool @dataclass @@ -86,6 +88,11 @@ def _create_connection_profile(self): s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4())) s_con.set_property(NM.SETTING_CONNECTION_TYPE, NM.SETTING_DUMMY_SETTING_NAME) + # Only permanent kill switch needs to be a system connection; + # weaker kill switches can be user connections to avoid polkit auth. + if not self._general_settings.permanent: + s_con.add_permission(NM.SETTING_USER_SETTING_NAME, getuser(), None) + s_dummy = NM.SettingDummy.new() s_ipv4 = self._generate_ipv4_settings() diff --git a/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection_handler.py b/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection_handler.py index 3dd755d..5f72b1b 100644 --- a/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection_handler.py +++ b/proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection_handler.py @@ -115,7 +115,8 @@ async def add_kill_switch_connection(self, permanent: bool): general_config = self._config or KillSwitchGeneralConfig( human_readable_id=_get_connection_id(permanent), - interface_name=_get_interface_name(permanent) + interface_name=_get_interface_name(permanent), + permanent=permanent ) connection = self.nm_client.get_active_connection( @@ -246,7 +247,8 @@ async def add_ipv6_leak_protection(self): interface_name = _get_interface_name(permanent=False, ipv6=True) general_config = KillSwitchGeneralConfig( human_readable_id=connection_id, - interface_name=interface_name + interface_name=interface_name, + permanent=False ) kill_switch = KillSwitchConnection(