DSCP Classify - a service for applying DSCP to connections

Creating this thread as a centralised place for discussing my nftables 'dscpclassify' scripts for applying DSCP classifications to connections.

I will update this post with up to date information as the service is enhanced and welcome ideas and people's inputs.

I'd like to thank @ldir and @amteza for their great suggestions and contributions!

I aim soon to incorporate this functionality into a proper package, but in the meantime the steps below should be sufficient to install the functionality.

Link to my git repo:

Associated discussions:

DSCP Classify

An nftables based service for applying DSCP classifications to connections, compatible with OpenWrt's firewall4 for dynamically setting DSCP packet marks (this only works in OpenWrt 22.03 and above).

This should be used in conjunction with layer-cake SQM queue with ctinfo configured to restore DSCP on the device ingress.
The dscpclassify service uses the last 8 bits of the conntrack mark (0x000000ff).

Classification modes

The service supports three modes for classifying and DSCP marking connections.

User rules

The service will first attempt to classify new connections using rules specified by the user in the config file.

These follow a similar syntax to the OpenWrt firewall config and can match upon source/destination ports and IPs, firewall zones etc.

The rules support the use of nft sets, which could be dynamically updated from external sources such as dsnmasq.

Client DSCP hinting

The service can be configured to apply the DSCP mark supplied by a non WAN originating client.

This function ignores CS6 and CS7 classes to avoid abuse from inappropriately configed LAN clients such as IoT devices.

Dynamic classification

Connections that do not match a pre-specified rule will be dynamically classified by the service via two mechanisms:

  • Multi-connection client port detection for detecting P2P traffic
    • These connections are classified as Low Effort (LE) by default and therefore prioritised below Best Effort traffic when using the layer-cake qdisc.
  • Multi-threaded service detection for identifying high-throughput downloads from services such as Steam/Windows Update
    • These connections are classified as High-Throughput (AF13) by default and therefore prioritised as follows by cake:
      • diffserv3/4: Equal to besteffort (CS0) traffic.
      • diffserv8: Below besteffort (CS0) traffic, but above low effort (LE) traffic.

External classification

The service will respect DSCP classification stored by an external service in a connection's conntrack bits, this could include services such as netifyd.

Service architecture


Service installation

To install the dscpclassify service via command line you can use the following:

mkdir -p "/etc/dscpclassify.d"
if [ ! -f "/etc/config/dscpclassify" ]; then
    wget "$repo/etc/config/dscpclassify" -O "/etc/config/dscpclassify"
    wget "$repo/etc/config/dscpclassify" -O "/etc/config/dscpclassify_git"
wget "$repo/etc/dscpclassify.d/main.nft" -O "/etc/dscpclassify.d/main.nft"
wget "$repo/etc/dscpclassify.d/maps.nft" -O "/etc/dscpclassify.d/maps.nft"
wget "$repo/etc/dscpclassify.d/verdicts.nft" -O "/etc/dscpclassify.d/verdicts.nft"
wget "$repo/etc/hotplug.d/iface/21-dscpclassify" -O "/etc/hotplug.d/iface/21-dscpclassify"
wget "$repo/etc/init.d/dscpclassify" -O "/etc/init.d/dscpclassify"
chmod +x "/etc/init.d/dscpclassify"
/etc/init.d/dscpclassify enable
/etc/init.d/dscpclassify start

Ingress DSCP marking requires the SQM queue setup script 'layer_cake_ct.qos' and the package 'kmod-sched-ctinfo'.

To install these via command line you can use the following:

opkg update
opkg install kmod-sched-ctinfo
wget "$repo/usr/lib/sqm/layer_cake_ct.qos" -O "/usr/lib/sqm/layer_cake_ct.qos"
wget "$repo/usr/lib/sqm/layer_cake_ct.qos.help" -O "/usr/lib/sqm/layer_cake_ct.qos.help"

Service configuration

The user rules in '/etc/config/dscpclassify' use the same syntax as OpenWrt's firewall config, the 'class' option is used to specified the desired DSCP.

A working default configuration is provided with the service.

The service supports the following global classification options:

