Pointing a WireGuard client's DNS query at a different resolver looks like one rule and behaves like three decisions. The destination has to be rewritten, the packet has to be forwarded, and the answer has to find its way back through the translation state the gateway created. Skip the third and the query leaves cleanly while the client waits forever.

Take the example of authorised clients in 10.77.0.0/24 arriving on wg0, querying the advertised address 10.77.0.1, and needing a resolver at 203.0.113.53. The upstream address is a documentation placeholder rather than a working service.

Redirecting a query changes only its destination

A destination-NAT rule in PREROUTING rewrites the address before the routing decision is made, so the packet is then routed toward the new destination instead of the old one. The client's source address is untouched by that operation. This is why a rule that looks complete can still produce silence: the request now travels to a resolver that has never heard of the client's private range. The gateway translated one field and inherited a second problem.

Both DNS transports need matching rules, because a client falls back to TCP when a response is truncated.

The rules below select one incoming interface, one client range and one original destination address. Nothing else on the gateway is affected, and a query from the gateway itself does not match at all.

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -i wg0 -s 10.77.0.0/24 -d 10.77.0.1 -p udp --dport 53 -j DNAT --to-destination 203.0.113.53:53
iptables -t nat -A PREROUTING -i wg0 -s 10.77.0.0/24 -d 10.77.0.1 -p tcp --dport 53 -j DNAT --to-destination 203.0.113.53:53

The reply path is a separate decision

Two arrangements let the answer return, and picking one early saves a debugging session. Either the upstream resolver has a route back to 10.77.0.0/24 through this gateway, or the gateway translates the source on egress so the reply arrives at an address the upstream already knows. Both work; only one of them matches the control you have over the upstream.

Routing deserves the first attempt, because it keeps the client's address visible to the resolver and avoids a second translation layer. When the upstream is outside your control, source translation on the specific egress interface is the practical choice.

Scope that translation as tightly as the redirect, or the rule becomes a general-purpose NAT for anything that happens to match it:

iptables -t nat -A POSTROUTING -s 10.77.0.0/24 -d 203.0.113.53 -o eth0 -p udp --dport 53 -j MASQUERADE
iptables -t nat -A POSTROUTING -s 10.77.0.0/24 -d 203.0.113.53 -o eth0 -p tcp --dport 53 -j MASQUERADE

Connection tracking keeps the mapping paired, so the reply is rewritten back to the client's original tuple on the way in. Without the egress rule, the resolver sends its answer toward a private address it cannot reach, and the client sees a timeout rather than an error.

Write the rules narrower than the task looks

Forwarding permission belongs to the same scope. Permit the client-to-resolver flow and the return traffic that conntrack already recognises, rather than accepting every DNAT connection on the box. A blanket acceptance also authorises port forwards that have nothing to do with DNS.

The gateway's own resolver is a different code path. A query issued locally goes through OUTPUT, so testing from the gateway shell exercises neither the PREROUTING rule nor the forwarding policy. That is why a local test can pass while every client fails, and the reverse is just as common.

Nothing here covers DNS over HTTPS on port 443 or DNS over TLS on port 853, and nothing here covers IPv6. A client with an IPv6 resolver configured will happily ignore the whole arrangement.

What a fresh query proves

Connection tracking remembers earlier mappings, so a query sent before a rule change can be answered from the old state and make the new rule look broken. Start with a new source port and, where the client caches answers, a new name.

dig @10.77.0.1 example.com A +short
conntrack -L -p udp --dport 53 | head

The first command shows whether the client path resolves at all, and the second shows the translation the gateway actually installed, including both directions of the tuple. A mapping pointing at the right resolver with a reply counter stuck at zero is a return-path problem rather than a filter problem.

Confirm the resolver knows the names you expect before redirecting anything. A replacement that answers public names but not your private zone turns a working lookup into an NXDOMAIN that looks like a broken tunnel. Test the resolver directly, from the gateway, before the client path is involved. The iptables extension reference documents the DNAT and masquerade semantics these rules depend on.

Artaigilean co-cheangailte