You add a rule for VPN traffic in the FORWARD chain, run the request from the same Linux box, and the counter stays at zero. The rule is probably correct. The packet never took that path, because a request generated on the machine itself is not forwarded by it. Walking the packet's life from the socket to the wire explains why, and where the rule belongs instead.

Find where the packet was born

Netfilter's hook order separates three cases, and only one of them involves FORWARD:

OriginPathHook that sees it
Local processOUTPUT then POSTROUTINGOUTPUT, POSTROUTING
Addressed to a local servicePREROUTING then INPUTPREROUTING, INPUT
Received for another hostPREROUTING, FORWARD, POSTROUTINGFORWARD

A local application starts in the OUTPUT path, because the kernel has already established that the packet originates here and is never a candidate for forwarding. Creating a tunnel does not change that classification; the tunnel is a device the packet may leave through, not a stage that turns local output into transit.

Two packets, one story

Once the tunnel is up, the application's inner packet and the encrypted outer packet are different objects. The inner packet carries the destination and payload the app chose. The outer packet carries ciphertext to the peer's address and port, and it is what actually crosses the physical interface.

Your policy concerns one of them, and picking the wrong one is a common reason a rule appears to do nothing. A rule matching the peer's outer address sees every tunnel packet equally, whatever the application inside is doing. A rule matching an inner destination sees the traffic you care about, and it will only fire if that packet reaches the hook you installed the rule on.

Namespaces move the boundary

A container makes this harder without changing the principle. The application's request is local output inside the container's network namespace. The host receives that traffic on a virtual Ethernet interface and may forward it, so a FORWARD rule on the host can be meaningful even though the process that generated the request never touched the host's OUTPUT chain.

Before choosing a hook, establish where the ruleset and the tunnel actually live. A tunnel inside the container, a tunnel on the host with the container routed through it, and a tunnel in a separate namespace are three different topologies with three different rule placements. Read the rules inside the namespace that owns the device, not just in the default one.

Ask whether you need a mark at all

If the goal is to send one destination prefix through the tunnel, a route for that prefix may be enough. Check the effective routes first, including anything the server pushed: a redirected default sends far more traffic through the VPN than the narrow policy you had in mind, and no amount of marking will make the result match the intent.

Classification is needed when the decision depends on something a route cannot express, such as the source, the user, or the process. For received traffic, marking in PREROUTING works. For local traffic, the marking has to happen in an output stage that can trigger a new route lookup after the mark is set. With nftables that is a route-type chain on the output hook; an ordinary output filter chain will set the value and leave the route unchanged.

With iptables, the equivalent lives in the mangle OUTPUT arrangement, and the same requirement applies: the routing decision must be revisited after the mark exists. Then a policy rule has to consume the mark and point at a table containing a usable route. Setting a conntrack mark is a third, separate operation; it stores a value on the flow and does not by itself put that value on the current packet for routing to read.

Verify the lookup and the counter together

nft list ruleset
ip route get 198.51.100.25
ip route get 198.51.100.25 mark 0x1

The address and mark are illustrative, and the two route queries answer different questions. The second asks what the kernel would do if a packet carried that mark; it does not show that your ruleset assigns one. Treat it as a hypothesis test, not as evidence that the policy works.

Then run a harmless request and watch two things at once: the counter on the hook you chose, and the egress the packet actually took. If the counter moves and the packet leaves elsewhere, the mark or the table is wrong. If neither moves, the packet is not reaching that hook at all, and the namespace or the path is the next question.

Finally, scope the test to what the mechanism can express. One route for one address does not become a permanent policy for a service whose addresses rotate. Decide the packet scope first, then check that the routing and marking design can hold that scope after a reconnect.

Аффингæй хæстæг статьялтæ