Config option Description Type Default
class_bulk The class applied to threaded bulk clients string le
class_high_throughput The class applied to threaded high-throughput services string af13
client_hints Adopt the DSCP class supplied by a non-WAN client (this exludes CS6 and CS7 classes to avoid abuse) boolean 1
threaded_client_min_bytes The total bytes before a threaded client port (i.e. P2P) is classified as bulk uint 10000
threaded_client_min_connections The number of established connections for a client port to be considered threaded uint 10
threaded_service_min_bytes The total bytes before a threaded service's connection is classed as high-throughput uint 1000000
threaded_service_min_connections The number of established connections for a service to be considered threaded uint 3
lan_device Manually specify devices that the service should treat as LAN string
lan_zone Manually specify firewall zones that the service should treat as LAN string lan
wan_device Manually specify devices that the service should treat as WAN string
wan_zone Manually specify firewall zones that the service should treat as WAN string wan
wmm When enabled the service will mark LAN bound packets with DSCP values respective of WMM (RFC-8325) boolean 0

Below is an example user rule:

config rule
	option name 'DNS'
	list proto 'tcp'
	list proto 'udp'
	list dest_port '53'
	list dest_port '853'
	list dest_port '5353'
	option class 'cs5'
	option counter '0'

The OpenWrt firewall syntax is outlined here https://openwrt.org/docs/guide-user/firewall/firewall_configuration

The counter option can be enabled to count the number of matched connections for a rule.

SQM configuration

The 'layer_cake_ct.qos' queue setup script must be selected for your wan device in SQM setup,

It is important that Squash DSCP and Ignore DSCP on ingress are not enabled in SQM setup otherwise cake will ignore the service's DSCP classes.


Below is a tested working SQM config for use with the service:

Config parameter Value
qdisc_advanced 1
squash_dscp 0, to ensure cake does not remove ingress packet DSCP values
squash_ingress 0, to ensure cake looks at packet marks on ingress
qdisc_really_really_advanced 1
iqdisc_opts nat dual-dsthost ingress diffserv4
eqdisc_opts nat dual-srchost ack-filter diffserv4
script layer_cake_ct.qos

Great work!

Question: you have a WMM node in which you seem to over-write DSCPs so that intent maps to the the right (we can argue about that :wink: *) AC for each DSCP. I would argue that instead of re-writing DSCPs over and over again, we might be better of using qos_map_set to configure the desired DSCP to AC mapping for the AP and all stations instead?

*) Neither AC_VI and especially not AC_VO are without considerable side-effects on "normal" traffic in AC_BE (both by taking weighted priority in air access as well as reducing the total WiFi segment capacity by limiting aggregation duration for AC_VO), so it is arguable whether one should actually move all that much traffic to the higher classes. I know that https://www.rfc-editor.org/rfc/rfc8325 seems to endorse such usage of ACs, but I note that rfc8325 is pretty thin on studies actually showing the consequences of doing so... so IMHO rfc8325 is decent information how to map different PHBs to WiFi but one first should figure out whether one wants to give a specific PHB special treatment over WiFi in the first place.

Fair points, I know we've discussed before too :slight_smile: I make use of it in my setup as my mesh APs are running stock firmware.
What I can do is make this off by default and then it's down to the user to decide whether they want to use it with their setup (which feels like a good compromise given the RFC's draft status).


Good argument! I had only considered the full OpenWrt APs case here and forgot about non-qos-map configurable APs at all.

That would be a safer default IMHO that still allows users to opt in (I am a big fan of opt-in, but also of sane defaults and here the best path is unclear to me).

great script thanks again,

I don't know yet if there is counter addition in your repository ?

to be sure and help people in sqm we have to put like your image

DO not Squash


ECN (by default )


I also noticed a problem with your repository

if I take from github on your repository my rules

in the chain static class don't work all

if I take from Ldir in these cases it works all I put you in illustration the error :wink:

type or paste code here

Yes I've added the counter option now :slightly_smiling_face:

You can enable counters for a particular rule by adding the option

option counter '1'

That's correct yes: do not squash, allow.

