4. Implementation

Now with all of the explanation out of the way it's time to implement upstream bandwidth management with Linux.

4.1. Obtaining, Compiling and Installing sch_fwprio

If you haven't already, you'll need to obtain the sch_fwprio module or kernel patch. You can download either of these here.

Note: It is not necessary to compile the sch_fwprio module AND apply the fwprio.diff patch! Both accomplish the same thing.

4.2. Setting the Queue Length

Even though we'll be placing traffic in different bands based on priority, we still want the queues we set up to empty in about two seconds. This suggests a queue size of 20 packets for our example:

ip link set eth0 txqueuelen 20

4.3. Setting up CBQ

CBQ is poorly documented and can get very complicated, so I will attempt to keep things simple and explain them as best I can. Once again, the reason that we are using CBQ is to limit the rate that we send data out eth0. This will prevent the ADSL modem from queuing packets. Also, we will attach the sch_fwprio queue as a leaf queuing discipline.

All of the statements should be added to your rc.local file or another appropriate startup script.

First we tell Linux that we want CBQ to be the root queuing discipline on eth0:

tc qdisc add dev eth0 root handle 128: cbq bandwidth 10Mbit avpkt 700

With this line we've told Linux to use CBQ on eth0 and that the handle for this class is 128:0, and the average packet is 700 bytes. We've also specified the total bandwidth for eth0 as 10Mbit/sec because this is the maximum speed at which our ethernet card can transmit data. This is NOT your upstream data rate! We'll specify that in the next statement:

tc class add dev eth0 parent 128:0 classid 128:1 cbq bandwidth 10Mbit \
   rate 90Kbit allot 1514 weight 9Kbit prio 5 maxburst 1 avpkt 700 \
   bounded

This statement creates the class which will throttle outbound bandwidth on eth0 (in this case, to 90kbit/s). Since this document is not meant to explain the inner workings of CBQ, I'll skip a detailed explanation. The only important numbers up there are numbers following rate and weight. The number following rate should be slightly lower than your upstream data rate. I use 90kbit which works well for my 128kbit/s upstream, giving me almost full use of my available bandwidth. Whatever you set your rate to, the weight should be about 1/10 of that value. Also, the number following allot sould be set to the mtu for eth0 (1514 works fine for ethernet). Also important is the bounded keyword. If this keyword is not specified, then the class will attempt to borrow extra bandwidth from the parent class.

Now if you've decided NOT to patch your kernel, and instead have compiled the sch_fwprio module, you'll need to load it BEFORE you use the prio queue. Otherwise the stock sch_prio will be loaded:

insmod /usr/local/src/fwprio/sch_fwprio.o

Now you have to attach the queue as a leaf discipline to the CBQ:

tc qdisc add dev eth0 parent 128:1 prio bands 4 priomap 0 1 2 3 3 3 3 3 3 3 3 3 3 3 3 3

tc filter add dev eth0 parent 128:0 protocol ip prio 5 u32 match ip src \
   1.2.3.4/32 flowid 128:1

With these statements we've created a prio queue of 4 bands (which is really using our new sch_fwprio code) and then we've used a filter to mark all of the packets to be handled by the 128:1 classid of our CBQ, which the prio filter is attached to. The priomap keyword, although unused by sch_fwprio, is still necessary to initialize the individual bands. You MUST specify each band number at least once in the 16-field priomap (in the above example I've initialized 4 bands, 0..3). You must start at zero and end with the number of bands minus one. You must specify 16 fields so it's okay to repeat the last band like I've done above. All of this garbage is to satisfy the requirements of the old sch_prio code.

4.4. Setting up Packet Classification

Here's where you get to specify how packets are classified into different priority bands in the queue. This is completely up to you, but here is a good place to start:

Create a new chain called qos-out. Create a new rule as the first rule in the output chain that sends all packets to the qos-out chain (we'll send them back at the end of the qos-out chain):

ipchains -N qos-out
ipchains -I output -i eth0 -j qos-out

Now we'll set up a few packet classification rules (don't forget to RETURN packets back to the output chain if you have other rules there that need to be checked):

ipchains -A qos-out -m 3
ipchains -A qos-out -p icmp -m 0
ipchains -A qos-out -p tcp -s 0.0.0.0/0 0:1024 -m 1 
ipchains -A qos-out -p tcp -d 0.0.0.0/0 0:1024 -m 1
ipchains -A qos-out -p tcp -d 0.0.0.0/0 25 -m 2
ipchains -A qos-out -j RETURN

The first rule here marks all packets into the lowest priority band by default. The next rule puts ICMP packets in the highest priority band, since we will be using ping to test latency we want it to return an accurate result. The next two rules place packets to or from system ports (0-1024) in band 1. Although not as high as the ICMP band, this is the band we'll use for 'interactive' traffic such as web requests and telnet. Note that we place port 25 outbound (SMTP) into band 2. This is because we don't want someone sending a file attachment in an email to swamp a telnet session. The final rule sends packets back to the output chain now that we're done classifying them.

Use this only as a starting point. Your qos-out chain will undoubtedly become quite complex as you decide how you want to prioritize traffic. You may find that you need more than 4 bands. You will probably find that giving DNS requests high priority works well, although I haven't included this in the example above.