Skip to content
SumGuy's Ramblings
Go back

VPN Kill Switch and DNS Leak Prevention: Paranoia, Justified

Your VPN Drops. Then What?

Most VPN tutorials stop at “here’s how to connect.” That’s like teaching someone to drive and ending the lesson before you mention the brakes.

When a VPN drops — and they drop, because networks are chaos — your OS doesn’t just sit there waiting. It falls back to the next available route: your regular internet connection. Your “private” traffic is now going out naked over your ISP’s network. This is called a VPN leak, and it happens silently, with no warning, often for minutes before you notice.

A kill switch prevents this. When the VPN goes down, traffic stops entirely rather than falling back. No fallback, no leak. Just silence until the VPN comes back.

Combined with DNS leak prevention — making sure your DNS queries also go through the VPN — you get something that’s actually worth calling a private connection.

WireGuard Kill Switch with iptables

WireGuard has PostUp and PreDown hooks that run shell commands when the interface comes up or goes down. This is where you put your kill switch logic.

Here’s a complete WireGuard config with a kill switch built in:

[Interface]
PrivateKey = YOUR_PRIVATE_KEY_HERE
Address = 10.0.0.2/24
DNS = 1.1.1.1

# Kill switch: block all traffic not going through WireGuard
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PostUp = ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT

# Tear down kill switch when VPN goes down
PreDown = iptables -D 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 REJECT

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

Let’s unpack the iptables rule:

When WireGuard is up, it marks its own packets with an fwmark so they bypass the OUTPUT rule. When WireGuard goes down, the iptables rule stays in the OUTPUT chain (PostUp ran, PreDown didn’t) — so all traffic is rejected. When PreDown runs cleanly (i.e., you intentionally bring the VPN down), it removes the rule, restoring normal traffic.

The AllowedIPs = 0.0.0.0/0 Pattern

The AllowedIPs = 0.0.0.0/0, ::/0 line is what makes this a full tunnel — all IPv4 and IPv6 traffic routes through the VPN peer.

Split tunnel alternative (if you only want specific traffic through the VPN):

# Only route private network traffic through VPN
AllowedIPs = 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

# Only route a specific external subnet
AllowedIPs = 203.0.113.0/24

Full tunnel vs split tunnel trade-offs:

Full TunnelSplit Tunnel
PrivacyAll traffic hidden from ISPOnly specific traffic hidden
PerformanceVPN becomes bottleneck for everythingLocal traffic unaffected
Kill switchAll-or-nothingHarder to reason about leaks
DNS leaksEasier to preventMore complex configuration

For privacy use cases: full tunnel. For performance-sensitive home lab access: split tunnel.

Testing Your Kill Switch

# Check your current IP before VPN
curl -s ifconfig.me

# Bring up WireGuard
sudo wg-quick up wg0

# Verify IP changed
curl -s ifconfig.me

# Simulate VPN drop (bring down the interface without running PreDown)
sudo ip link set wg0 down

# This should fail or hang — no traffic should leave
curl -s --max-time 5 ifconfig.me
# Expected: curl: (28) Operation timed out

# Properly bring VPN down (runs PreDown, removes iptables rules)
sudo wg-quick down wg0

# Traffic should work again
curl -s ifconfig.me

The key test is step 4: silently dropping the interface without running wg-quick down. Traffic should stop. If it doesn’t, your kill switch isn’t working.

What Is a DNS Leak?

DNS is the phone book: you ask “what’s the IP for google.com?” and a DNS server answers.

Your VPN encrypts your traffic, but DNS queries are just network traffic like anything else. Without explicit configuration, they might:

  1. Go out your regular network interface before the VPN routes kick in
  2. Use your ISP’s DNS servers instead of the VPN’s DNS
  3. Use systemd-resolved’s caching, which might have stale entries from before the VPN connected

The result: your VPN says you’re in Germany, but your DNS queries go through your US ISP’s servers. Any observer watching DNS traffic knows exactly what sites you’re visiting — and your ISP’s resolver can see it all.

Locking DNS Through the VPN

Method 1: The WireGuard DNS directive

The DNS = 1.1.1.1 line in your WireGuard config tells wg-quick to update your system’s DNS settings when the VPN connects. Simple, but depends on your system actually respecting it.

[Interface]
# ...
DNS = 1.1.1.1, 1.0.0.1
# Or use your VPN provider's private DNS
DNS = 10.0.0.1

Method 2: systemd-resolved configuration

If you’re using systemd-resolved (most modern Ubuntu/Debian systems are), configure it to send DNS queries through the VPN interface:

