The most surprising thing about QoS traffic shaping is that it’s not about speeding up your network, but about slowing down certain traffic to make room for the important stuff.

Imagine your network is a highway. Without traffic shaping, all cars (packets) are treated equally. If too many cars try to use the highway at once, you get a massive traffic jam, and everyone is stuck. Traffic shaping is like adding toll booths and HOV lanes. It lets you say, "Okay, ambulances (critical traffic) get to go through the toll-free express lane, even if the highway is busy. Regular cars might have to wait a bit at the toll booth if things get crowded."

Let’s see it in action. We’ll set up a basic scenario to prioritize SSH traffic (port 22) over HTTP traffic (port 80) on a Linux machine.

First, we need to create a traffic control class. This class will define the characteristics of the traffic we want to manage. We’ll create a root class and then child classes for our different traffic types.

# Create a root class for our main interface (e.g., eth0)
sudo tc qdisc add dev eth0 root handle 1: htb default 10

# Create a parent class for all traffic
sudo tc class add dev eth0 parent 1: classid 1:1 htb rate 10mbit

# Create a class for high-priority traffic (SSH)
sudo tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit ceil 10mbit

# Create a class for low-priority traffic (HTTP)
sudo tc class add dev eth0 parent 1:1 classid 1:20 htb rate 2mbit ceil 10mbit

Here’s what’s happening:

  • tc qdisc add dev eth0 root handle 1: htb default 10: This sets up the Hierarchical Token Bucket (HTB) queuing discipline on eth0. handle 1: gives our root qdisc an identifier. default 10 means any traffic not explicitly classified will go to class 1:10.
  • tc class add dev eth0 parent 1: classid 1:1 htb rate 10mbit: This creates a parent class (1:1) under our root (1:), limiting its total bandwidth to 10mbit. This is the overall pipe for our shaping.
  • tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit ceil 10mbit: This defines our high-priority class (1:10). It’s allocated 5mbit of guaranteed bandwidth (rate) but can burst up to the parent’s 10mbit (ceil).
  • tc class add dev eth0 parent 1:1 classid 1:20 htb rate 2mbit ceil 10mbit: This defines our low-priority class (1:20). It gets a guaranteed 2mbit but can also burst up to 10mbit. The key is that 1:10 has a higher rate.

Now, we need to tell tc how to filter traffic into these classes. We use tc filter for this.

# Filter SSH traffic (port 22) into the high-priority class
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip protocol 6 0x4 skip_hw 0 \
  flowid 1:10

# Filter HTTP traffic (port 80) into the low-priority class
sudo tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip protocol 6 0x4 \
  match ip dst port 80 0xff 0x00 flowid 1:20

Let’s break down these filters:

  • tc filter add dev eth0 parent 1: protocol ip prio 1 u32: We’re adding a filter to eth0, attached to our root qdisc (parent 1:), for IP packets, with priority 1 (lower number = higher priority for the filter itself). u32 is the matching engine.
  • match ip protocol 6 0x4 skip_hw 0: This matches TCP packets (protocol 6). 0x4 is a mask. skip_hw 0 ensures it’s handled in software.
  • flowid 1:10: This directs matching packets to class 1:10 (our SSH class).
  • match ip dst port 80 0xff 0x00: This part is added to the HTTP filter. It specifically matches destination port 80. 0xff 0x00 is the mask for the port.

With this setup, if your network interface eth0 is saturated, SSH traffic will be serviced first because its class (1:10) has a higher guaranteed rate than the HTTP class (1:20). Even if both classes are using their ceil rate, the rate defines the minimum guaranteed bandwidth.

The real power and complexity come from how you define these classes and filters. You can match on source/destination IP addresses, ports, protocol types, and even create intricate hierarchies of classes. For example, you might have a "critical" parent class with a high rate, and then child classes for SSH, VoIP, and other high-priority services, all drawing from that critical pool.

A common pitfall is overlooking egress versus ingress shaping. The commands above shape traffic leaving your machine (eth0 in this case). Shaping incoming traffic is a different beast and often requires cooperation from your ISP or router, as you can’t truly slow down traffic before it hits your interface. You can, however, drop it if your buffer is full, which is a form of policing rather than shaping.

The next step in mastering tc is often understanding how to implement different queuing disciplines beyond HTB, such as fq_codel for fairness or tbf for simple token bucket shaping.

Want structured learning?

Take the full Computer Networking course →