Kill switches fail in a direction people do not expect: they are built to block traffic while the tunnel is down, and the packets that build the tunnel are themselves traffic. That is the entire bug. A rule set that drops everything not leaving through the tunnel also drops the handshake that would bring it up, so the client retries in a loop and the error belongs to whichever stage is checked first. Before widening a firewall, find out which of three stages is being denied: the name lookup, the outer packet to the peer, or the reply coming back.

How the tunnel and its own transport depend on each other

Read a VPN client as two network states sharing one socket path. Before the tunnel exists, the client opens a socket on the physical adapter and sends encrypted frames to the peer address and port. Only after that exchange succeeds does it create the tunnel device and install routes. A kill switch is a policy about the second state, written so broadly that it applies to both.

Interface-based rules are the usual form: accept what leaves through tun0 or wg0, drop the rest of the output path. That formulation cannot exempt the client's own connection to the server, so a correct implementation adds that exception explicitly, and so do you when writing rules by hand. Per-application rules on Windows and macOS dodge part of this by matching a process instead of an interface, but the process still has to get out before the tunnel exists.

Three stages, in the order they happen

The first stage is resolution. When the profile stores a hostname instead of a literal address, the client needs a DNS answer before it can send anything. A profile that points DNS at the tunnel resolver creates a deadlock: the resolver is reachable only through the tunnel that the lookup is supposed to enable. Resolving outside the tunnel, or pinning the endpoint to an address, removes the ambiguity.

The second stage is the outer packet, which has to survive routing and then the firewall, in that order. wireguard-tools handles the routing half of this for WireGuard by marking its own packets and adding a policy rule so they skip the default route it just installed; the wg-quick man page describes that rule and the table it uses. Rebuild that routing by hand, or let a second tool rewrite the rules, and the marked packets can be sent back into the tunnel they are meant to establish.

The third stage is the return path. A handshake with no answer is not proof of an outgoing drop; it is equally consistent with a server inbound rule or a NAT device that forgot the mapping. Separate those before touching the client.

What you seeStageWhere to look
The client log repeats a resolve step and no packets appear on the wire1, name lookupResolver path while the tunnel is down
The send counter on the physical adapter stays flat2, routingPeer route and policy rules
The send counter rises and a firewall drop counter rises2, firewallRule order, mark, and interface match
Packets leave and nothing comes back3, return pathServer input rules, NAT, listening port

Clients log the symptom they detect, not the stage that failed, so the table is usually faster than the log.

Counters decide faster than logs

On Linux, nft list ruleset or iptables -vnL OUTPUT prints a packet counter per rule. Connect once, let it fail, then read the counters again. A rule whose counter moved is a rule the handshake hit; if nothing moved anywhere, the packet never reached the firewall. I have watched a rule set drop its own handshake, and only the counters said so.

To separate never reached the firewall from never left the host, watch the interface itself. ip -s link show dev wlan0 reports transmitted packets, and tcpdump -ni wlan0 udp port 51820 shows the frames. If it stays flat while the client retries, the fault is upstream of netfilter: a missing peer route, a policy rule pointing into a device that does not exist yet, or a socket error the client is swallowing.

Test exceptions against documentation addresses like 203.0.113.10, so a slip cannot probe someone else's host.

An outdated endpoint exception reads as a server outage

The most misleading version appears after the peer address changes: a dynamic-address service moved it, the host was rebuilt, or your network's egress address differs. The exception still names the old address, so packets to the new one hit the drop rule. The same configuration worked yesterday, which is why people start by troubleshooting the server.

A hostname inside a firewall rule does not save you. In nftables the name is resolved when the rule set is loaded, so it pins whatever address existed at that moment and goes stale like a literal one. The durable fix is to update the exception when the endpoint changes and keep it narrow: one peer address, one transport port. A range wide enough to survive an address change is also wide enough to let unrelated traffic out.

When the kill switch runs on the server host

The same dependency exists at the other end, and it is easier to miss there. If the machine acting as your VPN server also runs a desktop client with a kill switch, or a firewall profile copied from a laptop, its drop rules apply to the service terminating the tunnels. Replies to a client handshake get dropped, so every client fails to connect while the server looks healthy and its tunnel interface sits unused.

That host needs three narrow accepts ahead of the drop: the listen port on the physical interface, the tunnel interface for decrypted traffic, and forwarding from the tunnel toward whatever clients are meant to reach. Read the installed set with nft list ruleset, iptables -S, or pfctl -sr before changing anything, and remember that a persisted rule set survives reboot, so a mistake there outlives the process that made it.

Recovery order that keeps the machine closed

Work outward from the profile. Read the peer address and port the client uses, confirm the hostname resolves without the tunnel, then add one exception for that address and port on the physical adapter only. Reconnect and watch the send counter; a completed handshake is the signal that stage two is fixed.

Leave the rest of the drop policy in place while testing. A harmless check is to attempt a connection to an unrelated documentation address and confirm it still times out, which shows the kill switch working rather than switched off. Tighten the exception once the tunnel is stable, and do not leave a permanent allow-all for the client binary because it is convenient.

If the tunnel comes up and the application still fails, the remaining stages are not this one: split DNS, MTU, and per-application routing each produce a connected tunnel with broken traffic, and each has a different first check.

Mikanda mikatakata