# Check if systemd-resolved is running
systemctl status systemd-resolved

# After WireGuard connects, tell systemd-resolved to use the VPN interface for DNS
sudo resolvectl dns wg0 1.1.1.1
sudo resolvectl domain wg0 "~."  # The ~. means "all domains go through this interface"

# Verify
resolvectl status

For a permanent fix, create a resolved configuration:

# /etc/systemd/resolved.conf.d/vpn-dns.conf
[Resolve]
DNS=1.1.1.1 1.0.0.1
DNSOverTLS=yes
DNSSEC=yes

Method 3: Locking /etc/resolv.conf

The old-school approach — lock the resolv.conf file so nothing can change it:

# Set your DNS servers
cat > /etc/resolv.conf << EOF
nameserver 1.1.1.1
nameserver 1.0.0.1
EOF

# Make it immutable (not even root can change it without removing this flag)
sudo chattr +i /etc/resolv.conf

# Verify
lsattr /etc/resolv.conf
# Should show: ----i---------e- /etc/resolv.conf

This is blunt and breaks NetworkManager/DHCP trying to update DNS. Fine for a dedicated VPN device or server. Annoying on a laptop that moves between networks.

dnscrypt-proxy: Encrypting the DNS Queries Themselves

Even with DNS going through your VPN, your DNS queries land on a resolver somewhere. If that resolver is 1.1.1.1 at Cloudflare, you’re trusting Cloudflare. dnscrypt-proxy encrypts DNS traffic using the DNSCrypt protocol and lets you route to resolvers that don’t log.

apt install dnscrypt-proxy

# Edit /etc/dnscrypt-proxy/dnscrypt-proxy.toml
listen_addresses = ['127.0.0.1:5300']

# Only use servers that don't log and aren't censored
require_nolog = true
require_nofilter = true
require_dnssec = true

# Fallback resolver (used only for initial bootstrap)
fallback_resolvers = ['1.1.1.1:53', '8.8.8.8:53']
ignore_system_dns = true
systemctl enable dnscrypt-proxy
systemctl start dnscrypt-proxy

# Update resolv.conf or systemd-resolved to use it
echo "nameserver 127.0.0.1" > /etc/resolv.conf

Then in your WireGuard config:

DNS = 127.0.0.1

This routes DNS through dnscrypt-proxy on localhost, which encrypts it and sends it through the VPN to a no-log resolver. It’s layered in a satisfying way.

Verifying DNS Isn’t Leaking

# Quick check — what DNS server is being used?
dig +short whoami.akamai.net @ns1-1.akamaitech.net

# Or use a public DNS leak test
curl -s https://ipv4.icanhazip.com
# Should show your VPN IP

# Check where DNS queries are going
dig example.com

# In the ANSWER section, look for the server used
# Compare it against what you configured

For a thorough test, use dnsleak.com or dnsleaktest.com from a browser — they run multiple DNS queries and show every resolver that responded.

Split Tunnel DNS (The Complicated Case)

If you’re running a split tunnel where only some traffic goes through the VPN, DNS gets complicated. You might want *.company.internal to resolve via the VPN’s DNS, and everything else via a public resolver.

With systemd-resolved:

# Route internal domain DNS through VPN interface
sudo resolvectl dns wg0 10.0.0.1
sudo resolvectl domain wg0 "company.internal"

# Default DNS for everything else
sudo resolvectl dns eth0 1.1.1.1
sudo resolvectl domain eth0 "~."

The ~. on eth0 makes it the fallback for all domains not matched by another interface’s domain list.

The Practical Summary

For a “good enough for most threat models” WireGuard setup:

  1. Use AllowedIPs = 0.0.0.0/0, ::/0 for a full tunnel
  2. Add the iptables kill switch rules to PostUp/PreDown
  3. Set DNS = 1.1.1.1 in your WireGuard config
  4. Configure systemd-resolved to respect the VPN interface
  5. Test by simulating a drop with ip link set wg0 down

If you want to go deeper: add dnscrypt-proxy for encrypted DNS, lock resolv.conf with chattr +i, and test with a proper DNS leak tester.

Paranoia is justified here. The whole point of a VPN is that your traffic doesn’t leak out the sides — and without these configurations, it absolutely does.


Share this post on:

Previous Post
Open WebUI vs LibreChat: Self-Hosted ChatGPT Alternatives Compared
Next Post
Authentik vs Authelia: Single Sign-On for Your Home Lab (Without a PhD)