Testing a VPN kill switch properly
A kill switch that has never been seen to fail closed is only a hope. Breaking the tunnel on purpose, and proving that nothing leaked.
Most kill switches are checked once, with a leak-test website, while everything is working. That proves the VPN works. It says nothing about the kill switch, which only matters in the moment the tunnel is gone. So that is the state to test in: tunnel down, traffic trying to get out, and someone watching the wire.
Know what you are testing
On this firewall, the kill switch is two independent layers. The first is routing: the WAN port sits alone in its own routing domain, and every internal network shares another whose only default route is the tunnel. With the tunnel down, there is simply no path to the WAN. The second is pf: on the WAN port, only the tunnel's own packets, DHCP and traffic deliberately tagged as an exception may leave. Anything else is blocked, logged as block_wan_leak, and raises an alert.
Two layers means two things to prove, separately. If you only test the combination, a broken first layer stays hidden behind a working second one, and you find out the day someone changes the pf rules.
Watch the wire, not the browser
A browser shows what one device experienced. The WAN port shows what actually left the building. Before breaking anything, start a capture on the WAN port that hides the tunnel's own traffic, so anything that prints is worth a look:
tcpdump -ni igc0 'not (udp and port 51820)' # everything except the tunnel itselfTest one: take the tunnel down
ifconfig wg0 down
route -n show -inet | grep default # still points at wg0, now unusableFrom a laptop and a phone: web pages, ping and DNS all failed. The routing table kept its default route on the tunnel, marked unusable, rather than finding another way out. On the capture, nothing but the tunnel's own error replies left the WAN port.
Then the important follow-up: the block_wan_leak counter was still zero. pf never had to step in, which means routing alone held. That is the first layer proved on its own.
pfctl -s labels | grep block_wan_leak # evaluations, packets, bytes, ...Test two: force a leak on purpose
The second layer only does anything when a packet is already heading for the WAN outside the tunnel. Routing makes that impossible from the internal networks, so create one deliberately, from the firewall, directly in the WAN's routing table:
ping -V 1 -c 3 9.9.9.9 # -V 1: use routing domain 1, the WAN'spf blocked all three pings. They appeared in the SIEM as block_wan_leak, and the alert fired. Second layer proved, and the alert with it.
Test three: the alarm, not just the lock
A kill switch that holds silently leaves you wondering why the internet stopped. A small script logs an error every minute while the last WireGuard handshake is more than three minutes old, and the SIEM alerts on it. Running it with a zero threshold forces the message, which tests the whole chain from script to syslog to alert without breaking anything.
wg-check 0 # pretend the tunnel is stale, then watch for the alertTest four: the view from outside
With the tunnel back up, the provider's own connection check reported traffic through the VPN, no DNS leaks and no WebRTC leaks. It is the least rigorous test of the four, but the only one that looks from the outside in.
The exceptions got the same treatment. A phone on the guest network, which deliberately leaves directly, showed the internet provider's address, as intended; a phone on the trusted network showed the VPN's.
Bring it back
ifconfig wg0 up # a handshake follows within secondsNone of this takes long, and all of it can be repeated after any change to routing or pf. That is the real point: the kill switch is now something with evidence behind it, not a line in a configuration file.