Two clients on one server exchange files, the transfer succeeds, and every forward rule you point at the tunnel reports zero packets. The rules are not broken and the chain is not misplaced. With peer-to-peer forwarding enabled, the packet is delivered inside the server process and the host stack never becomes the router, so there is nothing for the kernel firewall to count. Understanding that split decides whether you keep the shortcut or rebuild the path through the kernel.

Follow one packet through the server

In a routed tunnel, the daemon hands each client's decrypted packet to the tunnel device. From there the host routes it, the forward hook runs, and a rule that matches the tunnel device can count or drop the traffic. That is the path most firewall examples assume.

Forwarding pathWho decidesWhat the host firewall sees
Kernel routing between clientsHost routing table and forward rulesEvery inner packet, with interface names
Peer forwarding inside the server processThe VPN daemon's own peer tableOnly the outer encrypted transport

The shortcut removes the host from the middle. One session's decrypted packet is handed to another session inside the daemon, and the inner packet is never emitted on the tunnel device or routed by the host. Encapsulated traffic still crosses the physical interface, so an outer capture looks busy while the inner rules stay silent. A capture taken on the tunnel device shows nothing at all, which surprises anyone who expects to watch the transfer there. That asymmetry is the quickest way to recognise the shortcut from outside the daemon.

The counter test that settles which path ran

Attach a counter to the rule that should apply, run a transfer between two clients that rule ought to block, and read the counter afterwards.

sudo nft list ruleset
sudo nft add rule inet filter forward iifname "tun0" oifname "tun0" counter accept
sudo nft list chain inet filter forward

If the transfer succeeds and the counter stays at zero, the kernel never handled those packets, and no amount of rule editing will change that. Repeat the same test against a service on the server itself, because that flow does traverse the host stack and its counter moves. Comparing the two results is what produces the answer.

Two details prevent a false reading. An interface name written into a chain label filters nothing on its own, because matching happens on interface fields inside rules. And a bridged layer-2 design behaves differently again: there the clients share a broadcast domain, so peer discovery is expected rather than a routing fault.

A switch is not an access list

This is the part that costs an afternoon. Peer forwarding is a single on or off setting, and while it is on the daemon is the only forwarding point. The host firewall cannot express who may reach whom, and per-client files do not turn the switch into a policy matrix.

If you need per-peer rules, the choice is between giving up the shortcut and giving up the host firewall as the control. Turning it off restores kernel forwarding, which requires IP forwarding on the server and explicit rules for the flows you want. Keeping it on means the policy lives in the daemon's configuration, which in practice is all peers or no peers.

Separate server processes with their own address pools and ports are the workable middle ground. Each process carries exactly one access decision, so two groups can have different rules without a policy matrix. The host can route between the pools when that is intended, and the firewall then sees those flows because they cross the host. The cost is extra ports, extra certificates and a profile per group.

Choosing the forwarding point

Pick kernel forwarding when the firewall is the policy engine, when you need logging per flow, or when accounting matters. The cost is throughput through the host stack and more rules to maintain. The benefit is that every existing tool, from counters to flow exporters, describes the traffic again. On a small server that cost is invisible; on a busy relay it is not.

Pick in-process forwarding when peer traffic is either all allowed or all denied and the server is sized for it. The cost is that counters, queueing disciplines and connection tracking no longer describe the inner flows; that is acceptable only if nobody later relies on them.

Whichever you choose, write it down next to the firewall rules. A ruleset that appears to permit peer traffic while the daemon decides it anyway misleads the next person who debugs a failed transfer. Anyone reading only the rules will assume the kernel is the gate.

What to check before changing rules

Read the configuration the server actually loaded, not the file you remember editing, and confirm whether the peer option is present. If you changed it, make sure the service reloaded rather than only rewrote a file on disk.

Then repeat the test with a client-to-server flow to prove the rule matches at all, and check the pushed routes, since a missing route looks exactly like a firewall block from the client side. Change rule order only after you know which forwarding point owns the decision.

Pennadoù liammet