Account-based routing fits when every IPv4 socket owned by one Linux user should leave through an existing tunnel, and it misfits when you need program-level isolation. It selects by UID, so anything running as that account is in scope, including the batch job you forgot. Settle that question before you touch a single ip rule.

The example below assumes a dedicated browser account at UID 1500, a tunnel device tun-office holding local address 10.80.0.11, and a VPN that permits the destinations in question. The daemon itself runs outside that UID.

Three conditions that pick the mechanism

Choose a UID rule when the identity doing the work is the account, when moving all of its traffic is acceptable, and when a namespace would be heavier than the task deserves. Choose a network namespace when the workload needs its own interfaces, resolver and routing table. Choose a proxy or a per-process bind when exactly one application should move while the rest of the session stays on the local path. Writing those three down prevents a rule that quietly covers more than intended.

The mistake to avoid is assuming a launcher defines the owner. A browser handed a URL by another user's session has not created sockets under UID 1500, and no rule will move the traffic it generates.

The table, the rule and the guard

After checking existing rules and picking unused table and priority numbers, the arrangement is short: a default route in a dedicated table carrying the tunnel's source address, a rule that sends the account's lookups to that table, and a terminating rule that blocks the fallback. Written out, that is an idempotent route replacement, ip -4 route replace default dev tun-office src 10.80.0.11 table 51820, followed by ip -4 rule add priority 12100 uidrange 1500-1500 lookup 51820.

The guard is ip -4 rule add priority 12101 uidrange 1500-1500 unreachable, and it matters more than its length suggests. Without it, a missing route in table 51820 falls through to the ordinary uplink and the account silently leaks traffic the policy was meant to capture. With it, the same failure becomes a timeout, which is blocking rather than fallback.

Keep the normal local-address rule ahead of these, and decide consciously whether LAN or management exceptions belong before the selected table. A default found there does not consult main for a more specific LAN route afterwards, which surprises people who expect longest-prefix matching to apply across tables. Write the intended exception list into the change note while the reasoning is still fresh.

What UID routing leaves out: DNS

A resolver daemon opens its upstream sockets under its own UID, so selecting 1500 does not move those queries into table 51820. DNS integration is a separate decision: point the account at a resolver reachable through the tunnel, or accept that name resolution follows the host's path while traffic does not.

A loopback query to a local stub is not evidence that the upstream query used the browser's route, and neither is a successful page load from cache.

IPv6 needs its own sentence in the design

The rules above cover IPv4 only. If the requirement is an IPv4 tunnel with no direct IPv6 from this account, add a reviewed terminating rule such as ip -6 rule add priority 12101 uidrange 1500-1500 unreachable, then check earlier IPv6 rules plus the loopback exceptions you intend to keep.

Blocking direct IPv6 is not dual-stack tunnelling; it is a boundary. Calling it tunnelling will confuse the next person who reads the policy, and the confusion will surface as a helpdesk ticket about a broken IPv6 site.

Verify with the real account, then own the lifecycle

Compare lookups: ip -4 route get 198.51.100.25 uid 1500 against the same destination with an ordinary UID. Then make a permitted request under the dedicated account and check the source address the far end sees. A preferred source address is not source NAT, and it will not override an application that binds something else. If both lookups return the same path, the rule is not doing what you think.

Persist the table, the rules and the tunnel-route lifetime under one owner, test a reconnect and a tunnel loss, and when retiring the policy remove only its exact entries. A namespace is still available if separate interfaces and separate state are what you actually need.

Qillqakuna t'inkisqa