You can leave the ECN settings at their default values.

  chain static_classify {
                meta l4proto { tcp, udp } th dport { 53, 853, 5353 } goto ct_set_cs2 comment "DNS"
                ip saddr tcp dport 1935 counter packets 0 bytes 0 goto ct_set_cs3 comment "twitch1"
                ip dscp != { cs0, cs6, cs7 } iifname != { "wan" } (@nh,8,8 & 0xfc) >> 2 vmap @dscp_ct
                ip6 dscp != { cs0, cs6, cs7 } iifname != { "wan" } (@nh,0,16 & 0xfc0) >> 6 vmap @dscp_ct
                meta l4proto != { tcp, udp } goto ct_set_cs0
                ct mark set ct mark & 0xffffff80 | 0x00000080

my rules for consoles game ps5 doesn't appair so if i take to Ldir he appair i don't know if you undesrtand ?

config rule
        option name 'PS5udp'
        list proto 'udp'
        list src_ip ''
	list dest_port '!80'
	list dest_port '!443'
	option class 'cs4'
        option family 'ipv4'
	option counter '1'

i has just dns and twitch1

1 Like

Ahh yes I see you're making use of the negative matching present in the latest commit from @amteza in @ldir's fork.
I'm looking to merge that functionality into the main branch in a couple of hours, will update back here once it's ready. :slightly_smiling_face:


ok great so I could take from your repository

on the hand or developped ??

the most recent I could see in terms of time

and thanks for the ECN

ECN (by default) the first one
and NOECN (by default) the second one :slight_smile:

and not "ECN" the second

cake will ignore the ECN setting in the GUI and always use ECN if the packets have either ECT(0) or ECT(1) set. (Or cake is already in emergency drop mode or the respective flow triggered cake's BLUE mode which IIRC drops hard).

1 Like

It's not committed to develop yet.


That’s what I also suggested in my thread as new users could get confused if their static marks are overwritten when wmm is enabled.

One thing that always comes to my mind when i think about dscpclassify is: do you inted to make a luci app for dscpclassify? Or could it maybe be integrated into the luci firewall?

Setting dscp through luci firewall would probably already work with your script. I have to try that…



yes it would be a great idea if by luci we could put it as hudra suggests but I think the work would be long?
if we could integrate our dscpclassify parameters like in my thread

it would be christmas for the first time in such a short time :sweat_smile:

yes exactly :wink:

I will wait for your last merge to update the script,

@Hudra yes I also have the same little problem as you the counter seems to be blocked especially for twitch1

 ip saddr tcp dport 1935 counter packets 1 bytes 60 goto ct_set_cs3 comment "twitch1"

Actually, I don’t have any problem, maybe you misunderstood something:

In this post I just wanted to make clear that I think it would be better to disable the option wmm by default. But I was too slow as @yelreve already fixed it in his repository.

If you are referring to my thread where I asked why my counters are not increasing by much when the option “counter” is set here is your answer:

So, if you have 1 packet in your counter it probably works and you won’t ever see counters increasing for every packet. Only for every flow… at least unless there is an option to use counters from conntrack but I don’t know if this is possible.


I've updated the main branch now with the negative matching ability, I took it one step further to allow mixing both negative and positive matches on a single rule (though probably a fairly niche use case).

@Dopam-IT_1987 I think your config in thread Sharing dscpclassify configuration on OpenWrt should now work as expected with the main code branch. :slight_smile:

It also includes the auto-merge tweak for sets with intervals.

@Hudra you're correct, the rule counters only count matched connections (not every packet in the connection).


may i ask what negative matching mean ??

It allows you to write a rule which would match everything except the entries you've specified with an '!'.
For example the below would match all tcp/udp connections that did not have a destination port of 443 or 80.

config rule
	option name 'Not HTTP'
	list proto		'tcp'
	list proto		'udp'
	list dest_port	'!443'
	list dest_port	'!80'
	option class 'cs4'

is it the same as writing nowash in iqdisc_opts and eqdisc_opts right ??
what if i only wash on egress ?

In SQM setup those two parameters correspond to the following options

Squash DSCP on inbound packets (ingress) = squash_dscp
Ignore DSCP on ingress = squash_ingress

You're correct, setting squash_dscp to 0 (allow) is equivalent to nowash in iqdisc_opts.

Enabling squash/wash has the effect of making cake overwrite the DSCP to 0 after passing through the tc qdisc, however DSCP classify would re-apply it from the conntrack for lan bound packets.

squash_ingress doesn't seem to actually do anything in my testing this morning. *Edit: per @moeller0's note below this is because I'm specifying a diffserv in my qdisc_opts.

Washing on WAN egress via eqdisc_opts would make sure your DSCPs are all set to CS0 when sent to your ISP after passing through the qdisc - that could be beneficial depending on how your ISP handles different DSCP marks.