DSCP based QoS with HTB

From MikroTik Wiki
Jump to: navigation, search

DSCP based QoS with HTB

About

This page tries to describe a way to prioritize traffic by using DSCP tags. The DiffServ Code Point is a field in the IP header that allows you to classify traffic. DSCP is meant to be administered in a per-hop-based way, allowing each router on a path to determine how each traffic class should be prioritized. The solution described in this document is built around the Hierarchical Token Bucket queuing algorithm in RouterOS, dividing the 64 possible DSCP code values into the 8 queues available. This solution also utilizes the tree-based queuing, in order to have a parent queue do bandwidth control, with sub-queues for each possible DSCP value.

The actual queuing is done as per this table:

Name Precendence DSCP Range HTB Priority
Routine (default) 000 (0) 000000(0) – 000111 (7) 8
Priority 001 (1) 001000 (8) – 001111 (15) 7
Immediate 010( (2) 010000 (16) – 010111 (23) 6
Flash 011 (3) 011000 (24) – 011111 (31) 5
Flash Override 100 (4) 100000 (32) – 100111 (39) 4
Critical 101 (5) 101000 (40) – 101111 (47) 3
Internetwork Control 110 (6) 111000 (48) – 110111 (55) 2
Network Control 111 (7) 111000 (56) – 111111 (63) 1

This solution has been tested on RB450, RB600 and RB1000 using any 3.x interface.

DSCP marking/mangling

In order to match DSCP values in your queues, it is necessary to mark the packets using firewall mangling. This is best done with this command:

:for x from 0 to 63 do={/ip firewall mangle add action=mark-packet chain=postrouting \
comment=("dscp_" . $x . "_eth") disabled=no dscp=$x new-packet-mark=("dscp_" . $x . "_eth") passthrough=no}

This command creates 64 lines under /ip firewall mangle, that simply marks each packet with a DSCP value to be processed later.

Having that done, it's time to move on to the actual queues.

Set up the queue tree

The next example assumes that ether1 is the wan interface, and your available bandwidth is 5Mbit/s.

/queue tree
add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=5000000 name=ether1 \
parent=ether1 queue=default

#prio8
:for z from 0 to 7 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \ 
name=("routine_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=8 queue=ethernet-default}

#prio7
:for z from 8 to 15 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("priority_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=7 queue=ethernet-default}

#prio 6
:for z from 16 to 23 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("immediate_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=6 queue=ethernet-default}

#prio 5
:for z from 24 to 31 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("flash_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=5 queue=ethernet-default}

#prio 4
:for z from 32 to 39 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("flash_override_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=4 queue=ethernet-default}

#prio 3
:for z from 40 to 47 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("critical_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=3 queue=ethernet-default}

#prio 2
:for z from 48 to 55 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("intercon_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=2 queue=ethernet-default}

#prio 1
:for z from 56 to 63 do={/queue tree add burst-limit=0 burst-threshold=0 burst-time=0s disabled=no limit-at=0 max-limit=0 \
name=("netcon_" . $z . "_ether1") packet-mark=("dscp_" . $z . "_eth") parent=ether1 priority=1 queue=ethernet-default}

Remarks

This solution is the most flexible solution I could come up with. It is built around the philosophy that highest DSCP marking is served first. The actual shaping of the interface could be moved to a simple queue in order to be able to police differently on upstream and downstream, but I prefer to shape in both ends of a circuit, so when you have different upload and download speed, you should shape in according to the upload speed.

Further Refinements by BrotherDust

Using the former script as a starting point, I have devised the following script:


#Set interface here
:global outboundInterface "ether1"
#Set bandwidth of the interface (remember, this is for OUTGOING)
:global interfaceBandwidth 0
#Set where in the chain the packets should be mangled
:global mangleChain postrouting

#Don't mess with these. They set the parameters for what is to follow
:global queueName ("qos_" . $outboundInterface)
:global qosClasses [:toarray "netcon,intercon,critical,flash_override,flash,immedate,priority,routine"]
:global qosIndex 64

#Set up mangle rules for all 64 DSCP marks
#This is different in that the highest priority packets are mangled first.
:for indexA from 63 to 0 do={
	/ip firewall mangle add \
	action=mark-packet \
	chain=$mangleChain \
	comment=("dscp_" . $indexA) \
	disabled=no \
	dscp=$indexA \
	new-packet-mark=("dscp_" . $indexA) \
	passthrough=no
}


#Add a base queue to the queue tree for the outbound interface
/queue tree add \
	max-limit=$interfaceBandwidth \
	name=$queueName \
	parent=$outboundInterface \
	priority=1

#Set up queues in queue tree for all 64 classes, subdivided by 8.
:for indexA from=0 to=7 do={
	:local subClass ([:pick $qosClasses $indexA] . "_" . $outboundInterface)
	/queue tree add \ 
		name=$subClass \
		parent=$queueName \
		priority=($indexA+1) \
		queue=ethernet-default
	:for indexB from=0 to=7 do={
		:set qosIndex ($qosIndex-1)
		/queue tree add \
		name=($subClass . "_" . $indexB) \
		parent=$subClass \
		priority=($indexB+1) \
		packet-mark=("dscp_" . $qosIndex) \
		queue=ethernet-default
	}
}

Set the variables accordingly on the globals, paying attention to the comments. This script creates an even more granular priority structure by creating 64 different priorities subdivided by 8 master priorities. So, this is what it will look like this under interface queues when you enter it in the console:

QoS Structure.png

Some usage notes:

1. Remember! The way that this script is set up by default is such that it will only work with outgoing traffic. It's best practices (in my opinion) to keep it set up that way as doing it for incoming traffic would be redundant.

2. If this going to be applied to more than one interface, cut the script up so that it doesn't make the mangle rules again.

3. Bandwidth parameter need not be set. It's just for if you have an interface with fixed bandwidth or you you want to limit that interface. If it is set it must be in bits per second. I have not yet tested this on a wireless interface because the rates are unstable and I want them to be as fast as possible.

Updated on 20090604: I changed the script slightly to reverse the mangling chain. Now highest priority packets are processed first. Probably not going to make a huge difference. But we'll see.

Updated on 20100514: In response to the comments below: OSPF packets with DSCP tag 48 do not get priority 8 globally; rather, they get priority 8 inside of a priority 2 queue. This script creates an extremely granular queue structure to work with. Most people do not need this level of granularity. For the most part they will delete the queues that aren't needed.

Comment on difference between this solution and first solution

Please note that the DSCP tagging strategy here is completely different from that of the first script. Please consider if this fits within your current QoS setup before applying it. For instance, RouterOS automatically tags dynamic routing with DSCP value 48, and following this script, routing updates will have priority 8, which is the lowest priority. In practical network setup, I suggest you only handle the DSCP codes that you know your network is using. My current mangle setup script looks like this:

/ip firewall mangle add action=mark-packet chain=postrouting comment=dscp.0 disabled=no \
dscp=0 new-packet-mark=dscp.0 passthrough=no
/ip firewall mangle add action=mark-packet chain=postrouting comment=dscp.46 disabled=no \
dscp=46 new-packet-mark=dscp.46 passthrough=no
/ip firewall mangle add action=mark-packet chain=postrouting comment=dscp.48 disabled=no \
dscp=48 new-packet-mark=dscp.48 passthrough=no
:for x from 1 to 45 do={/ip firewall mangle add action=mark-packet chain=postrouting \
comment=dscp.1-45 disabled=no dscp=$x new-packet-mark=dscp.other passthrough=no}
/ip firewall mangle add action=mark-packet chain=postrouting comment=dscp.47 disabled=no \
dscp=47 new-packet-mark=dscp.other passthrough=no
:for x from 49 to 63 do={/ip firewall mangle add action=mark-packet chain=postrouting \
comment=dscp.49-63 disabled=no dscp=$x new-packet-mark=dscp.other passthrough=no}

This basically gives you four markings:

  • dscp.0 for packets that have no DSCP tags
  • dscp.46 for EF packets (my VoIP traffic)
  • dscp.48 for routing updates
  • dscp.other for all other DSCP values

I am then able to assemble a queue tree where unmarked packets have the lowest priority, followed by dscp.other, dscp.46 and dscp.48, under the philosophy that routing updates should always be prioritized highest - without them, nothing works, then VoIP, other prioritized packets and lowest of all, non-marked packets.