Guests on the default libvirt network resolve names perfectly until the WireGuard interface comes up, and then every lookup stalls at the same place. dnsmasq is still listening on 192.168.122.1:53, the guest still has that address as its resolver, and the query still arrives. What changed is that the host's own upstream query, or the reply to it, now gets dropped by a rule that is evaluated before any filter chain you would think to edit. The way out is a mark applied early enough, on a connection the reply inherits.

Guests lose DNS the moment the tunnel comes up

Confirm the shape before changing anything. Bring the tunnel down, run dig @192.168.122.1 example.com from a guest, and watch it answer. Bring the tunnel up, repeat, and watch it time out. That pair of results rules out a broken libvirt network, a wrong resolver address, and a dead dnsmasq in one step.

Then look at the host side. If dnsmasq logs the query and its upstream forward, the packet arrived and the failure is on egress. If dnsmasq never logs anything, the guest query is being dropped on ingress and the exception belongs on a different hook than the one this article marks.

Why an accept rule in a later chain never fires

A mark-based VPN firewall decides by looking at the packet mark or the connection mark, not at your source address. Anything arriving on the physical interface without the expected mark is refused, and that refusal happens in the rule set that owns the mark test. An ip saddr 192.168.122.0/24 accept line added to some later chain is simply never consulted for that packet, which is why the obvious rule appears correct and changes nothing.

Widening it until something works is the second trap. Accepting the whole libvirt subnet in the host input path opens every host service to guests, when the actual need is one port.

The mechanism that makes a narrow exception possible is connection tracking. When you set ct mark on a packet, the value is stored on the conntrack entry rather than on the packet alone, so every packet in that connection carries it afterwards, including the reply from the host back to the guest. That is the whole trick: mark the request once, and the return path is already marked.

Mark the query before the VPN table sees it

Put the exception in its own table so a later reader can find it without parsing the VPN rules:

table inet allowedLocal {
  chain guest_dns {
    type filter hook input priority mangle; policy accept;
    ip saddr 192.168.122.0/24 meta l4proto { tcp, udp } \
      th dport 53 ct mark set 0x00000f41
  }
}

priority mangle is negative, which places this chain ahead of the ordinary filter chains the VPN package installs at priority zero. Hook order is what makes the rule effective; the same rule in a chain that sorts after the VPN chain is decoration.

The mark number itself is not portable. Take the value the installed VPN rules accept for their own traffic, read it out of the live ruleset, and use that. A wrong mark produces a rule that loads cleanly, matches every query, and admits nothing.

Keep the hole the width of port 53

As written, this permits DNS from the libvirt subnet to the host over TCP or UDP port 53 and nothing else. It does not let guests reach the internet outside the tunnel, and it does not let the host open connections toward guests. If a guest needs another host service later, add a separate rule naming that destination port instead of relaxing this one.

Be honest about what you are trading. A guest that can reach a resolver can also tunnel arbitrary data through DNS queries, and this exception is a deliberate gap in the VPN boundary rather than a statement that the local network is trusted. Restricting the source to 192.168.122.0/24 keeps the gap off every other local subnet, and counting queries against that source is cheap if the threat matters to you.

Make it survive reboot and prove both halves

A rule typed at a shell prompt disappears at the next boot, and a rule loaded after the VPN interface is already passing traffic loses the race on the first connection. Put the table in the host's persistent nftables configuration, or in a unit ordered before the VPN service, and check the ordering rather than assuming it.

Verification needs three separate answers. Confirm the chain is loaded and ordered first, confirm the conntrack entry shows the mark in both directions, and confirm a guest lookup returns an address. Then confirm the other half: ordinary guest web traffic to a public host still fails while the tunnel is down or blocking. If DNS works and everything else leaks, your rule is broader than the one above.

If lookups still fail after the mark is in place, resist the urge to widen the source. Check what dnsmasq forwards to, whether the guest resolver points at 192.168.122.1 or at a public address someone typed in, and whether the query reaches the host at all. Marking admits the packet and nothing else, so a bad upstream or a broken virtual network stays broken no matter how many rules you add.

Ingingo zihuye