A mark set in nftables is supposed to send one class of traffic through the tunnel, and the traffic keeps leaving by the physical interface. The ruleset looks correct in isolation. What usually happened is that something else wrote the same mark, or the routing rule matches a mask the new value no longer satisfies.

Check 1: read the rule the kernel matches

Start with the policy rule rather than the firewall. The kernel consults rules in priority order and the first match decides the table. Read the fwmark value and mask from the rule itself, then confirm that a packet with the mark you set actually matches it. A rule expecting a full 32-bit value will not match a packet carrying only a single bit.

Then confirm which table that rule selects and what the table contains. A rule pointing at an empty table produces a lookup failure, which on many systems means the packet falls through to the next rule and out of the physical interface.

Two commands settle this: the rule list, and the route lookup for the destination in that table.

Check 2: packet mark or connection mark

nftables can set a mark on the packet and a mark on the connection-tracking entry, and they are different objects. The packet mark lives for the packet's processing pass, while the connection mark persists for the flow and has to be copied deliberately. Setting one and expecting the other is a common half-fix.

If the routing rule keys on the packet mark, restoring the connection mark later in the path does nothing for that decision, because the decision already happened.

Decide which object owns the routing signal, then set it at the point where the routing decision is made.

Check 3: who else writes that mark

WireGuard's own tooling marks its UDP packets so the encrypted transport does not get routed back into the tunnel it belongs to. If your ruleset assigns an unrelated value to the same field, that exclusion disappears and the tunnel's outer traffic tries to enter itself. The symptom is a tunnel that establishes and then stops passing traffic.

Marks are also used by other firewall tooling, by container networking and by policy routing for other purposes, so two subsystems writing the whole field is a conflict even when each is individually correct.

List the writers before adding another one.

Check 4: the mask decides what you keep

A mark is a 32-bit field, and masks are how multiple owners share it. Setting a value with a mask preserves the bits you do not own; setting the whole field destroys everyone else's. Read the mask in every rule that touches the field, including the ones you did not write.

Document the bit allocation next to the ruleset.

The next person will otherwise reuse a value that already has an owner, and a conflict here looks like a routing bug while being an addressing bug. The nftables documentation on packet metadata describes the mark operations themselves.

Check 5: verify with a real flow, not a lookup

A plain route lookup does not carry a mark, so it can show a different path from the real packet. Generate the traffic you care about and observe the counters, the conntrack entry and the interface it actually leaves. Test both the included class and the excluded class, because a mark that is too broad breaks the exclusion while looking successful. If the included class works and the excluded class also enters the tunnel, the mark is too wide rather than too narrow.

Avoid flushing connection state to force a refresh during a live session, since new and existing flows can legitimately take different paths.

Record the mark, the mask and the table in one place, and the next edit becomes reviewable.

Qillqakuna t'inkisqa