Two client pools, one gateway, and the plan looks trivial: one rule sends the first pool out the fast uplink, another sends the second pool through a partner tunnel. Then the rules are in place and the second pool still leaves by the default route. The rule is not broken. The packet's source is not what the author expected at the moment the lookup happens, because translation and lookup occur at different points in the same packet's journey, and the order is the opposite of the mental diagram most people draw.

The lookup happens before the source is rewritten

For a packet forwarded from a VPN client, the kernel runs a fixed sequence. It arrives on the tunnel interface, passes prerouting where conntrack and DNAT live, then the routing decision is made, then forwarding, then postrouting, where SNAT is applied last. A from rule is consulted during that routing decision, which means it sees the client's original address, not the translated one.

So adding a masquerade rule does not change which policy matches the outgoing flow. What it changes is what the far end sees, and therefore which return path exists. Those are two different consequences, and change reviews conflate them constantly. Separate them and the design usually becomes obvious within a minute.

The reply direction is where the translated address shows up in the rules. A packet coming back to the gateway is addressed to the translated address, and if you have policy rules keyed on destination or on the interface it arrived from, that is the value you will be matching. Same connection, different source of truth in each direction.

StageSource the kernel seesWhat it means for your rule
PreroutingClient address, 172.31.7.42Where marks and DNAT are applied
Routing decisionStill 172.31.7.42This is where from rules match
PostroutingBecomes 198.51.100.9What the far end and its return route see

Two pools with different sources therefore do need different tables, and the design is sound. What the table above rules out is the shortcut of debugging by looking at what the remote site received and inferring the rule from it.

A test from the gateway is not a test of the pool

Traffic the gateway generates itself never passes through prerouting, because it did not arrive on an interface. It is routed in the output path, with the gateway's own address as the source, and a from rule keyed on a client range will not match. Every "it works from the box but not from the client" ticket in this area is that fact restated.

Simulate the client instead of approximating it. The routing lookup can be asked directly, with a source and an ingress interface supplied:

ip route get 198.51.100.9 from 172.31.7.42 iif tun0
ip route get 198.51.100.9 from 172.31.8.42 iif tun0
ip route show table 201

Run it once per pool. If the two commands return the same interface, the rules are not separating the pools, and the reason is either a missing rule or a higher-priority rule that matches both. If they return different interfaces but the client still fails, the fault is downstream of the lookup and you can stop looking at rules.

Priority, not position in the file

Rules are evaluated in ascending priority order, and the number is what matters, not the order you typed them. The local table sits at priority 0, the main table at 32766 and the default rule at 32767, which is why a rule added with the default priority lands near the end of the list and loses to almost everything.

ip rule show
ip rule add from 172.31.8.0/24 lookup 201 priority 1000
ip -4 route show table 201

An explicit priority makes the intended order visible in the file and in review. It also prevents a later addition from silently jumping ahead of an existing rule, which is the most common cause of a working policy that stops working after an unrelated change.

A matching rule pointing at an incomplete table is the other trap. If table 201 contains a default route but no route to the destination, the packet is dropped or sent to the wrong place, and the rule looks perfectly correct in isolation. Read the table, not just the rule. A rule is a pointer, and an empty table is a pointer into nothing.

The return path still has to exist

Source policy chooses an exit. It does not create a return path. If the second pool leaves through a partner tunnel, the partner has to know how to reach 172.31.8.0/24, either by a route back to the gateway or by receiving translated addresses it already knows.

Choosing between those is a design decision with visible consequences. Routing the client range back preserves per-client identity at the partner and requires them to accept your prefix. Translating hides the pool behind one address and requires the partner to accept that address only, at the cost of your logs and theirs no longer agreeing about who connected.

Whatever you choose, write it down next to the pool definition. A return route that exists only in someone's memory is exactly the sort of thing that survives until the partner changes a firewall, and then takes a day to reconstruct.

Do not catch the gateway's own tunnel in the policy

The rules you write for clients also apply to traffic the gateway generates, unless you exclude it. A broad rule keyed on a mark or on the output interface can capture the gateway's own connection to the partner, and then the tunnel that carries the policy is routed by the policy. That produces a loop or an outage that looks like a provider fault.

Scope each rule as narrowly as the design allows: source prefix, ingress interface, and where supported a mark that only client packets carry. Then test the gateway's own connectivity after every rule change, because the failure is self-referential and does not show up in a client test.

The ip-rule manual page documents the selector and priority semantics. Combine it with a per-pool lookup test and the design becomes checkable, which is the difference between policy routing that survives a year and policy routing that everyone is afraid to touch.

ХӀокхунах тера йаззамаш