How Linux picks between multiple routing tables.
Linux Networkingip route, longest-prefix-match, and the netfilter flow. Today we introduce the rule list that sits in front of every routing table lookup.Lesson 02 showed how the kernel consults the main routing table for every packet. But Linux actually ships with three built-in tables:
ip route show displays. Your static routes live here.Plus up to 252 user-defined tables, each with its own independent prefix set. The question is: who decides which table to use?
ip ruleA sorted list of rules maps packet attributes → table IDs, evaluated before any table lookup.
When the kernel needs to route a packet, it performs two distinct lookups in sequence:
ip rule picks a tableWalk the rule list by priority. The first matching rule names a routing table. If no user rule matches, fall through to main (priority 32766).
Perform a normal longest-prefix-match lookup inside the selected table. If the table has no match, the rule list continues to the next rule (not necessarily main).
This two-step process lets you route different traffic classes through entirely different forwarding paths — without touching the main table at all.
ip ruleEvery rule has three parts: a priority, a selector, and a target table.
ip rule add pref 100 from 10.10.0.0/24 lookup 200
pref 100 — priority; lower numbers are evaluated firstfrom 10.10.0.0/24 — selector: match packets whose source address falls in this subnetlookup 200 — target: if this rule matches, consult routing table 200A single rule can combine multiple selectors — all must match for the rule to fire.
| Selector | Example | Matches when… |
|---|---|---|
from |
from 10.10.0.0/24 |
Source address is within the given prefix |
to |
to 8.8.0.0/16 |
Destination address is within the given prefix |
fwmark |
fwmark 0x42 |
Packet carries the given netfilter mark (set by iptables/nftables MARK) |
iif |
iif eth1 |
Packet arrived on the named interface |
oif |
oif eth0 |
Packet is leaving on the named interface (locally-originated traffic) |
tos |
tos 0x10 |
IP Type of Service / DSCP field matches |
fwmark is the most powerful — it lets netfilter "tag" packets with arbitrary 32-bit values before the routing decision. This is the key to VPN policy routing, QoS, and split tunnels.A server has two uplinks (eth0 → ISP-A, eth1 → ISP-B). Traffic from subnet 10.10.0.0/24 must always leave via ISP-B, regardless of the destination.
# Table 200 gets its own default route via ISP-B
ip route add default via 203.0.113.1 dev eth1 table 200
# Rule: any packet sourced from 10.10.0.0/24 uses table 200
ip rule add from 10.10.0.0/24 lookup 200 pref 100
# All other traffic falls through to table main → ISP-A
ip route add default via 198.51.100.1 dev eth0
Because a single default route can't express "use this gateway for some sources and that gateway for others." PBR handles this elegantly without NAT or complex iptables rules.
Netfilter's MARK target paints a 32-bit tag onto a packet in the mangle table. The ip rule fwmark selector then routes by that tag.
# Mark packets from VPN clients with 0x42
iptables -t mangle -A PREROUTING -i tun0 -j MARK --set-mark 0x42
# Route all marked packets through table 200
ip rule add fwmark 0x42 lookup 200 pref 100
# Table 200's default: force through the secondary WAN
ip route add default via 10.99.0.1 dev eth1 table 200
CONNMARK saves a mark to the conntrack entry, so every subsequent packet in the flow inherits it — even before the mangle rule fires again.
# Three-rule mangle chain for CONNMARK
# 1. Restore saved mark for ESTABLISHED/RELATED packets
iptables -t mangle -A PREROUTING -m conntrack --ctstate ESTABLISHED,RELATED \
-j CONNMARK --restore-mark
# 2. Mark NEW packets
iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW \
-i tun0 -j MARK --set-mark 0x42
# 3. Save the mark back to conntrack for future packets
iptables -t mangle -A PREROUTING -j CONNMARK --save-mark
Two default routes with different metrics provide automatic failover. Lower metric = preferred. When the primary link goes down, the kernel falls back to the higher-metric route automatically.
# Primary default (metric 100 — preferred)
ip route add default via 10.99.0.1 dev eth0 metric 100
# Backup default (metric 200 — used only if eth0 fails)
ip route add default via 10.99.1.1 dev eth1 metric 200
# Optional: source-policy overlay to guarantee return-path symmetry
ip rule add from 10.99.0.0/24 lookup isp-a pref 100
ip rule add from 10.99.1.0/24 lookup isp-b pref 110
rp_filter gotchaWhen you add policy routing, return packets may arrive on an interface that doesn't match the route the kernel would use to reach the sender. The Reverse Path Filter will silently drop these packets.
You can ping out, but replies never arrive. ip route get shows the right table. conntrack -L shows the flow. But packets vanish.
# Check current rp_filter values
sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.eth0.rp_filter
# 0 = off | 1 = strict (default) | 2 = loose
# For PBR, use loose mode (2) on the relevant interface
sysctl -w net.ipv4.conf.eth0.rp_filter=2
sysctl -w net.ipv4.conf.all.rp_filter=2
# Persist in /etc/sysctl.d/
echo "net.ipv4.conf.all.rp_filter=2" >> /etc/sysctl.d/99-pbr.conf
Loose mode (2) accepts packets if any route leads back to the source — catching obvious spoofs. Strict mode (1) requires the packet to arrive on the same interface the kernel would use to reach the source, which breaks asymmetric PBR setups.
Understanding the data structures behind ip rule and ip route helps you reason about performance and design rule lists correctly.
Five commands cover 90% of PBR debugging:
# 1. Show the rule list (and which table each rule points to)
ip rule show
# 2. Show routes in a specific table
ip route show table 200
# 3. Show all tables at once (filter noise)
ip route show table all | grep -v "^broadcast\|^local\|^multicast"
# 4. Simulate the full two-step lookup for a given source/destination
ip route get 8.8.8.8 from 10.10.0.5
# 5. Watch conntrack marks on live traffic
conntrack -L -o extended | grep mark
# Bonus: trace the netfilter path and routing decision together
iptables -t mangle -A PREROUTING -j LOG --log-prefix "[mangle] "
# (remember to delete after debugging)
ip route get … from … is the most underused command in Linux networking. It tells you exactly which table the kernel selected and which route it matched, in one call.sysctl net.ipv4.conf.all.rp_filter first before spending hours in tcpdump.
--save-mark / --restore-mark.
ip rule add is ephemeral like ip route add. Write rules to /etc/iproute2/rt_rules or your distro's network configuration (NetworkManager, systemd-networkd, ifupdown).
ip rule picks a table; that table is consulted for the longest prefix match.from, to, fwmark, iif, oif, tos; combine freely, lower pref wins.--save-mark / --restore-mark makes marks sticky to the entire conntrack flow.rp_filter (use loose mode=2), return-route symmetry, and rule list length.ip link add vrf0 type vrf table 100), IPv6 policy routing (ip -6 rule), and traffic shaping with tc using fwmark as a classifier.