Policy-Based Routing (PBR) on Linux: Multiple Routing Tables, ip rule & fwmark
Created: 2026-11-08 · Last updated: 2026-11-08 · Nejat Hakan
Executive Summary: Standard IP routing makes decisions strictly based on the Destination IP address. When a server has multiple internet uplinks (Multi-WAN multihoming), isolated tenant subnets, or policy-driven VPN tunnels, destination-only routing inevitably causes asymmetric routing failures and connection drops. Policy-Based Routing (PBR) unlocks the Linux kernel's Routing Policy Database (RPDB), allowing engineers to route packets based on Source IP, Ingress Interface, or Netfilter Firewall Marks (
fwmark) across multiple independent routing tables.
-
The RPDB Architecture
Understand the Routing Policy Database (
ip rule), priority order evaluation (0–32767), and the default tables (local,main,default). -
:material-table-split:{ .lg .middle } Custom Routing Tables
Register named routing tables in
/etc/iproute2/rt_tablesto isolate ISP-specific gateways and VPN interfaces from the main table. -
Multi-WAN Multihoming
Solve the asymmetric return-path dilemma: ensure traffic arriving on WAN-2 always exits via WAN-2's gateway, regardless of the default route.
-
Firewall Marking (
fwmark)
Classify traffic dynamically in
nftables(by port, protocol, or systemd cgroup) and steer matching packets through dedicated routing tables.
1. The Dilemma of Destination-Only Routing
Under standard routing, the Linux kernel maintains a single primary routing table (main). When an application sends a packet to 93.184.216.34, the kernel looks only at the destination IP:
The Asymmetric Routing Trap in Multi-Homed Servers
Consider a server connected to two ISPs:
* eth0 (ISP 1): 203.0.113.10 (Default Gateway: 203.0.113.1)
* eth1 (ISP 2): 198.51.100.20 (Gateway: 198.51.100.1)
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
CLIENT["Internet Client"] ==>|"1. SYN Packet arrives on eth1 (ISP 2)"| SRV["Multi-Homed Debian Server"]
SRV -.->|"2. Kernel consults default route (ISP 1)!<br/>SYN-ACK exits eth0 with Src IP 198.51.100.20"| ISP1["ISP 1 Upstream Router"]
ISP1 -->|"3. DROPPED by ISP Anti-Spoofing (BCP 38)!"| BLACKHOLE["Packet Lost / Client Hangs"]
Because the kernel's default route points to ISP 1, the return packet (SYN-ACK) exits via eth0, carrying eth1's source IP. ISP 1's upstream router detects a spoofed IP and drops it, or stateful firewalls drop the asymmetric return flow.
2. The Routing Policy Database (RPDB)
Linux solves this via the Routing Policy Database (RPDB). Instead of immediately consulting the routing table, the kernel first evaluates a prioritized list of rules:
Default output on a fresh Debian system:
- Priority
0(localtable): Reserved for loopback, local interface IPs, and broadcast addresses. Cannot be removed. - Priority
32766(maintable): The standard routing table populated byip route addor DHCP. - Priority
32767(defaulttable): Previously used for fallback routes; rarely used today.
3. Registering Custom Routing Tables
Custom routing tables are identified internally by an integer ID (1 to 2147483647). To make administration human-readable, map table IDs to symbolic names in /etc/iproute2/rt_tables:
# Append custom named tables to /etc/iproute2/rt_tables
sudo bash -c 'echo "201 isp2" >> /etc/iproute2/rt_tables'
sudo bash -c 'echo "202 vpn_out" >> /etc/iproute2/rt_tables'
Verify contents:
4. Multi-WAN Multihoming Solution
To solve the dual-ISP asymmetric routing problem, we enforce a strict policy: Traffic originating from ISP 2's IP must always use ISP 2's routing table.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
A["Outgoing Packet"] --> B{"Evaluate ip rules in priority order"}
B -->|"Source IP == 198.51.100.20"| C["Lookup Table 'isp2'<br/>Default via 198.51.100.1 dev eth1"]
B -->|"All other traffic"| D["Lookup Table 'main'<br/>Default via 203.0.113.1 dev eth0"]
Step 1: Populate Table isp2
Create a dedicated default route and local subnet route inside table isp2:
# Add default gateway for ISP 2 into table isp2
sudo ip route add default via 198.51.100.1 dev eth1 table isp2
# Add local subnet route so table isp2 knows how to reach directly connected hosts
sudo ip route add 198.51.100.0/24 dev eth1 src 198.51.100.20 table isp2
Step 2: Create the Policy Rule
# Any packet with source IP 198.51.100.20 must consult table isp2
sudo ip rule add from 198.51.100.20 table isp2 priority 1000
Step 3: Verify the Kernel Route Selection
Query the kernel routing engine to prove the rule works:
# Query route for an internet IP from ISP 2's source address
ip route get 8.8.8.8 from 198.51.100.20
# Output: 8.8.8.8 from 198.51.100.20 via 198.51.100.1 dev eth1 table isp2
# Query route without specifying source (falls back to main table)
ip route get 8.8.8.8
# Output: 8.8.8.8 via 203.0.113.1 dev eth0 table main
5. Advanced Steering: Firewall Marks (fwmark)
Source IP steering only works if the source IP is known before routing occurs. If you want to route traffic based on Destination Port (e.g., steer all BitTorrent or S3 backup traffic through a WireGuard VPN while web browsing uses WAN-1), use Netfilter Firewall Marks:
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
A["Application generates S3 Backup traffic (TCP 9000)"] --> B["nftables PREROUTING / OUTPUT Hook<br/>Mark packet with fwmark 0x64 (100)"]
B --> C["Kernel RPDB evaluates: ip rule fwmark 100 lookup vpn_out"]
C --> D["Table vpn_out routes packet via wg0 tunnel"]
1. Populate Table vpn_out
2. Add fwmark Policy Rule
# Match any packet marked with hex 0x64 (decimal 100)
sudo ip rule add fwmark 100 table vpn_out priority 2000
3. Mark Matching Packets in nftables
Add the mark rule in /etc/nftables.conf:
table inet pbr_mangle {
chain output {
type route hook output priority mangle; policy accept;
# Mark all outbound TCP traffic destined for port 9000 (S3 Backup) with mark 100
tcp dport 9000 meta mark set 100
}
}
6. Persistent Configuration with systemd-networkd
In production, commands executed directly via ip rule are lost upon reboot. Configure persistent PBR in /etc/systemd/network/:
# File: /etc/systemd/network/20-wan-isp2.network
[Match]
Name=eth1
[Network]
Address=198.51.100.20/24
# 1. Add Default Route to Table 201 (isp2)
[Route]
Gateway=198.51.100.1
Table=201
# 2. Add Policy Rule to force traffic from this IP to consult Table 201
[RoutingPolicyRule]
From=198.51.100.20/32
Table=201
Priority=1000
Apply and reload:
7. Next Steps
With multi-table policy routing fully configured, explore physical link redundancy and traffic isolation using Layer-2 bonding and VLANs:
-
802.1Q VLAN Trunks & 802.3ad LACP Bonding
Aggregate multiple 10GbE NICs into high-availability active-active bonds and trunk isolated VLAN security zones.