WireGuard kill switch crashes with AttributeError when a bridge-slave (port) Ethernet device is present
Summary
When connecting via WireGuard, the kill switch route injection code (add_vpn_server_route) crashes with AttributeError: 'NoneType' object has no attribute 'get_num_routes' if a physical Ethernet interface is enslaved to a NetworkManager bridge (e.g. br0 with eth0 as a bridge port). The connection attempt aborts every time, regardless of the kill switch setting (reproduced with kill switch Off).
Root cause
KillSwitchConnectionHandler.add_vpn_server_route() (killswitch_connection_handler.py) iterates over NMClient.get_physical_devices():
def get_physical_devices(self) -> List[NM.Device]:
"""Returns all the active ethernet/wifi devices."""
return [
device for device in self._nm_client.get_devices() if (
device.get_device_type() in (NM.DeviceType.ETHERNET, NM.DeviceType.WIFI) and
device.get_state() is NM.DeviceState.ACTIVATED and
device.get_active_connection()
)
]
This does not exclude Ethernet devices that are bridge ports/slaves. When an interface is a bridge slave, its own NM.RemoteConnection (the "Bridge slave" profile, e.g. bridge-slave-eth0) has no ipv4 setting group — the IPv4 config lives on the bridge master connection (br0), not on the port. Calling nmcli connection show bridge-slave-eth0 confirms it: there is no ipv4.* property block at all for a slave profile.
add_route_to_device() then calls _remove_ipv4_routes():
connection = active_connection.get_connection()
config = connection.get_setting_ip4_config() # returns None for a bridge-slave profile
for i in range(config.get_num_routes()): # AttributeError: 'NoneType' object has no attribute 'get_num_routes'
config is None, so the loop crashes immediately.
Reproduction
- Set up a NetworkManager bridge with a physical Ethernet device as a port, e.g.:
nmcli connection show
NAME TYPE DEVICE
bridge-br0 bridge br0
bridge-slave-eth0 ethernet eth0
(br0 carries the IP/default route; eth0 is enslaved with no IP config of its own.)
- Optionally also have a second active interface (Wi-Fi in my case) — not required to reproduce, but present in my setup.
- Connect via WireGuard protocol (kill switch setting: Off).
- Connection fails immediately every time.
Traceback (proton-vpn-gtk-app 4.16.5, Flatpak com.protonvpn.www)
proton.vpn.app.gtk.utils.exception_handler:300 | CRITICAL | APP:CRASH | Unexpected error.
Traceback (most recent call last):
File ".../proton/vpn/core/vpnconnector.py", line 394, in connect
await self._on_connection_event(events.Up(...))
File ".../proton/vpn/core/vpnconnector.py", line 463, in _on_connection_event
event = await self._handle_on_event(event)
File ".../proton/vpn/core/vpnconnector.py", line 448, in _handle_on_event
return await self._update_state(new_state)
File ".../proton/vpn/core/vpnconnector.py", line 499, in _update_state
new_event = await self._current_state.run_tasks()
File ".../proton/vpn/connection/states.py", line 257, in run_tasks
await self.context.kill_switch.enable(...)
File ".../proton/vpn/backend/networkmanager/killswitch/wireguard/wgkillswitch.py", line 70, in enable
await self._ks_handler.add_vpn_server_route(server_ip=vpn_server.server_ip)
File ".../proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection_handler.py", line 154, in add_vpn_server_route
await _wrap_future(...)
File ".../proton/vpn/backend/networkmanager/killswitch/wireguard/killswitch_connection_handler.py", line 56, in _wrap_future
return await asyncio.wait_for(...)
File ".../proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py", line 375, in _add_ipv4_route
cls._remove_ipv4_routes(active_connection, new_server_ip)
File ".../proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py", line 314, in _remove_ipv4_routes
for i in range(config.get_num_routes()):
AttributeError: 'NoneType' object has no attribute 'get_num_routes'
Same crash reproduced with kill switch Off, on both the WireGuard and default-gateway server ("Fastest" pick), on two different WireGuard servers.
This is a regression
The exact same bridge topology (br0 + eth0 as bridge slave) worked fine with WireGuard on 2026-03-07 using the then-current native package proton-vpn-gtk-app 4.14.1-2 (Arch Linux). Log excerpt from that successful connection:
proton.vpn.core.connection:400 | INFO | CONN.CONNECT:START | ... Protocol: wireguard
proton.vpn.backend.networkmanager.core.networkmanager:91 | INFO | VPN server REACHABLE.
proton.vpn.core.connection:508 | INFO | CONN:STATE_CHANGED | Connected
So this isn't an inherent WireGuard+bridge incompatibility — something in the kill switch route-injection code regressed between then and the current release (reproduced on 4.16.5).
Environment
- OS: Arch Linux
- proton-vpn-gtk-app: 4.16.5 (Flatpak,
com.protonvpn.www, flathub)
- Network: NetworkManager-managed bridge
br0 with a USB-C-attached Ethernet adapter (eth0) as bridge port, plus Wi-Fi (wlan0) active concurrently
- Protocol: WireGuard (OpenVPN works fine as a workaround — its kill switch implementation uses exclusion routes on a dedicated connection profile instead of
device_reapply on physical devices, so it never hits this code path)
Suggested fix
In NMClient.get_physical_devices(), exclude devices that are bridge/bond/team ports (e.g. check device.get_active_connection().get_connection().get_setting_connection().get_master() is None, or check device.get_master() is None on the NM.Device), since a port device never carries its own routable IPv4 config — only the master (bridge) device does. Alternatively, guard _add_ipv4_route/_remove_ipv4_routes against config is None and skip the device gracefully instead of crashing.
WireGuard kill switch crashes with AttributeError when a bridge-slave (port) Ethernet device is present
Summary
When connecting via WireGuard, the kill switch route injection code (
add_vpn_server_route) crashes withAttributeError: 'NoneType' object has no attribute 'get_num_routes'if a physical Ethernet interface is enslaved to a NetworkManager bridge (e.g.br0witheth0as a bridge port). The connection attempt aborts every time, regardless of the kill switch setting (reproduced with kill switch Off).Root cause
KillSwitchConnectionHandler.add_vpn_server_route()(killswitch_connection_handler.py) iterates overNMClient.get_physical_devices():This does not exclude Ethernet devices that are bridge ports/slaves. When an interface is a bridge slave, its own
NM.RemoteConnection(the "Bridge slave" profile, e.g.bridge-slave-eth0) has noipv4setting group — the IPv4 config lives on the bridge master connection (br0), not on the port. Callingnmcli connection show bridge-slave-eth0confirms it: there is noipv4.*property block at all for a slave profile.add_route_to_device()then calls_remove_ipv4_routes():configisNone, so the loop crashes immediately.Reproduction
br0carries the IP/default route;eth0is enslaved with no IP config of its own.)Traceback (proton-vpn-gtk-app 4.16.5, Flatpak
com.protonvpn.www)Same crash reproduced with kill switch Off, on both the WireGuard and default-gateway server ("Fastest" pick), on two different WireGuard servers.
This is a regression
The exact same bridge topology (
br0+eth0as bridge slave) worked fine with WireGuard on 2026-03-07 using the then-current native packageproton-vpn-gtk-app 4.14.1-2(Arch Linux). Log excerpt from that successful connection:So this isn't an inherent WireGuard+bridge incompatibility — something in the kill switch route-injection code regressed between then and the current release (reproduced on 4.16.5).
Environment
com.protonvpn.www, flathub)br0with a USB-C-attached Ethernet adapter (eth0) as bridge port, plus Wi-Fi (wlan0) active concurrentlydevice_reapplyon physical devices, so it never hits this code path)Suggested fix
In
NMClient.get_physical_devices(), exclude devices that are bridge/bond/team ports (e.g. checkdevice.get_active_connection().get_connection().get_setting_connection().get_master()isNone, or checkdevice.get_master() is Noneon theNM.Device), since a port device never carries its own routable IPv4 config — only the master (bridge) device does. Alternatively, guard_add_ipv4_route/_remove_ipv4_routesagainstconfig is Noneand skip the device gracefully instead of crashing.