You can route traffic based on packet marks, but the surprising part is that the kernel does the marking, not iptables itself. iptables just tells the kernel when to apply a mark based on packet headers. Then, iproute2 uses that mark to decide where the packet goes.
Let’s see it in action. Imagine we have two network interfaces, eth0 (our main internet connection) and eth1 (a secondary, maybe slower, connection). We want to send all SSH traffic (TCP port 22) out through eth1, while everything else goes through eth0.
First, we mark the SSH packets. We’ll use iptables for this.
sudo iptables -t mangle -A OUTPUT -p tcp --dport 22 -j MARK --set-mark 1
This command tells the kernel: "For any outgoing TCP packet destined for port 22, apply a mark of 1." The -t mangle table is used for packet alteration, and MARK is the target that sets the mark.
Now, we need iproute2 to use this mark to make routing decisions. We’ll create a new routing table, let’s call it table_ssh, and tell the kernel to use it for packets marked with 1.
First, add an entry to /etc/iproute2/rt_tables to give our new table a name:
100 table_ssh
Then, add a default route to table_ssh that uses eth1:
sudo ip route add default via 192.168.2.1 dev eth1 table table_ssh
This says: "For traffic destined for anywhere (default), if it’s going through eth1, use the gateway 192.168.2.1." (Replace 192.168.2.1 with your actual gateway IP on eth1).
Finally, we tell the kernel to use table_ssh for packets with mark 1:
sudo ip rule add fwmark 1 table table_ssh
This is the magic. It means: "If an incoming or outgoing packet has the firewall mark 1, consult table_ssh for routing information."
Now, if you try to SSH to a remote server, the packet will be marked 1, and ip rule will direct the kernel to look at table_ssh. Since table_ssh has a default route via eth1, your SSH traffic will go out that interface. All other traffic, not being marked, will fall through to the main routing table and use eth0 (assuming it has a default route there).
You can verify the marks with iptables -t mangle -L -v and the rules with ip rule show. You can also see which table a packet is using with tcpdump and iptables -j TRACE (though TRACE can be noisy).
The system builds this by layering packet filtering (iptables) on top of sophisticated routing policies (iproute2). iptables doesn’t route, it inspects and annotates. iproute2 then acts on those annotations. It’s a powerful decoupling that allows for complex traffic management.
One of the most subtle aspects is how the marking interacts with existing routing. If a packet is already routed before it hits the OUTPUT chain (for locally generated traffic) or an PREROUTING chain (for transit traffic), and then gets marked, the routing decision is re-evaluated based on the mark. This means you can dynamically change a packet’s path mid-flight, not just at the initial routing lookup.
The next step is often dealing with return traffic, ensuring that replies to traffic sent out eth1 also come back via eth1.