Skip to content

WireGuard kill switch crashes with AttributeError when a bridge-slave (port) Ethernet device is present #17

Description

@thongor77

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

  1. 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.)
  2. Optionally also have a second active interface (Wi-Fi in my case) — not required to reproduce, but present in my setup.
  3. Connect via WireGuard protocol (kill switch setting: Off).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions