One host service has to answer the internet over the physical uplink while everything else on the machine keeps using the tunnel. Three conditions decide whether policy routing can carry that load, and each one is cheap to check before you write a rule. The service has to terminate on this host rather than behind a DNAT or a forwarded port, the VPN's own rules have to permit the exception, and you have to match the reply instead of the request. Fail any of the three and the commands below load cleanly while doing nothing useful.

Check the three conditions before you write a rule

A container publishing a port through DNAT, or a server sitting behind a port forward, produces packets that do not originate on this host. Those packets are forwarded, so a rule keyed on host-local input never sees them and the traffic keeps taking the default route. Confirm where the service actually runs before continuing, because this is the condition people get wrong most often.

The second condition is policy rather than mechanics. Any route that bypasses the tunnel is an exception to the tunnel's purpose, and on a managed host that decision belongs to whoever owns the policy, not to the person debugging a timeout.

Match the reply, not the client's destination port

A reply from a server listening on TCP 443 leaves the host with source port 443. A browser on the same host connecting to a remote HTTPS site sends an ephemeral source port and destination port 443. Both are port-443 traffic and only one of them should take the WAN table, which is why a destination-port match would grab the wrong half.

The reply direction is what you can distinguish. Match the source port, restrict the rule to locally generated packets, and the rule covers the service's own answers without touching the browser's requests.

Build a complete return table before the rule points at it

An empty table means no route, which looks like a broken service rather than a broken policy. Fill it first, using a table number and priority that nothing else on the host claims:

ip -4 route add table 100 192.0.2.0/24 dev eth0
ip -4 route add table 100 default via 192.0.2.1 dev eth0
ip -4 rule add priority 1000 iif lo ipproto tcp sport 443 lookup 100

Read the numbers off the live host instead of copying them. ip -4 rule show tells you which priorities are taken, and adding a rule at a priority that already exists silently changes the order of two rules that both look fine on their own.

Two selectors do the real work here. iif lo is how the kernel reports packets the host generated itself, so the rule cannot accidentally capture forwarded traffic arriving from outside. ipproto tcp sport 443 narrows it to one transport and one port, which is as tight as the requirement allows. Drop the protocol selector and the rule also grabs UDP 443, which is a different service with different behaviour.

Keep private replies inside the tunnel

If the same listener also answers clients on the private side, a catch-all WAN default in table 100 will try to return those replies through the uplink. Add the private prefixes that must not follow the WAN default, or make them fall through to the next policy rule:

ip -4 route add table 100 throw 10.90.0.0/24

A throw route ends the lookup in this table and hands the packet back to the rule list, so the VPN policy that follows gets its chance. It does not itself route anything into the tunnel, and if no later rule has a matching route the packet still fails; verify the next policy rather than assuming the throw is a fix.

Reverse-path filtering and host firewall rules still apply to the arriving request. Look at their real verdicts before loosening validation, because a routing exception does not make rp_filter accept a return path it considers impossible.

Test TCP, then QUIC, then a reboot

Open one connection from an authorised external client and one from the private side, then inspect the reply lookup with the actual addresses and ports. If the external reply takes table 100 and the private reply does not, the policy is doing what you asked.

HTTP/3 is served over UDP and will not be matched by a TCP rule, so a QUIC listener needs its own selector or it will keep using the tunnel while HTTPS works. Persist whichever rules you settle on with the component that owns the interface, and confirm them after a reconnect or a DHCP renewal. When a competing manager removes a runtime route after a reboot, that is an ownership problem to fix at the source, not a reason to add broader NAT rules on top.

פֿאַרבונדענע אַרטיקלען