VPN Killswitch for Kali Linux — 7 Easy Steps 🔒
I learned this the annoying way: a VPN tunnel can drop for one second, and Linux will happily continue routing traffic like nothing happened. Your browser won’t scream. Your terminal won’t warn you. Your ISP just gets a little surprise cameo.
This guide is my “no excuses” approach: a small nftables ruleset that enforces fail-closed behavior, simple scripts you can bind to dock buttons, and a verification routine that catches problems before you trust the connection.
VPN Killswitch for Kali Linux should mean one thing: if the tunnel isn’t up, nothing leaves. Not DNS. Not “just one packet.” Not “oops I clicked refresh.”
This post builds on my baseline WireGuard setup guide. If you haven’t done that yet, start here first:
👉 How to Setup WireGuard ProtonVPN on Kali Linux (Step-by-Step Guide) 🧭
Promise of this guide: if the tunnel drops, your machine should go dark. No DNS trickle. No fallback. If it’s not through
wg0, it doesn’t leave.
Key Takeaways 🧭
What you’ll get after following this guide:
- Default-deny outbound rules so “tunnel down” automatically means “no traffic.”
- A tiny firewall policy you can actually read and audit in under a minute.
- One-click scripts for enabling/disabling the policy and for instant blackout mode.
- A strict DNS path so your resolver doesn’t quietly wander off.
- A repeatable 5-minute verification routine that prevents “I assumed it was fine.”
- Clean region switching without weird routing or accidental double tunnels.
What you’ll build (exactly) 🎯
- A minimal nftables killswitch that only allows traffic via
wg0(plus what’s needed to establish the tunnel). - A “panic button” ruleset that blocks everything instantly.
- A DNS guard that refuses off-tunnel resolver traffic.
- Scripts you can attach to dock buttons or desktop launchers.
- Multi-region switching without overlap or leaks.

What I’m using (and why) 🧰
Environment: Kali Linux in a VM (VirtualBox/VMware). NAT or bridged both work. Bridged can be handy for some lab scenarios.
WireGuard profiles: Downloadable .conf files from your provider dashboard. I keep short names like protonus, protonbe (or protonnl), protonin.
Packages:
sudo apt update
sudo apt install -y nftables wireguard-tools resolvconf dnsutils curl jqIf you prefer systemd-resolved over resolvconf, that’s fine. The core requirement is simple: wg-quick must be able to set resolvers on up and restore them on down.
Step 1 — Start clean 🧹
Old firewall rules are like old coffee: best case they do nothing, worst case they ruin your day.
sudo nft flush ruleset
sudo nft list ruleset # should print nothingIf you’re on a VM, take a snapshot now. Future-you will send a thank-you email to past-you (and forget to hit send).
Step 2 — The minimal nftables killswitch 🛡️
Create a working directory:
mkdir -p "$HOME/vpn/scripts"Save this as $HOME/vpn/killswitch.nft:
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0;
policy drop;
iif lo accept
ct state established,related accept
iifname "wg0" accept
}
chain forward {
type filter hook forward priority 0;
policy drop;
}
chain output {
type filter hook output priority 0;
policy drop;
oif lo accept
ct state established,related accept
oifname "wg0" accept
# Basic mode: allow WireGuard handshake port (works everywhere, less strict).
# Tighten this in Step 2B by pinning the endpoint IP:port.
udp dport 51820 accept
}
}What this does: everything is dropped by default, except loopback, established connections, and traffic through wg0. The only non-wg0 outbound allowed is the WireGuard handshake so the tunnel can come up.
Step 2B — Harden it (pin the handshake to your endpoint) 🔩
The “basic mode” rule allows UDP/51820 to anywhere. It’s fine for a quick start, but if you want a stricter policy, pin it to the endpoint IP and port in your .conf.
First, identify your endpoint from your WireGuard profile:
grep -n "^Endpoint" /etc/wireguard/protonus.confIf it’s a hostname, resolve it (run this before enforcing ultra-strict rules):
# Replace HOSTNAME with what you see after Endpoint=
dig +short HOSTNAME | head -n 1Then replace the broad line:
udp dport 51820 accept
…with an endpoint-pinned allow (example):
ip daddr 185.159.X.X udp dport 51820 accept
Note: if your provider rotates endpoint IPs, pinned rules mean you’ll need to update them when the IP changes. For travel setups, I keep “basic mode” as a reliable fallback.

