A WireGuard kill switch is firewall state that you drive from the config file itself. WireGuard ships no such feature, so wg-quick gives you the hooks instead. The result is portable: the block goes in the same .conf you already import, it comes up with the tunnel, and it comes down with the tunnel. This page gives the rule for iptables and the rule for nftables. It adds IPv6 and a LAN exception to both, then explains the one config setting that quietly breaks the whole thing.
Why WireGuard has no kill switch of its own
WireGuard is a tunnel, not a network manager. It does not decide what happens to traffic that is not for the tunnel, because that is a policy question and WireGuard does not hold policy. So the kill switch lives one layer out, in the firewall, and wg-quick gives you four hooks to reach it: PreUp, PostUp, PreDown and PostDown. The wg-quick manual describes them as script snippets run by bash before and after setting up or tearing down the interface. It names custom DNS options and firewall rules as their most common use.
Two clients do offer something built in, and they are the reason the answer sounds inconsistent depending on who you ask.
- The WireGuard app on Windows applies firewall rules of its own, toggled by a checkbox in its editor
- Some providers ship configs with the
PostUprule already written in, so it looks like a protocol feature wg-quickon Linux, macOS and BSD gives you the hooks but writes no rule for you- On iOS and Android, check the platform always-on VPN setting instead
The Windows behaviour is documented rather than guessed, and the detail is worth having. The WireGuard project states that the rules activate when an interface has a single peer and that peer carries an AllowedIPs entry with a /0 prefix. Under them, packets from the tunnel service, packets through the tunnel, loopback, DHCP and IPv6 neighbour discovery are permitted. Everything else is blocked. Where DNS servers are configured, port 53 traffic is permitted only to those servers.
The documented way to keep a default route without those rules is to write 0.0.0.0/1 and 128.0.0.0/1 in place of 0.0.0.0/0, and ::/1 and 8000::/1 in place of ::/0. That is what the checkbox toggles. Knowing this matters on Linux too, because a provider config using split halves rather than /0 is telling you something about how it was generated.
Everything below is Linux, because that is where you write the rule yourself.
The WireGuard kill switch in PostUp and PreDown
Read this before you run anything. A wrong `PostUp` rule locks the machine out of the network. A rule inserted by PostUp is not removed until PreDown runs. Do not do this over SSH on a remote machine. The session that would let you fix it is the session the rule drops. Keep a console or a recovery path open.
The iptables version
This is the rule from the wg-quick manual page, and it is the one every incumbent reproduces:
[Interface]
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECTRead it right to left. It rejects any outbound packet that fails three tests at once. The packet is not leaving on the WireGuard interface, does not carry WireGuard's own firewall mark, and is not addressed to this machine. The mark exception is what lets the encrypted packets themselves reach the server. Without it the rule would block the tunnel it exists to protect.
%i expands to the interface name, so the same file works as wg0.conf or as anything else. $(wg show %i fwmark) is evaluated when the hook runs, which matters more than it looks. That is covered below.
The nftables version
This is the section no page in the top 10 has, and it is the reason the Arch BBS poster in thread 276745 was stuck. The vendor documentation told them to use iptables and wireguard-tools. They were running nftables and had neither installed, and translating the rule with iptables-translate produced directives that did not work.
[Interface]
PostUp = nft add table inet killswitch
PostUp = nft add chain inet killswitch output '{ type filter hook output priority 0; policy accept; }'
PostUp = nft add rule inet killswitch output oifname != "%i" meta mark != $(wg show %i fwmark) fib daddr type != local counter reject
PreDown = nft delete table inet killswitchThe inet family is the point. One table covers IPv4 and IPv6 together, so the nftables version needs one rule where iptables needs two. fib daddr type != local is the direct equivalent of -m addrtype ! --dst-type LOCAL. The nftables manual documents fib under FIB expressions, where fib daddr type looks the destination address up in the forwarding information base and returns its address type. It needs a kernel and an nftables build carrying that support. Load the rule by hand once, and confirm with nft list ruleset before you commit it to a config file.
Teardown is one line rather than a mirrored delete. nft delete table inet killswitch removes the table, the chain and the rule together, so there is nothing to leave behind if the string drifts. That is a real operational difference and it is not cosmetic. Whichever engine you choose, run each hook line at a shell prompt before it goes into the config file. Then read the loaded state back with iptables -S OUTPUT or nft list ruleset, and confirm that the rule you meant is the rule that landed.
What Table = off changes
Table controls the routing table wg-quick writes to. The manual gives two special values: off disables the creation of routes altogether, and auto, the default, adds routes to the default table and enables special handling of default routes.
That special handling is the part that matters here. When AllowedIPs contains a default route, wg-quick sets a firewall mark on WireGuard's own packets and installs policy routing rules to keep them out of the tunnel. $(wg show %i fwmark) is how the kill switch rule reads that mark back.
Set Table = off and there is no mark to read. Run wg show <interface> fwmark yourself before relying on the rule. If it does not hand back a mark, the command substitution has nothing to insert, the rule does not parse, and the hook fails. The same happens if AllowedIPs is a set of specific subnets rather than 0.0.0.0/0, because there is no default route to handle specially. If you are running split tunnelling, this rule is the wrong rule for you. The interface-scoped approach on the Linux kill switch page is the one that still works.
IPv6, and why the IPv4 rule alone is not enough
An iptables rule filters IPv4 only. Leave it at that and every dual-stack destination on the internet remains reachable over IPv6 the moment the tunnel drops, which defeats the entire setup on any modern connection. Add the paired rule:
PostUp = ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECTThe nftables rule already covers this, because inet is a dual-family table. That is the strongest single argument for the nftables version on a machine you control.
Blocking IPv6 outbound is not the same as carrying IPv6 inside the tunnel. Check the Address line and AllowedIPs in your config. With no IPv6 address and no ::/0, IPv6 is simply dead while the tunnel is up, and dead is safe. Getting IPv6 to work inside the tunnel is a separate job and it belongs with AllowedIPs and policy routing.
Letting your LAN through
The manual rule exempts addresses local to the machine itself. It does not exempt your local network. Once it is active your printer, your NAS and your router admin page all stop answering, and that is the change people notice first.
Add the exception after the reject rule, not before:
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PostUp = iptables -I OUTPUT -d 192.168.1.0/24 -j ACCEPT
PreDown = iptables -D OUTPUT -d 192.168.1.0/24 -j ACCEPT
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECTOrder is the whole trick. -I inserts at the top of the chain, so whichever rule you insert last is evaluated first. Insert the reject rule first and the LAN accept second, and the LAN accept ends up above it. Reverse the two lines and the exception never gets read.
Name your actual subnet. Copying 192.168.0.0/16 from somewhere else opens a range far wider than your house, and on a shared network that is the opposite of what you came here for. Routing is not the problem to solve here. With Table = auto, wg-quick adds a policy rule of the form ip rule add table main suppress_prefixlength 0. The ip-rule manual defines suppress_prefixlength as rejecting routing decisions with a prefix length of that number or less. At zero it rejects only a default route, so your connected LAN route still wins and LAN traffic never enters the tunnel. The LAN exception is therefore purely a firewall matter.
The failure mode to plan for
A half-run config is the specific way this bites. PostUp lines run in order, and if one of them fails the interface may not finish coming up. PreDown has not run, because the interface never reached the state where bringing it down means anything. The rule from the line that did succeed is now sitting in your OUTPUT chain with nothing scheduled to remove it.
The symptom is a machine with no network access and no VPN, which looks like a broken tunnel and is actually a stranded firewall rule. Check before you conclude anything else:
- Run
iptables -S OUTPUTornft list rulesetand read what is actually loaded - Delete the stranded rule by hand with the exact
-Dform of the-Iline that created it - On nftables,
nft delete table inet killswitchclears everything in one command - Test any new hook line in a shell first, so a typo fails at the prompt rather than inside
wg-quick
Keep the recovery command somewhere you can reach without a network. A machine locked out by its own stranded rule cannot search for the fix, and the exact -D string you need is sitting in a config file you can still read locally. Print it, or write it on paper, before you enable a hook on a machine that matters.
Comparing the two rule engines
| Capability | iptables | nftables |
|---|---|---|
| Rule syntax | Two commands, one for IPv4 and one for IPv6, mirrored again in PreDown | One inet table covers both families, in one rule |
| IPv6 handling | Separate ip6tables rule, easy to omit, and omitting it is the most common real leak | Included by default in the inet family |
| Persistence across reboot | Not applicable here. The rules live in PostUp and are recreated on every tunnel start | Same. Do not add these to /etc/nftables.conf or they outlive the tunnel |
| Teardown on a half-run config | Delete each rule individually with the exact matching -D string | nft delete table inet killswitch removes the whole table at once |
| Availability | Present or emulated on effectively every distribution | Run nft --version to see whether your distribution ships it |
The honest summary is that iptables is more portable and nftables is easier to undo. If you are choosing today on a machine you own, choose nftables. If you are writing a config that will be handed to strangers on unknown distributions, the iptables form fails in fewer places.
Confirming it actually blocks
Bring the tunnel up, confirm browsing works, then run wg-quick down wg0 and confirm traffic stops. That much is a sanity check rather than a test.
The real check is narrower and it separates two things a browser cannot separate. An IP leak and a DNS leak are different failures with different fixes, and a refreshed tab shows you neither one directly. Run a packet capture on the physical interface, generate traffic that exercises name resolution and a raw address independently, and then drop the tunnel four different ways. The full procedure is in how to test if a VPN kill switch works.
Quick answers
- Can I build a kill switch with only a bare WireGuard config file
- Yes, and that is exactly what PostUp and PreDown are for. The Arch Linux BBS poster in thread 276745 described the case precisely: bare WireGuard configuration files, no vendor client, and a wish to block the connection automatically when the tunnel goes down. No extra software is required beyond wg-quick and whichever firewall your distribution already runs.
- Does WireGuard have a kill switch built in
- No. WireGuard is a tunnel protocol and does not decide what happens to traffic outside the tunnel. What exists is wg-quick, which runs your firewall commands through PostUp and PreDown, and the WireGuard app on Windows, whose editor carries a checkbox toggling firewall rules the project documents. Both are wrappers around firewall behaviour rather than protocol features.
- What does Table = off actually do
- It stops wg-quick from creating any routes for the interface. The manual describes off as disabling the creation of routes altogether, against auto, which adds routes to the default table and handles default routes specially. Use it when you are managing routing yourself. It also removes the firewall mark that the standard kill switch rule reads, so the two settings do not combine.
- Does a kill switch stop DNS leaks
- Not by itself. A kill switch blocks traffic when the tunnel is down. A DNS leak happens while the tunnel is up, when the operating system sends queries to a resolver outside it. The firewall rule cannot tell a leaking query from a legitimate one, because both are outbound packets on a working connection. Fix DNS separately, starting with DNS leak protection.
- How to tell if wireguard is working
- Run wg show. A working peer has a recent latest handshake timestamp and transfer counters that move in both directions. A peer with no handshake and a 0 timestamp never completed the key exchange. Counters that move while nothing loads point at MTU or routing rather than at the tunnel. Reading wg show first saves changing settings that were never wrong. For the cross-OS view of where a kill switch belongs, start at the kill switch setup guide. If you are still getting the tunnel up, installing WireGuard is the earlier step. For the firewall-outward version that covers OpenVPN as well, and for the UFW recipe deliberately left out of this page, see the Linux VPN kill switch page. And keep what a kill switch does and does not do open while you work, because a kill switch does not make a weak provider trustworthy.