Linux Policy Routing with ip rule: Source-Based Paths and Verification
A practical Linux policy-routing guide for multiple uplinks, source-based rules, route-table verification, rp_filter behavior, persistence, and rollback.
The ordinary Linux routing lookup selects a route primarily by destination prefix and longest-prefix match in a routing table. Policy routing adds an ordered rule database that can select a table based on packet properties such as source address, destination, incoming interface, mark, or UID range. It is useful when one host has multiple uplinks, source-address-specific gateways, separate VPN paths, or traffic classes that must use different routes.
Policy routing is not the same as bonding, ECMP, NAT, or a firewall. It does not combine links, translate addresses, provide health checks, or guarantee return-path symmetry by itself. Keep those responsibilities explicit: routes choose a next hop, policy rules choose which table to consult, a firewall filters traffic, and an application or routing daemon may manage failover.
Inventory the actual routing state first
Collect interface addresses, link state, all routing rules, and the relevant route tables before changing anything:
ip -brief link
ip -brief address
ip rule show
ip route show table all
ip route get 203.0.113.80
Use reserved documentation networks in examples only. Replace interface names, prefixes, gateways, and test destinations with the addresses actually assigned by your network. On a remote host, use an out-of-band console or a tested timed rollback before changing the table carrying the management session. A syntactically valid rule can instantly strand SSH if its selected table lacks a route to the administrator.
The default rule database normally checks the local table first, then main, and then default. Rule priorities are ordered numerically: lower numbers have higher priority. Inspect the installed output instead of assuming that a distribution, VPN, container runtime, or administrator has left the default rules untouched. Reserve nonconflicting priorities and table IDs in the site’s network plan.
Route traffic sourced from a particular uplink through its gateway
Suppose a host has address 192.0.2.10/24 on wan0, with gateway 192.0.2.1. The main table may have a different default route for other traffic. Add the directly connected subnet and default route to table 100, then add a rule selecting that table for packets sourced from the first uplink’s address:
sudo ip route add 192.0.2.0/24 dev wan0 src 192.0.2.10 table 100
sudo ip route add default via 192.0.2.1 dev wan0 table 100
sudo ip rule add priority 100 from 192.0.2.10/32 table 100
The connected route is intentional: it gives the table a route to the on-link gateway as well as a default next hop. Without appropriate reachability to the gateway, the default route can be rejected or fail at resolution. If the provider uses point-to-point addressing, DHCP, a VRF, or a gateway outside the interface prefix, use the route form required by that link rather than copying this Ethernet example.
Verify the rule order and table contents, then ask the kernel to resolve a route for an explicit source:
ip rule show
ip route show table 100
ip route get 203.0.113.80 from 192.0.2.10
The last output should show table 100, device wan0, and a compatible source address for this example. ip route get reports the kernel’s lookup result; it does not send a packet or prove that the gateway, upstream route, firewall, DNS, or remote service works. Test an approved real endpoint from the selected source, inspect packet flow on the relevant interface, and confirm the return path.
Understand priorities and rule termination
The routing policy database is evaluated in priority order. A matching rule may return a successful route lookup, an unreachable/prohibit/blackhole result, or continue when a lookup table has no applicable route. A rule with an explicit priority is easier to audit than one that relies on an automatically assigned value. Avoid adding broad rules ahead of the local-table rule or flushing the rule database to “start over”; both can disrupt local and system-managed routes.
Use numeric table IDs in ephemeral commands so the example does not depend on a local /etc/iproute2/rt_tables alias. For readability, a site may assign a unique name there, but the name is a local mapping, not a network protocol identity. Maintain the same mapping in configuration management on every host where operators will inspect or restore the rules.
Handle return traffic and reverse-path filtering
Source-based routing is often introduced to keep outbound packets on the uplink that owns their source address. The same design must account for replies and for incoming connections. If traffic arrives on wan0 but the reverse lookup for its source chooses wan1, strict reverse-path filtering can reject it. Inspect both global and per-interface IPv4 settings:
sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.wan0.rp_filter
sysctl net.ipv4.conf.wan1.rp_filter
Linux documents strict mode as validating that the incoming interface is the best reverse path, and loose mode as requiring the source to be reachable by some interface. The effective setting uses the maximum of conf/all and the interface setting; changing only one value may therefore not change the effective behavior. Do not disable source validation globally just to make a rule appear to work. Choose settings based on the host’s routing threat model and asymmetric-routing design, and verify the change through the network owner’s policy.
If firewall marks participate in routing, confirm whether marks are included in reverse-path lookup. The kernel’s src_valid_mark setting controls that interaction. A design that marks only one traffic direction can legitimately need different validation reasoning from a design that applies the mark in both directions. Validate both directions with packet captures and counters; a successful ip route get alone is not sufficient.
Persist policy with the network manager that owns the link
The ip route and ip rule commands above change runtime kernel state; they are not persistent configuration. Pick one manager for the interface. Do not combine a hand-written systemd-networkd file with NetworkManager managing the same device, or a cloud-init/network provisioning agent that rewrites the profile.
For a system managed by systemd-networkd, the following is an illustrative .network fragment for the wan0 example. Place it in the correct networkd configuration directory and verify the installed systemd version’s supported keys before activation:
[Match]
Name=wan0
[Network]
Address=192.0.2.10/24
[Route]
Destination=192.0.2.0/24
Scope=link
Table=100
[Route]
Destination=0.0.0.0/0
Gateway=192.0.2.1
Table=100
[RoutingPolicyRule]
From=192.0.2.10/32
Table=100
Priority=100
This file is not an instruction to enable networkd on a machine already managed by another service. Stage the new configuration with local console access, validate it using the installed systemd tooling, and schedule activation in a maintenance window. Preserve the old file and a tested recovery route; reload/reconfigure can interrupt connectivity. For NetworkManager, configure the profile using its supported route-table and routing-rule properties rather than replaying runtime shell commands from a boot script.
Test all relevant lookups and failure modes
Test the kernel lookup for at least the uplink source, the other uplink source, an internal destination, a VPN destination, and the management path. Then send bounded test traffic and observe which interface emits it:
ip route get 203.0.113.80 from 192.0.2.10
ip route get 203.0.113.80 from 198.51.100.10
ip route get 10.20.0.15 from 192.0.2.10
sudo tcpdump -ni wan0 host 203.0.113.80
sudo tcpdump -ni wan1 host 203.0.113.80
The second address is another documentation-only example and must be replaced or omitted if the host does not own it. A route lookup test with a source address not assigned to the host can still return a hypothetical route; it does not validate source-address availability. Test name resolution separately because DNS queries may use a different source and resolver route than the application connection.
If the route is wrong, inspect rule ordering and table contents before changing firewall or NAT. If the outgoing interface is correct but replies do not arrive, inspect upstream return routes, anti-spoofing policy, firewall state, and source address ownership. If a connection works outbound but inbound replies vanish, compare packet captures on both interfaces and the reverse-path filter settings. Do not add an extra default route as an unmeasured workaround: it may introduce ECMP, asymmetric flows, or a route that survives only until the manager reloads.
Roll back only the exact test rule and route
For the runtime example, remove the precise rule and its specific table entries after restoring the previous path:
sudo ip rule del priority 100 from 192.0.2.10/32 table 100
sudo ip route del default via 192.0.2.1 dev wan0 table 100
sudo ip route del 192.0.2.0/24 dev wan0 table 100
Do not use ip rule flush or ip route flush table all on a managed host. If the configuration is persistent, revert the owning manager’s file/profile, then reload it under the same recovery plan. Confirm the old default route, internal destinations, DNS, and remote administration work again before ending the maintenance window.
Related:
- How to Configure Persistent Linux Networking with NetworkManager and nmcli
- How to Configure Network Bonding on Linux
Sources: