One rule can keep an admitted VPN pool away from a destination range without disturbing anything else the gateway does. The rule itself is short; the parts that decide whether it works are the hook it lives in and its position in the chain. Getting those two wrong produces a firewall that looks correct and behaves inconsistently. Start from what the packet is doing when the rule sees it.

What the Rule Actually Matches

The rule matches three things at once: an input interface, a source prefix and a destination prefix. All three must be true before the drop applies, which is what keeps it narrow. Reading it as one expression is easier than reading it as three separate policies.

The interface match is what distinguishes forwarded client traffic from the gateway's own connections. Without it, a rule naming only the source pool would also affect packets the gateway itself generates from that address, if it ever does. The source prefix confines the policy to admitted clients, and the destination prefix confines it to one range. Read the rule as an intersection rather than as a list.

Write the three values down before editing anything: the tunnel interface name, the pool the server hands out, and the prefix you intend to deny. A typo in any of them produces a rule that parses cleanly and protects nothing. Keep those three values in the change record beside the rule.

The Hook Decides Who the Rule Applies To

A netfilter chain is attached to a hook, and the hook decides which packets the chain ever sees. The forward hook sees traffic routed through the gateway, which is what client-to-destination traffic is. Everything else in the chain is aimed at a different audience.

The input hook sees traffic addressed to the gateway itself, so a rule placed there governs access to its own services rather than transit. The output hook sees traffic the gateway originates, which is the opposite of what a client-pool policy needs. Chain names are arbitrary labels chosen by whoever wrote the ruleset, so do not infer the hook from the name. Check where the chain is attached before assuming what it can match.

HookTraffic it seesRight for this policy
inputAddressed to the gatewayNo, that is the host's own services
forwardRouted through the gatewayYes, for client-to-destination traffic
outputGenerated by the gatewayNo, that is the opposite direction

Reading the attachment is a one-line check that prevents a rule from landing in a chain that never sees the packet.

Position the Rule Before the Broad Accept

Rules are evaluated in order, and the first verdict wins. A broad accept earlier in the chain ends evaluation for the packets it matches, so a drop appended afterwards never runs for them. That ordering rule is the whole reason a correct-looking rule can have no effect.

Insert the rule ahead of the accept rather than appending it to the end. If the chain already contains a general accept for the tunnel interface, the new rule must precede it or it is decorative. Check the resulting order rather than the file you edited, because a ruleset assembled from several files may not match any single one. Then confirm with a test packet from the pool.

sudo nft list chain inet filter forward
sudo nft insert rule inet filter forward iifname "tun0" ip saddr 10.90.0.0/24 ip daddr 198.51.100.0/24 drop
sudo nft -c -f /etc/nftables.conf

The third command parses the file without applying it, which catches syntax errors before they become an outage. Apply with a rollback plan, since a mistaken rule in a forward chain can cut the tunnel you are working over.

Existing Flows Do Not Care About Your New Rule

A conntrack entry created before the rule existed continues to match the earlier acceptance. That is why a new drop sometimes appears to work for fresh connections and to do nothing for the one being tested.

Read the state match at the top of the chain. If it accepts established and related traffic before your rule, every flow already in progress keeps flowing until it closes. That is usually the behaviour you want for an emergency rule, and it is confusing when you expect an immediate cut. Decide which effect you want before deploying. A test that reuses an open connection will tell you the wrong story.

sudo conntrack -L -s 10.90.0.14 -d 198.51.100.20
sudo nft list ruleset | grep -n -A3 'ct state'

The first command shows whether a flow exists, and the second shows whether an early state rule keeps accepting it. Both are read-only and safe to run during an incident.

What This Rule Is Not

The rule is not a domain blacklist, an IPv6 policy, or a restriction on an individual application account.

Each of those needs its own mechanism. A domain block has to resolve names, which a packet filter does not do. IPv6 traffic needs its own match, because an ip expression says nothing about ip6 packets. An application-level restriction belongs to the service or to an authenticated proxy rather than to a prefix match. Keeping those requirements separate is what stops one narrow rule from being asked to do four jobs.

The contract this rule delivers is precise: traffic from an admitted pool to one destination prefix is denied, while unrelated gateway traffic keeps its existing policy. Write that sentence beside the rule, and the next person will know what it was for.

ບົດຄວາມທີ່ກ່ຽວຂ້ອງ