The LAN client reaches its VPN correctly and its DNS queries still appear on the wrong uplink. That is not a leak in the usual sense; it is what happens when the router stops forwarding a query and answers it itself. Interception rewrites the destination to a local socket, and the resolver then opens a second, unrelated conversation with its own source address and its own routing decision. Marks applied to the client's flow describe the first conversation and nothing at all about the second.

The Mark Does Not Travel With the Query

Packet marks live on a conntrack entry and on the packet in flight. A REDIRECT rule ends that flight: the payload is handed to a local socket, and the original entry has done its job. What leaves the router afterwards is a fresh socket created by the resolver process, sourced from the router's own address.

That is why a policy route keyed on the LAN client's address never matches the upstream query. The new socket belongs to dnsmasq rather than to the client, and the kernel evaluates the route for the router's own traffic. Source rules, interface rules and mark rules all see a different tuple. Assuming the mark propagates is the most common mistake in this setup, and it is invisible in a client-side capture.

The two conversations do share a name and a timing, which is what makes them look like one flow when you skim a capture.

Where Forwarding Turns Into Local Resolution

Start by finding whether port 53 is redirected, translated to another host, or left alone, because the three cases behave differently. A redirect to a local port ends forwarding, while a translation to another machine keeps the packet moving and keeps the client's tuple meaningful.

Look at both protocols. A resolver reachable over UDP and TCP will be tested over both, and a rule set covering only UDP leaves a quiet hole for large answers. Rules generated by the router's own configuration manager count as well, and editing a generated file usually loses to the next reload.

sudo nft list ruleset | grep -n -B2 -A4 'dport 53'
sudo iptables -t nat -L -n -v | grep -E ':53|dns'
sudo ss -lunp | grep ':53'

Those outputs answer three questions in order: what rewrites the query, what receives it, and which process owns the listening socket. Once the owner is known, the upstream policy has somewhere to live.

Give the Resolver Its Own Egress Policy

A domain-specific upstream is the cleanest tool when only some names need the tunnel. dnsmasq accepts a server line scoped to a domain, with an optional interface or source address that steers the query out of a chosen device. That steering is what makes the second conversation follow the VPN.

server=/corp.example/10.30.0.53@tun_work
log-queries

The domain, address and interface above are placeholders for your own values. Domain selection and egress control remain separate decisions: the route must exist, the source address must be permitted by the upstream resolver, and the firewall must allow the reply. Interface steering alone is not fail-closed, because a vanished tunnel can leave the resolver falling back to another path. Keep any public fallback away from a namespace that has to stay private.

Put the line in the configuration owner the router supports rather than in a generated file, or the next reload erases the fix.

What a Shared Cache Cannot Preserve

One forwarder with one cache serves every LAN client, and that collapses a distinction some networks need. If two clients must resolve the same name differently, the shared socket has no way to recover which client asked. Per-client policy has to live in separate resolver instances or in a forwarding feature that keeps the identity.

A cached answer is also not evidence about the new path. The response can arrive from memory while the upstream route stays broken.

Port-53 rules say nothing about applications that speak DNS over HTTPS or over TLS. Those queries never reach the redirect, so a policy built only on port 53 describes a subset of the traffic and should be documented as a subset.

Verify the Second Conversation, Not the First

Test with one name you control and watch both sides. A client-side capture proves the query left the LAN; only the router side proves where it went next.

sudo tcpdump -ni tun_work udp port 53
ip route get 10.30.0.53
dig +short corp.example @10.30.0.53

Flush the cache before each attempt, because a warm answer hides a broken upstream. Repeat over TCP, then repeat with the tunnel down to see what the resolver does when its preferred path is gone. That last behaviour decides whether the design is fail-closed or merely lucky.

ХӀокхунах тера йаззамаш