The usual loop protection is a host route: send traffic for the VPN server's public address out the uplink so the encrypted session never enters its own tunnel. That works right up to the moment you also want a web service on that same server reached through the tunnel, because the exemption is keyed on the destination and the destination is identical. The way out is to stop classifying by destination and start classifying by which socket sent the packet.

Start from the two flows you want to separate

One flow is the encrypted OpenVPN transport itself, which has to leave by the uplink or the tunnel would carry its own outer connection. The other is an ordinary HTTPS request to the same public address, which you want inside the tunnel so it arrives at the service from a tunnel source. Same destination, same port number in many cases, different required path.

For this walkthrough, assume a client on 10.90.0.10 over tun1, a server public address of 198.51.100.25, and an untouched main table whose default points at the ordinary uplink. The server side also has to accept the web service on the inner path and return traffic to the tunnel source; sending a packet to a public address does not override the server's listener or firewall policy.

Before changing anything, capture the host's current rules and pick values nobody else is using: a mark, a table number and three rule priorities. Keep an out-of-band way back into the machine, because a mistake here removes your own remote access rather than just breaking a browser tab.

Mark the socket that carries the envelope

The client configuration gains a mark for its encrypted outgoing packets, and stops installing routes on its own so the replacement policy is the only thing deciding paths:

mark 42
route-noexec
disable-dco

Decimal 42 is hexadecimal 0x2a, and the hex form is what the routing rules will match. The link-options reference describes mark as applying to encrypted outgoing packets on Linux, which is exactly the socket whose path you need to control. Disabling the data-channel offload keeps this example on the userspace path, where the mark behaviour is the one being described.

route-noexec suppresses OpenVPN's automatic route additions and removals. It does not install anything in their place, so from this moment the route owner has to maintain every route the tunnel needs. If a private destination disappears after the change, that is the missing replacement policy, not a fault in the mark.

Do not keep a main-table host route for 198.51.100.25 while you do this. Such a route reintroduces exactly the exemption the design is trying to narrow, and it will win for the HTTPS flow before any of the policy below is consulted.

Send everything else to the tunnel table

Once the tunnel interface exists and holds its address, the policy looks like this:

ip -4 route add default dev tun1 src 10.90.0.10 table 47100
ip -4 rule add priority 20000 lookup main suppress_prefixlength 0
ip -4 rule add priority 20001 not fwmark 0x2a lookup 47100
ip -4 rule add priority 20002 not fwmark 0x2a unreachable

The first rule keeps the host's specific routes visible, including any approved local network, while making main's default route invisible to this lookup. That distinction matters: suppressing the default is not the same as ignoring the table, and the local exceptions you still need survive because of it. Read those exceptions one by one rather than assuming the result is a universal kill switch.

Unmarked traffic then falls to the tunnel table, picks up the tunnel source, and leaves through the VPN. Marked traffic fails the fwmark test on the next two rules and continues to the ordinary main lookup further down the list, where the uplink default still lives. The terminating unreachable rule is what stops an unmarked flow from quietly falling back to the uplink when the tunnel route is missing. The selector is the mark from beginning to end, and the destination address never enters the decision.

Keep that guard in place across disconnects, and install it with the rest of the policy rather than by hand. A rule that vanishes with the interface is not a policy, it is a coincidence. Deleting the policy when the tunnel drops changes the failure mode from "blocked" to "leaked", and a stale tunnel route timing out later is a much easier problem to explain than traffic that left by the wrong interface.

Make the reverse path see the same mark

Outgoing marks are only half the mechanism, and this is the part most reproductions omit. Replies to the encrypted session arrive on the uplink and are subject to reverse-path validation, which asks whether the source would route back the way it came. Without help, that lookup applies your unmarked tunnel policy and concludes the packet is on the wrong interface.

The fix belongs to the firewall owner: save the mark on the outbound connection, restore it onto matching inbound packets, and enable source-validated mark handling for the relevant path. OpenVPN setting a socket mark does none of those three things, and a recipe that lists only the client options is incomplete. Ask for that integration in writing, because it is the least visible part of the design and the easiest to lose in a rebuild.

Loose reverse-path checking is not a substitute either. Loose mode still needs some route to exist for the source, and the unreachable guard you added deliberately removes one for unmarked lookups. Have the connection-mark integration finished and reviewed before you rely on this design, and treat host filters as a separate question: a mark is a routing signal, not permission. If the integration is missing, the symptom is a session that establishes and then goes quiet as soon as replies start arriving.

Prove it with two route lookups

Compare what the kernel decides for the two contexts, and read the dev and src fields rather than the exit status:

ip -4 route get 198.51.100.25
ip -4 route get 198.51.100.25 mark 0x2a

The first lookup should resolve to tun1 with the tunnel source, and the second to the uplink. Those are synthetic lookups, so they prove the policy and not the live process: confirm the running client's actual transport path separately, then make a real HTTPS request with certificate verification left on.

Write down what the design does not cover before declaring it finished. DNS, IPv6 and the lifecycle of the rules each need their own decision, and this is an IPv4 routing example rather than a complete privacy configuration. Its value is the selector: the envelope gets the uplink exception, and an ordinary service at the same address does not have to inherit it.

Mikanda mikatakata