WireGuard connects, your IP shows as home, and then you notice DNS queries still hit your ISP. A working tunnel isn’t a trustworthy VPN until you lock down routing, DNS, and a kill switch. Here’s how.

The Setup That Almost Works

A minimal config looks like this on the server:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>

[Peer]
PublicKey = <client-public-key>
AllowedIPs = 10.0.0.2/32

On the client, AllowedIPs = 0.0.0.0/0 makes WireGuard the default route for IPv4. If the client has IPv6 connectivity, use AllowedIPs = 0.0.0.0/0, ::/0 and configure IPv6 forwarding/routing on the server too; otherwise IPv6 can bypass an IPv4-only tunnel. After wg-quick up, a ping to 10.0.0.1 and an external IP check confirm routing, but you still need to verify DNS behavior and failure handling. If the tunnel drops without a kill switch, the normal physical-interface route becomes usable again.

Enable forwarding on the server. Without it, the server won’t route packets from the WireGuard interface to the outside world. For an IPv4-only tunnel, set this permanently in /etc/sysctl.conf:

net.ipv4.ip_forward=1

For an IPv6 full tunnel, also enable IPv6 forwarding and provide a valid routed or translated IPv6 path:

net.ipv6.conf.all.forwarding=1

Apply with sysctl -p, then verify both address families you intend to carry.

Routing Traffic Properly with iptables

To make the server a gateway, add NAT masquerading so return traffic finds its way back:

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

This rewrites the source address of packets leaving eth0 to the server’s own IP. Without it, reply packets destined for 10.0.0.2 get dropped by the first router that doesn’t know about your private subnet.

If the server's FORWARD policy does not already allow this path, permit tunnel clients out and only return traffic back in:

iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Do not use a broad -o wg0 -j ACCEPT rule unless you intentionally want unsolicited traffic from other interfaces to reach every tunnel client. Likewise, scope masquerading to the external interface; an unscoped POSTROUTING -j MASQUERADE can rewrite traffic on paths you did not intend.

Use PostUp and PostDown hooks to apply and remove these rules automatically. In the server’s [Interface] section:

PostUp = iptables -A FORWARD -i %i -o eth0 -j ACCEPT; iptables -A FORWARD -i eth0 -o %i -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -o eth0 -j ACCEPT; iptables -D FORWARD -i eth0 -o %i -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

%i expands to the interface name (wg0). The rules appear when the interface comes up and disappear when it goes down, keeping firewall state predictable.

The DNS Leak You’ll Miss

Even with a default route through the tunnel, a client can keep using a resolver learned from its local network. Whether those packets bypass the tunnel depends on the route and resolver configuration. A resolver outside the tunnel can expose DNS query names to that resolver and to networks on the path, although encrypted DNS and application-level behavior complicate what an observer can see.

Fix it on the client first. In the client’s [Interface] section, add:

DNS = 10.0.0.1

The upstream wg-quick manual defines DNS in terms of resolvconf: it adds the resolver when the interface comes up and removes it when the interface goes down. Distribution integrations differ. If NetworkManager or systemd-resolved owns DNS on your system, configure the WireGuard connection through that manager or use matching PostUp/PostDown commands such as resolvectl dns and resolvectl domain; verify the active resolver state instead of assuming the line was honored.

On the server, catch misconfigured or uncooperative clients by redirecting all outbound DNS traffic to a resolver you control. Any UDP or TCP packet on port 53 from the WireGuard subnet gets intercepted and sent to a local resolver like Unbound or a trusted public resolver.

Redirecting Plain DNS with iptables

The following DNAT rules can redirect conventional TCP/UDP DNS on port 53 from tunnel clients, ideally through PostUp:

iptables -t nat -A PREROUTING -i wg0 -p udp --dport 53 -j DNAT --to-destination 10.0.0.1:53
iptables -t nat -A PREROUTING -i wg0 -p tcp --dport 53 -j DNAT --to-destination 10.0.0.1:53

This assumes the server runs a resolver on 10.0.0.1. If you redirect to an external resolver, the server still sends that query over its own uplink. These rules cover ordinary DNS on port 53 only. They do not intercept DNS-over-TLS, DNS-over-HTTPS, DNS-over-QUIC, or an application that resolves names itself, so call this port-53 enforcement rather than a guarantee that every name lookup uses your resolver.

If you run a local resolver like Unbound, ensure it listens on the WireGuard interface. By default, many resolvers listen only on localhost. Edit /etc/unbound/unbound.conf:

interface: 10.0.0.1
access-control: 10.0.0.0/24 allow

Verifying No Leaks

Test with dig from the client, targeting a domain you haven’t visited recently:

dig example.com

On the server, run tcpdump -ni any port 53 while issuing a known test query. You should see the client-side packet arrive on wg0 and the resolver handle it. A resolver legitimately sends upstream queries on eth0, so outbound DNS on the server is not by itself a client leak. Also inspect the client’s routes and resolver state, and test both IPv4 and IPv6. Port-53 packet capture cannot detect application-level encrypted DNS.

Building a Real Kill Switch

A kill switch should reject ordinary packets that would leave outside the tunnel while still allowing WireGuard’s own marked UDP transport to reach the peer. The four-rule example often copied online is unsafe: its broad ESTABLISHED,RELATED exception can allow an already-established flow to continue over a physical interface.

The wg-quick manual’s kill-switch example uses WireGuard’s firewall mark and removes the exact rule during teardown:

[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 REJECT

That example is for IPv4. If the client can use IPv6, pair it with the equivalent ip6tables rules and include ::/0 in AllowedIPs:

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 REJECT

These rules intentionally permit local-destination traffic. If your threat model also forbids access to the local LAN, add narrowly tested rules for that requirement rather than assuming this example blocks it.

The Lockout Risk

The kill switch is sharp. A syntax error or missing teardown hook can cut off a remote machine. Test from local or out-of-band console access, bring the tunnel down deliberately, and confirm that PreDown removes the exact rule it inserted. Do not use a blanket iptables -F recovery command on a host with other firewall policy; remove only the test rule or use the host’s normal firewall rollback mechanism.

A network-layer kill switch prevents ordinary off-tunnel IP traffic, but DNS still deserves its own verification because resolvers, IPv6, and encrypted application DNS can behave differently.


A trustworthy WireGuard full tunnel needs more than a handshake. Routing, resolver configuration, and a client-side kill switch must cover every address family the client can use. Apply the rules deliberately, inspect routes and resolver state, test failure from a console you cannot lock yourself out of, and repeat the checks after network-manager or operating-system updates.