From 42e0edcb938f5013f7347850ae2cab18979e67df Mon Sep 17 00:00:00 2001 From: "Joel M. Pareja" Date: Tue, 5 May 2026 05:14:54 +0800 Subject: [PATCH] fix: make WireGuard killswitch bridge-aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_physical_devices()` enumerated only ETHERNET/WIFI devices and did not exclude slaves of a bridge controller. On hosts where the primary uplink is a NetworkManager-managed Linux bridge (e.g. `br0` with `enp12s0` enslaved), the slave NIC is ACTIVATED but its connection profile has no IPv4 setting — its IP config lives on the bridge. The killswitch then called `config.get_num_routes()` on a `None` IPv4 setting and raised `AttributeError` on connect. Changes: - `get_physical_devices()` now also accepts `NM.DeviceType.BRIDGE` and excludes any device whose active connection has a controller (slave of a bond/bridge/team). Uses `get_controller()` with a fallback to the deprecated `get_master()` for older libnm. - `_add_ipv4_route` / `_remove_ipv4_routes` raise `GatewayNotFoundError` with a clear message when `get_setting_ip4_config()` returns `None`, instead of crashing with `AttributeError`. --- .../killswitch/wireguard/nmclient.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py b/proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py index 24ed342..2433d07 100644 --- a/proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py +++ b/proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py @@ -214,13 +214,27 @@ def _add_connection_async(): return future_conn_activated + @staticmethod + def _device_is_enslaved(device: "NM.Device") -> bool: + ac = device.get_active_connection() + rc = ac.get_connection() if ac else None + sc = rc.get_setting_connection() if rc else None + if sc is None: + return False + getter = getattr(sc, "get_controller", None) or sc.get_master + return bool(getter()) + def get_physical_devices(self) -> List[NM.Device]: - """Returns all the active ethernet/wifi devices.""" + """Returns active ethernet/wifi/bridge devices that own their IP config + (excludes enslaved devices whose IP config lives on the master).""" return [ device for device in self._nm_client.get_devices() if ( - device.get_device_type() in (NM.DeviceType.ETHERNET, NM.DeviceType.WIFI) + device.get_device_type() in ( + NM.DeviceType.ETHERNET, NM.DeviceType.WIFI, NM.DeviceType.BRIDGE + ) and device.get_state() is NM.DeviceState.ACTIVATED - and device.get_active_connection() # Maybe this is redundant. + and device.get_active_connection() + and not self._device_is_enslaved(device) ) ] @@ -289,6 +303,11 @@ def _add_ipv4_route( connection = active_connection.get_connection() config = connection.get_setting_ip4_config() + if config is None: + raise GatewayNotFoundError( + f"Connection {connection.get_id()!r} has no IPv4 setting " + "(likely a bridge slave or unconfigured device)." + ) config.add_route( NM.IPRoute.new( @@ -309,6 +328,11 @@ def _remove_ipv4_routes( connection = active_connection.get_connection() config = connection.get_setting_ip4_config() + if config is None: + raise GatewayNotFoundError( + f"Connection {connection.get_id()!r} has no IPv4 setting " + "(likely a bridge slave or unconfigured device)." + ) routes_to_remove = [] for i in range(config.get_num_routes()):