The most surprising thing about QoS traffic shaping with iptables is that it doesn’t actually shape traffic in the way most people imagine; it manipulates packet metadata to enable shaping by other kernel components.

Let’s watch it in action. Imagine we have a web server on 192.168.1.100 that we want to limit to 1Mbps upload speed. We’ll use the tc (traffic control) command to create a rate-limiting "class" and then use iptables to mark packets so tc knows which ones belong to that class.

First, we need to set up a basic queueing discipline. We’ll use htb (Hierarchical Token Bucket), a common choice for shaping.

# Add a root qdisc (traffic control discipline) to the primary interface
sudo tc qdisc add dev eth0 root handle 1: htb default 12

# Add a parent class for our web server traffic, with a limit of 1Mbps
sudo tc class add dev eth0 parent 1: classid 1:10 htb rate 1mbit ceil 1mbit

# Add a default class for all other traffic (this will be class 1:12)
sudo tc class add dev eth0 parent 1:12 classid 1:12 htb rate 100mbit # Effectively unlimited for other traffic

Now, we need to tell iptables to mark packets originating from our web server (192.168.1.100) so that tc can identify them. We’ll use the mangle table and the MARK target. We’ll assign a mark of 10.

# Mark outgoing packets from the web server with mark 10
sudo iptables -t mangle -A OUTPUT -s 192.168.1.100 -j MARK --set-mark 10

Finally, we need to tell tc to associate packets with mark 10 with our 1:10 class. We do this by adding a filter to our root htb qdisc.

# Filter packets with mark 10 and direct them to class 1:10
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
    match ip mark 10 0xffffffff flowid 1:10

Now, any packets originating from 192.168.1.100 will be marked with 10. When tc processes outgoing traffic on eth0, it sees these marked packets. The tc filter directs them to the 1:10 class, which is configured with a rate of 1mbit. If the traffic exceeds this rate, tc will buffer or drop packets according to the htb discipline, effectively shaping the traffic.

The iptables mangle table is where we intercept packets before they are routed or queued for transmission. The MARK target doesn’t change the packet’s content or destination; it adds a small, internal kernel tag. This tag is then read by tc’s filters. This separation of concerns is key: iptables identifies and tags, while tc enforces the actual rate limits based on those tags.

The handle 1: on the root qdisc and classid 1:10 and 1:12 for the classes are internal identifiers within the tc subsystem. The default 12 in the root qdisc means that any traffic not explicitly matched by a filter will fall into class 1:12. The u32 filter is a powerful way to match on various packet fields, including the ip mark. The 0xffffffff is a mask to ensure we match the entire mark value.

What most people miss is that iptables itself doesn’t slow anything down. The MARK target is essentially a no-op in terms of packet manipulation visible to the network. It’s purely an internal signal. The actual shaping happens entirely within the tc subsystem, which is triggered by these marks. Without tc configured to listen for these marks, the iptables rule would just be tagging packets that nobody is paying attention to.

After setting up your QoS, you’ll likely want to inspect the actual traffic rates to verify your configuration.

Want structured learning?

Take the full Iptables course →