Step 3 — Scripts that save hours ⚡
Create three scripts: killswitch on, killswitch off, and a “status peek.”
Script 1: $HOME/vpn/scripts/kill-on
#!/usr/bin/env bash
set -euo pipefail
sudo nft -f "$HOME/vpn/killswitch.nft"
echo "[kill-on] Fail-closed policy active."Script 2: $HOME/vpn/scripts/kill-off
#!/usr/bin/env bash
set -euo pipefail
sudo nft flush ruleset
echo "[kill-off] Firewall flushed (traffic is NOT restricted)."Script 3: $HOME/vpn/scripts/kill-status
#!/usr/bin/env bash
set -euo pipefail
echo "=== nftables (first lines) ==="
sudo nft list ruleset | sed -n '1,120p' || true
echo
echo "=== wg ==="
wg || trueMake them executable:
chmod +x "$HOME/vpn/scripts/"*Persistence (only when stable): Don’t persist while you’re still experimenting. When you’re happy:
sudo systemctl enable nftables
sudo sh -c 'nft list ruleset > /etc/nftables.conf'Step 4 — Panic button 🔴 (instant blackout)
This is for the moments when “only VPN traffic” isn’t enough and you want zero packets leaving your machine. Captive portals, sketchy Wi-Fi, “what is that network name,” etc.
Save this as $HOME/vpn/panic.nft:
flush ruleset
table inet filter {
chain input { type filter hook input priority 0; policy drop; }
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy drop; }
}$HOME/vpn/scripts/panic-on
#!/usr/bin/env bash
set -euo pipefail
sudo nft -f "$HOME/vpn/panic.nft"
echo "[panic-on] ALL traffic blocked."$HOME/vpn/scripts/panic-off
#!/usr/bin/env bash
set -euo pipefail
sudo nft flush ruleset
echo "[panic-off] Traffic unblocked (no firewall rules)."Make them executable too:
chmod +x "$HOME/vpn/scripts/panic-"*Quick proof:
"$HOME/vpn/scripts/panic-on"
ping -c 1 1.1.1.1 # should fail instantly
"$HOME/vpn/scripts/panic-off"
Step 5 — DNS leak guard (strict, but simple) 🌐🔍
DNS is the classic “silent failure.” Your public IP can look fine while your resolver still whispers sweet nothings to your ISP.
5A — Make sure a DNS manager exists
resolvectl status 2>/dev/null || systemctl status systemd-resolved 2>/dev/null || trueIf nothing is managing resolvers, wg-quick can’t reliably swap DNS on connect/disconnect.
5B — Set DNS inside your WireGuard profile
Inside your /etc/wireguard/protonXX.conf, under [Interface]:
DNS = 10.2.0.1Use the resolver address your provider documents for WireGuard (the example above is common, but verify yours).
5C — Enforce “DNS must go through the tunnel”
Add these lines to the output chain in your killswitch.nft (below the wg0 allow):
IPv4 (recommended baseline):
ip daddr != 10.2.0.1 udp dport 53 drop
IPv6 (optional): only if your setup actually uses IPv6 inside the tunnel and you know the resolver address.
ip6 daddr != fd00:10:2::1 udp dport 53 drop
Then re-apply:
"$HOME/vpn/scripts/kill-on"Browser DoH note: DNS-over-HTTPS can bypass OS DNS. During testing, disable DoH temporarily, confirm the OS path is correct, then re-enable DoH inside the tunnel if you want.
“A VPN without a leak policy is a false sense of security. Always test DNS, IPv6, and browser leaks after setup.”
Step 6 — One-click dock buttons 🖱️✨
Typing commands is where I personally start making “creative mistakes.” Dock buttons reduce mistakes and speed up your workflow.
Create a launcher file like $HOME/.local/share/applications/vpn-kill-on.desktop:
[Desktop Entry]
Name=VPN Kill-On
Exec=/bin/bash -lc "$HOME/vpn/scripts/kill-on"
Icon=security-high
Type=Application
Terminal=trueRepeat for Kill-Off, Panic-On, Panic-Off, and (optionally) region switching. You can pin these to the dock/menu depending on your desktop environment.

