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:
- Traffic from eth1 may exit via eth0's gateway (wrong VPN, wrong path)
- Return traffic may arrive on eth1 but reply via eth0, breaking the connection (asymmetric routing)
- Services bound to eth1's IP can't communicate reliably
Policy-based routing solves this by saying: traffic sourced from eth1's IP uses a separate routing table with eth1's gateway.
Concepts
| Term | What It Means |
|---|---|
| Routing table | A separate set of routes the kernel can consult |
| Routing rule | A condition that tells the kernel which table to use |
rt_tables | The file that maps table names to numeric IDs |
ip rule | Manages routing rules |
ip route table | Manages 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 inrt_tables, every command referencingtable storagewill silently fail —post-uplines in/etc/network/interfaceswon'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
| Symptom | Cause | Fix |
|---|---|---|
Error: ipv4: FIB table does not exist | Table has no routes (empty, not missing) | Run the ip route add commands |
post-up ran but table is empty | storage wasn't in rt_tables when interface came up | Add to rt_tables, then ifdown eth1 && ifup eth1 |
| Traffic still going out eth0 | Rule not added, or priority too low | Check ip rule show, verify rule exists at priority 100 |
| Ping works but service can't connect | Service not bound to eth1's IP | Bind service to 192.168.1.50 in its config |
Quick Reference
| Command | What It Does |
|---|---|
cat /etc/iproute2/rt_tables | List all named routing tables |
echo "100 name" >> /etc/iproute2/rt_tables | Register 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 show | Show all routing rules |
ip rule add from x.x.x.x table <name> priority 100 | Add a source-based routing rule |
ip rule del from x.x.x.x table <name> priority 100 | Remove a routing rule |
ifdown eth1 && ifup eth1 | Restart interface (triggers post-up) |