Guides

Policy-Based Routing in Linux

When a Linux host has two network interfaces (e.g., eth0 on VLAN 10, eth1 on VLAN 30), the kernel has one main routing table with one default route. Without policy routing:

Policy-based routing solves this by saying: traffic sourced from eth1's IP uses a separate routing table with eth1's gateway.

Concepts

TermWhat It Means
Routing tableA separate set of routes the kernel can consult
Routing ruleA condition that tells the kernel which table to use
rt_tablesThe file that maps table names to numeric IDs
ip ruleManages routing rules
ip route tableManages routes in a specific table

Step 1 — Register the Table Name

Linux routing tables are identified by number (0–255 reserved, 1–252 usable). You give them names by adding an entry to /etc/iproute2/rt_tables:

echo "100 storage" >> /etc/iproute2/rt_tables

This maps the name storage to table ID 100. You can use any name and any unused ID.

⚠️ Critical: If the name isn't in rt_tables, every command referencing table storage will silently fail — post-uplines in /etc/network/interfaces won't error out, they just won't do anything. Always add the name first.

Verify:

cat /etc/iproute2/rt_tables | grep storage

Step 2 — Add Routes to the New Table

# Route for the local subnet via eth1
ip route add 192.168.1.0/24 dev eth1 src 192.168.1.50 table storage

# Default route for this table (traffic that isn't local goes via eth1's gateway)
ip route add default via 192.168.1.1 dev eth1 table storage

Verify:

ip route show table storage

Expected output:

default via 192.168.1.1 dev eth1
192.168.1.0/24 dev eth1 scope link src 192.168.1.50

If you get Error: ipv4: FIB table does not exist — the table is empty (no routes yet), not that the name is wrong.


Step 3 — Add the Routing Rule

The rule tells the kernel: "when traffic is sourced from 192.168.1.50, use the storage table."

ip rule add from 192.168.1.50 table storage priority 100

The priority 100 sets rule precedence — lower numbers are checked first. The default rules are at 0, 32766, and 32767, so 100 is evaluated early.

Verify:

ip rule show

Expected output:

0:      from all lookup local
100:    from 192.168.1.50 lookup storage
32766:  from all lookup main
32767:  from all lookup default

Step 4 — Test

# Verify eth1 can reach its gateway
ping -I eth1 192.168.1.1

# Verify internet reachability via eth1
ping -I eth1 8.8.8.8

# Check what IP the internet sees (should be eth1's VPN IP)
curl --interface eth1 https://ifconfig.me
Note: ICMP (ping) is often blocked by VPN providers. If ping to 8.8.8.8 fails but curl returns an IP, the interface is working fine.

Step 5 — Make It Persistent

The routes and rules above are lost on reboot. Persist them using post-up and pre-down hooks in /etc/network/interfaces:

auto eth1
iface eth1 inet static
    address 192.168.1.50/24
    gateway 192.168.1.1
    post-up ip route add 192.168.1.0/24 dev eth1 src 192.168.1.50 table storage
    post-up ip route add default via 192.168.1.1 dev eth1 table storage
    post-up ip rule add from 192.168.1.50 table storage priority 100
    pre-down ip rule del from 192.168.1.50 table storage priority 100

The post-up lines run after the interface comes up. The pre-down line cleans up the rule when the interface goes down.

Test persistence:

ifdown eth1 && ifup eth1
ip route show table storage    # should still be populated
ip rule show                   # rule should still be present

Troubleshooting

SymptomCauseFix
Error: ipv4: FIB table does not existTable has no routes (empty, not missing)Run the ip route add commands
post-up ran but table is emptystorage wasn't in rt_tables when interface came upAdd to rt_tables, then ifdown eth1 && ifup eth1
Traffic still going out eth0Rule not added, or priority too lowCheck ip rule show, verify rule exists at priority 100
Ping works but service can't connectService not bound to eth1's IPBind service to 192.168.1.50 in its config

Quick Reference

CommandWhat It Does
cat /etc/iproute2/rt_tablesList all named routing tables
echo "100 name" >> /etc/iproute2/rt_tablesRegister a new table name
ip route show table <name>Show routes in a named table
ip route add default via x.x.x.x dev ethX table <name>Add default route to a table
ip rule showShow all routing rules
ip rule add from x.x.x.x table <name> priority 100Add a source-based routing rule
ip rule del from x.x.x.x table <name> priority 100Remove a routing rule
ifdown eth1 && ifup eth1Restart interface (triggers post-up)
Wayne M. Shelton Sr. is a retired military veteran and cybersecurity practitioner. He holds the CISM, CGRC, CySA+, Security+, and CTT+ certifications and writes about cybersecurity, AI, and ministry technology at waynesheltonsr.com. Contact: [email protected]