Multi-region switching 🌍
Keep profiles named predictably. Script: $HOME/vpn/scripts/switch-region
#!/usr/bin/env bash
set -euo pipefail
REG="${1:-us}" # us|be|nl|in (adapt to your names)
# Bring everything down first (prevents double tunnels / weird routes)
sudo wg-quick down protonus 2>/dev/null || true
sudo wg-quick down protonbe 2>/dev/null || true
sudo wg-quick down protonnl 2>/dev/null || true
sudo wg-quick down protonin 2>/dev/null || true
# Now bring the requested region up
sudo wg-quick up "proton${REG}"
echo "[switch-region] active: proton${REG}"Make it executable:
chmod +x "$HOME/vpn/scripts/switch-region"Step 7 — My 5-minute verification routine ✅🧪
This routine is what turns “I think it’s working” into “I verified it.”
# 1) Enforce fail-closed
"$HOME/vpn/scripts/kill-on"
# 2) Bring up a region
sudo wg-quick up protonus
# 3) Handshake visible?
wg
# 4) Public IP should NOT be your ISP
curl -s ifconfig.io; echo
# 5) DNS sanity check (basic signal)
dig +short TXT whoami.cloudflare @1.1.1.1
# 6) Bring it down
sudo wg-quick down protonus
# 7) Still blocked? (must FAIL)
ping -c 1 1.1.1.1If ping works after the tunnel is down, your policy isn’t fail-closed. Reapply kill-on and inspect the ruleset.
Why nftables (not iptables/UFW)? 🔧
I started with UFW because it’s friendly. But once you want a tight policy (“only wg0 + exactly what’s needed to establish the tunnel”), UFW becomes awkward. iptables works, but nftables is the modern firewall stack and it’s easier to keep one clean ruleset for IPv4/IPv6.
- Cleaner syntax that’s easier to audit later.
- Unified handling of IPv4/IPv6 (fewer blind spots).
- Simple default-deny policies (fail-closed by design).
“nftables is the future of Linux firewalls: cleaner syntax and a modern replacement for iptables.”

Common mistakes (so you don’t repeat my pain) 🚨
- Forgetting the handshake allow rule → tunnel never comes up.
- Assuming DNS is “fine” → test, then enforce.
- Two tunnels at once → confusing routes and unpredictable behavior.
- Pinning endpoint IP without a plan for IP rotation → strict rules suddenly block your connect.
- Forgetting to export
/etc/nftables.confafter changes → “why didn’t it persist?”

Frequently Asked Questions ❓
❓ Do I need a paid plan to use WireGuard configs on Linux?
It depends on your account tier and what downloads your dashboard exposes. The fastest way to confirm is to log in and check if WireGuard .conf files are available under Downloads. If you don’t see them, you can use OpenVPN configs or upgrade to a plan that includes WireGuard downloads.
❓ Why not just use the official app?
On supported distros the official app can be great. On Kali, app support is often not guaranteed. That’s why this manual method is so reliable: it’s portable, auditable, and easy to verify.
❓ How do I know if DNS is leaking?
I verify my public IP changed, then run a DNS query test. If IP changes but DNS behavior still feels inconsistent, I treat it as a signal to harden DNS routing. That’s where doing DNS protection properly is worth it.
❓ Is a kill switch overkill?
Not in my experience. Link flaps, suspend/resume, and network weirdness can drop a tunnel. A kill switch reduces the chance of traffic escaping over the default interface. I prefer nftables because it’s explicit and easy to audit.
❓ Can I use this approach on Ubuntu, Mint, or Debian?
Yes. The WireGuard layout and wg-quick workflow are very similar. The biggest differences are DNS management and desktop integration — the core firewall policy still applies.
Closing thoughts 🧠
This is my answer to “How do I stop leaks when the tunnel drops?”: make failure obvious by making it loud. No tunnel should equal no traffic. Once you have that baseline, you can build convenience on top without losing your safety net.
Next posts in this workflow (stand-alone guides):
- ✅ Baseline: How to Setup WireGuard ProtonVPN on Kali Linux
- ⏭️ Next: Kali Linux VPN Automation
- ⏭️ Later: Kali Linux Split Tunneling
Stay sharp, stay invisible 👻
