# In one terminal, watch DNS on your physical NIC
-weight: 600;">sudo tcpdump -i wlan0 -n 'udp port 53 or tcp port 53' # In another terminal, trigger a fresh lookup
# Use a unique domain so cached answers don't hide the issue
dig $(uuidgen | tr A-Z a-z).example.com
# In one terminal, watch DNS on your physical NIC
-weight: 600;">sudo tcpdump -i wlan0 -n 'udp port 53 or tcp port 53' # In another terminal, trigger a fresh lookup
# Use a unique domain so cached answers don't hide the issue
dig $(uuidgen | tr A-Z a-z).example.com
# In one terminal, watch DNS on your physical NIC
-weight: 600;">sudo tcpdump -i wlan0 -n 'udp port 53 or tcp port 53' # In another terminal, trigger a fresh lookup
# Use a unique domain so cached answers don't hide the issue
dig $(uuidgen | tr A-Z a-z).example.com
# systemd-resolved -weight: 500;">status, per-interface
resolvectl -weight: 500;">status # Classic view
cat /etc/resolv.conf # What's actually being asked, in real time
-weight: 600;">sudo resolvectl monitor
# systemd-resolved -weight: 500;">status, per-interface
resolvectl -weight: 500;">status # Classic view
cat /etc/resolv.conf # What's actually being asked, in real time
-weight: 600;">sudo resolvectl monitor
# systemd-resolved -weight: 500;">status, per-interface
resolvectl -weight: 500;">status # Classic view
cat /etc/resolv.conf # What's actually being asked, in real time
-weight: 600;">sudo resolvectl monitor
[Interface]
PrivateKey = <your-private-key>
Address = 10.0.0.2/24
# Use a resolver that lives on the VPN side
DNS = 10.0.0.1 [Peer]
PublicKey = <peer-public-key>
Endpoint = vpn.example.com:51820
# Route everything, including DNS
AllowedIPs = 0.0.0.0/0, ::/0
[Interface]
PrivateKey = <your-private-key>
Address = 10.0.0.2/24
# Use a resolver that lives on the VPN side
DNS = 10.0.0.1 [Peer]
PublicKey = <peer-public-key>
Endpoint = vpn.example.com:51820
# Route everything, including DNS
AllowedIPs = 0.0.0.0/0, ::/0
[Interface]
PrivateKey = <your-private-key>
Address = 10.0.0.2/24
# Use a resolver that lives on the VPN side
DNS = 10.0.0.1 [Peer]
PublicKey = <peer-public-key>
Endpoint = vpn.example.com:51820
# Route everything, including DNS
AllowedIPs = 0.0.0.0/0, ::/0
# In your client config
dhcp-option DNS 10.8.0.1
block-outside-dns # Windows-only, blocks leaks aggressively
script-security 2
up /etc/openvpn/-weight: 500;">update-resolv-conf
down /etc/openvpn/-weight: 500;">update-resolv-conf
# In your client config
dhcp-option DNS 10.8.0.1
block-outside-dns # Windows-only, blocks leaks aggressively
script-security 2
up /etc/openvpn/-weight: 500;">update-resolv-conf
down /etc/openvpn/-weight: 500;">update-resolv-conf
# In your client config
dhcp-option DNS 10.8.0.1
block-outside-dns # Windows-only, blocks leaks aggressively
script-security 2
up /etc/openvpn/-weight: 500;">update-resolv-conf
down /etc/openvpn/-weight: 500;">update-resolv-conf
network.trr.mode = 5 // Off by user choice; do not use DoH
network.trr.mode = 5 // Off by user choice; do not use DoH
network.trr.mode = 5 // Off by user choice; do not use DoH
// Force cgo-based resolution which respects /etc/resolv.conf changes
// done by the VPN client. The pure-Go resolver has caching that
// can outlast a VPN session change.
import _ "net" // Or via environment
// GODEBUG=netdns=cgo+2
// Force cgo-based resolution which respects /etc/resolv.conf changes
// done by the VPN client. The pure-Go resolver has caching that
// can outlast a VPN session change.
import _ "net" // Or via environment
// GODEBUG=netdns=cgo+2
// Force cgo-based resolution which respects /etc/resolv.conf changes
// done by the VPN client. The pure-Go resolver has caching that
// can outlast a VPN session change.
import _ "net" // Or via environment
// GODEBUG=netdns=cgo+2
# nftables: block UDP/53 and TCP/53 on the physical interface
-weight: 600;">sudo nft add table inet vpn_guard
-weight: 600;">sudo nft add chain inet vpn_guard output { type filter hook output priority 0 \; }
-weight: 600;">sudo nft add rule inet vpn_guard output oifname wlan0 udp dport 53 drop
-weight: 600;">sudo nft add rule inet vpn_guard output oifname wlan0 tcp dport 53 drop
# nftables: block UDP/53 and TCP/53 on the physical interface
-weight: 600;">sudo nft add table inet vpn_guard
-weight: 600;">sudo nft add chain inet vpn_guard output { type filter hook output priority 0 \; }
-weight: 600;">sudo nft add rule inet vpn_guard output oifname wlan0 udp dport 53 drop
-weight: 600;">sudo nft add rule inet vpn_guard output oifname wlan0 tcp dport 53 drop
# nftables: block UDP/53 and TCP/53 on the physical interface
-weight: 600;">sudo nft add table inet vpn_guard
-weight: 600;">sudo nft add chain inet vpn_guard output { type filter hook output priority 0 \; }
-weight: 600;">sudo nft add rule inet vpn_guard output oifname wlan0 udp dport 53 drop
-weight: 600;">sudo nft add rule inet vpn_guard output oifname wlan0 tcp dport 53 drop - systemd-resolved keeps per-link DNS configurations and may continue using the original interface's DNS even when traffic is routed elsewhere.
- Browsers with DNS-over-HTTPS (Firefox, Chrome) bypass the OS resolver entirely and talk directly to a hardcoded DoH endpoint over HTTPS — which is tunneled through the VPN, but goes to a third party you may not trust.
- Applications using their own resolvers — Go binaries with GODEBUG=netdns=go, some container runtimes, and language-specific resolver libraries can ignore system settings. - Test the leak path every time you change network config. Don't trust that the previous setup still works after a kernel -weight: 500;">update or VPN client -weight: 500;">upgrade.
- Prefer kill-switch behavior — drop all non-VPN traffic at the firewall when the tunnel is down. Most modern VPN clients support this; if yours doesn't, use nftables.
- Standardize DNS at the tunnel exit. Run an unbound or dnsmasq instance on the VPN server so you control the resolver path end to end.
- Audit application-layer resolvers. Browsers, container runtimes, and language standard libraries each have their own DNS quirks. Document them per project.
- Run a periodic automated leak test. A daily cron job that runs dig against a unique subdomain and checks your authoritative server's logs for the source IP works well.