DSCP policies for SQM (CAKE) with real-time conntrack visibility

Hi everyone,

I’d like to share a small experimental project built around SQM + CAKE,
focused on DSCP-based traffic policies and real-time visibility.

The project consists of two independent parts:

  1. A custom SQM script (CAKE-based)

    • Uses nftables to apply DSCP policies
    • Stores DSCP values into conntrack
    • Restores DSCP on ingress via tc ctinfo
    • Fully driven by SQM lifecycle hooks (start/stop)
  2. A read-only LuCI view: “DSCP Connections”

    • Real-time conntrack-based traffic view
    • Shows source/destination, ports, and protocol
    • Decodes DSCP classes (CSx / AFxx / EF)
    • Displays live PPS / BPS statistics
    • Purely observational (no traffic interception)

The DSCP Connections view works independently from SQM
and can be used to validate DSCP markings and CAKE DiffServ behavior.

This is NOT meant to replace SQM, fw4, or existing QoS solutions.
It is an experimental extension intended for:

  • latency-sensitive traffic analysis
  • DSCP validation
  • learning and testing purposes

Repository (early-stage):

Tested on OpenWrt 24.10+.

Feedback, design critique, and testing reports are very welcome.

2 Likes

Hey, can you peek at qosmate and share your opinions where to improve it? Seems both are working in same direction :wink:

Consider trying to make this generic enough to be incorporated into sqm-scripts as a DSCP/ctinfo option.

Respectfully, I think sqm-scripts might take the "configure the required tc action ctinfo" part and maybe the "required nftables bits to store the egress DSCP into the conntrack database" but anything beyond that is IMHO clearly outside sqm-script's wheelhouse...

1 Like

Yes, that’s what I was thinking when I mentioned “generic”, but didn’t mention any specifics. After Qosify, DSCPClassify, QoSmate, and others, the market for such scripts is saturated (IMHO). Getting the basic ctinfo mechanism into sqm-scripts will allow people to setup regular fw4 rules to mark traffic with DSCP for basic usage.

We had a similar discussion before:

Thanks for the feedback, that makes sense and I agree with most of it.

My goal with this project was not to propose yet another competing QoS framework, nor to replace existing solutions like qosify, QoSmate, or fw4-based setups.

What I was trying to address is a slightly different angle:
• SQM (and luci-app-sqm) is already widely installed and familiar to many users
• Many users are uncomfortable writing custom nftables or fw4 rules
• There is often no easy way to observe whether DSCP markings actually survive end-to-end and are honored by CAKE

The custom SQM script and LuCI additions are therefore meant as a simple, opinionated workflow:
• define a small set of latency-sensitive endpoints (IPv4/IPv6)
• apply DSCP in a controlled way
• immediately validate the result via a read-only conntrack view

I fully agree that the generic ctinfo mechanism (store egress DSCP into conntrack and restore on ingress) could be useful as a reusable building block, possibly within sqm-scripts, while higher-level policy logic and UI probably belong outside of it.

For now, this project is mainly intended as:
• a learning and experimentation tool
• a reference implementation of ctinfo + DSCP + CAKE
• a convenience layer for users already relying on luci-app-sqm

I appreciate the discussion and pointers they help clarify where this fits in the broader ecosystem.

2 Likes

Hi,

Just a quick update since the last discussion. I continued experimenting with the idea and added a few improvements that might make the setup easier to test and reproduce.

Recent changes include:

• optional CAKE multi-queue (cake_mq) support for multi-core hardware
• improved DSCP classification pipeline using nftables prerouting for download traffic
• optional ctinfo DSCP restore from conntrack
• separate UDP and TCP priority classification
• improved installer with automatic opkg / apk detection (OpenWrt 24.x / 25.x)
• a small LuCI view to observe active DSCP connections in real time

The goal is still not to compete with qosify or QoSmate, but rather to provide a simple way to experiment with ctinfo + DSCP + CAKE inside the familiar SQM workflow.

It also serves as a small reference implementation showing how nftables DSCP marking, conntrack storage and ctinfo restoration interact with CAKE.

The project is available here if anyone is interested in testing or experimenting with it:

Any feedback is welcome.

1 Like

Why dont you offer your hand to upstream sqm.scripts ?

Hi everyone,

It's been a few months since the last update on this thread (March), and
the project has evolved significantly since then. I wanted to give a
proper summary rather than just announce the latest release, because the
architecture and feature set have grown well beyond what I described
initially.

For context, the project sits between vanilla sqm-scripts and the more
opinionated tools like qosify/QoSmate. It's still meant as a small
reference implementation showing how nftables DSCP marking, conntrack
storage, and ctinfo restoration interact with CAKE — but it's now stable
enough for daily production use on my own setup.

──────────────────────────────────────────────────
What the project does today
──────────────────────────────────────────────────

Backend (Seg_Layer_Cake.qos):

  • Custom SQM script extending sqm-scripts with nftables-based DSCP
    classification (IPv4 + IPv6)
  • Dual-mode ingress pipeline:
    • ctinfo ON → postrouting marking + kernel ctinfo restore (efficient)
    • ctinfo OFF → prerouting marking before IFB redirect (no ctmark)
  • Per-device priority classification with configurable UDP/TCP ports
  • Micro-packet boost (<150 bytes → CS6/CS7/EF) and small-packet
    promotion (150–300 bytes → priority DSCP)
  • Big HTTPS auto-downgrade (>1000 bytes on TCP/UDP 443 → CS1) —
    prevents QUIC/H3 elephants from contaminating the gaming tin
  • Optional CAKE multi-queue (cake_mq) for multi-core routers
  • Browsing classification — only marks pure CS0 traffic (non-destructive)
  • Bulk classification with strict port gating
  • Clean nftables setup/teardown via SQM lifecycle hooks (table inet sqm_dscp)

LuCI integration:

  • DSCP Policies tab in SQM configuration with strict client-side
    validation on all port-list fields
  • Real-time DSCP Connections viewer (Network → DSCP → Connections)
    • Live conntrack-based monitoring, IPv4+IPv6
    • DSCP class decoding with colour coding
    • Adaptive polling (1–10s) based on router load
    • Multi-term filter with AND logic
    • Pause/Resume, zoom, column sorting
  • One-click "Flush Conntrack" button — reads gaming_ip/gaming_ip6 from
    UCI at click time, asks for confirmation, reports flushed flow count

Tooling:

  • tools/dscp-validate.sh — 19-section read-only validation report
    covering nftables rule counters, CAKE per-tin latency/drops,
    conntrack health, and a final verdict line
  • Auto-detects WAN/IFB and priority IPs from UCI (works on any setup)
  • --anonymize flag masks IPs and hostname for safe public sharing
    (useful when posting reports here or in GitHub issues)

Installer:

  • Auto-detects opkg/apk (OpenWrt 24.x / 25.x)
  • Disk space check, source files validation, WAN auto-detection
  • Backup of original sqm.js / sqm.config as .orig before overwriting
  • Dry-run and force modes
  • Matching uninstaller that restores everything cleanly

──────────────────────────────────────────────────
Production validation
──────────────────────────────────────────────────

Tested over multiple days on R86S i3-N305 / OpenWrt 25.12.2 / kernel
6.12.74, on a 1400/1200 Mbit init7 link (Geneva):

  • 14.5 GB shaped (download), 0.005% drops overall (Bulk only — by design)
  • CAKE Voice tin: 12 µs peak latency, 7 µs avg, zero drops on gaming UDP
  • Priority UDP IPv4: 1.5M packets correctly classified to CS4
    (Call of Duty on UDP/3074)
  • Bulk TCP: 14.9M packets classified to CS1
  • Big HTTPS auto-downgrade: 10 GB captured before reaching Voice tin
  • Browsing classification: 300K packets reclassified from CS0 to AF21
    (LAN devices, non-destructive)
  • LibreQoS bufferbloat test: A+ rating maintained

──────────────────────────────────────────────────
Design decisions I'm uncertain about
──────────────────────────────────────────────────

In case anyone has thoughts (would genuinely appreciate input):

  1. Using meta length thresholds (<150 / 150–300 / >1000 bytes) for
    small/big packet classification rather than rate-limiting. Works
    well in practice on real traffic, but feels coarse — wondering if
    anyone has tried both approaches and seen meaningful differences.

  2. The "big HTTPS downgrade" (>1000 bytes on 443 → CS1) is a pragmatic
    hack for QUIC/H3 elephants from gaming devices that legitimately use
    port 443 for both small and large flows. Open to more elegant ideas.

  3. Keeping the custom LuCI sqm.js as a complete replacement (with .orig
    backup) rather than patching upstream incrementally — to avoid
    maintenance debt against the upstream sqm-scripts tree, but I lose
    the ability to track upstream changes automatically. Curious how
    others have handled this trade-off.

  4. The Flush Conntrack button uses conntrack -D -s/-d for both IPv4 and
    IPv6, with shell-metacharacter input validation. It feels safe but
    I'd welcome a security review of the rpcd backend approach.

──────────────────────────────────────────────────
Links
──────────────────────────────────────────────────

Repo: https://github.com/Segalmen/openwrt-custom
Release: https://github.com/Segalmen/openwrt-custom/releases/tag/v1.4
Install: see the README for the one-liner

The project tries hard not to override anything outside its own scope:
no firewall rules touched, the nftables table sqm_dscp is fully managed
by the SQM lifecycle, and original LuCI files are backed up before
replacement.

As always, feedback, criticism and suggestions are very welcome.


root@R86S_i3-N305:~# /root/dscp-validate.sh --anonymize | tee /root/dscp-report-public.txt
====================================================================
          openwrt-custom — DSCP / SQM / CAKE VALIDATION
====================================================================
  Date         : 2026-04-25 16:02:30
  Host         : <anonymized-host>
  OpenWrt      : OpenWrt 25.12.2 r32802-f505120278
  Kernel       : 6.12.74
  Mode         : ANONYMIZED (safe to share publicly)
====================================================================


--- 1. Interfaces detected ---
  WAN interface  : eth3
  IFB interface  : ifb4eth3

--- 2. UCI SQM configuration (active) ---
  sqm.@queue[0]=queue
  sqm.@queue[0].enabled='1'
  sqm.@queue[0].interface='eth3'
  sqm.@queue[0].download='1400000'
  sqm.@queue[0].upload='1200000'
  sqm.@queue[0].qdisc='cake'
  sqm.@queue[0].script='Seg_Layer_Cake.qos'
  sqm.@queue[0].qdisc_advanced='1'
  sqm.@queue[0].ingress_ecn='ECN'
  sqm.@queue[0].egress_ecn='ECN'
  sqm.@queue[0].qdisc_really_really_advanced='1'
  sqm.@queue[0].itarget='auto'
  sqm.@queue[0].etarget='auto'
  sqm.@queue[0].linklayer='none'
  sqm.@queue[0].debug_logging='0'
  sqm.@queue[0].verbosity='5'
  sqm.@queue[0].squash_dscp='0'
  sqm.@queue[0].squash_ingress='0'
  sqm.@queue[0].iqdisc_opts='nat diffserv4 dual-dsthost ack-filter rtt 1ms memlimit 64mb'
  sqm.@queue[0].eqdisc_opts='nat diffserv4 dual-dsthost ack-filter rtt 1ms memlimit 64mb'
  sqm.@queue[0].enable_mq='1'
  sqm.@queue[0].ctinfo_enable='1'
  sqm.@queue[0].gaming_ip='<USER-IPV4>'
  sqm.@queue[0].gaming_ip6='<USER-IPV6-PREFIX>'
  sqm.@queue[0].priority_udp_enable='1'
  sqm.@queue[0].priority_tcp_enable='1'
  sqm.@queue[0].browsing_enable='1'
  sqm.@queue[0].gaming_bulk_enable='1'
  sqm.@queue[0].priority_udp_dscp='cs4'
  sqm.@queue[0].gaming_udp_ports='3659,3074,3478-3480,10000-45000'
  sqm.@queue[0].priority_tcp_dscp='af41'
  sqm.@queue[0].gaming_tcp_ports='3659,3074'
  sqm.@queue[0].gaming_bulk_dscp='cs1'
  sqm.@queue[0].micro_pkt_enable='1'
  sqm.@queue[0].micro_pkt_dscp='cs6'
  sqm.@queue[0].big_https_downgrade='1'
  sqm.@queue[0].browsing_tcp_ports='80,8080,443'
  sqm.@queue[0].browsing_udp_ports='443'
  sqm.@queue[0].browsing_dscp='af21'

--- 3. nftables table 'sqm_dscp' status ---
  ✓ Table sqm_dscp is LOADED
  Total rules with comments: 42

--- 4. Priority UDP rules (gaming traffic → CS4 by default) ---
  PrioUDPup4            pkts=929084        bytes=159 MB
  PrioUDPdwn4           pkts=597214        bytes=143 MB
  PrioUDPup6            pkts=0             bytes=0 B
  PrioUDPdwn6           pkts=0             bytes=0 B

--- 5. Priority TCP rules (gaming traffic → AF41 by default) ---
  PrioTCPup4            pkts=0             bytes=0 B
  PrioTCPdwn4           pkts=0             bytes=0 B
  PrioTCPup6            pkts=0             bytes=0 B
  PrioTCPdwn6           pkts=0             bytes=0 B

--- 6. Bulk TCP rules (web/HTTPS → CS1) ---
  BulkTCPup4            pkts=1140157       bytes=85 MB
  BulkTCPdwn4           pkts=2044738       bytes=2 GB
  BulkTCPup6            pkts=4424370       bytes=470 MB
  BulkTCPdwn6           pkts=7313762       bytes=9 GB

--- 7. Bulk UDP rules (UDP web → CS1) ---
  BulkUDPup4            pkts=0             bytes=0 B
  BulkUDPdwn4           pkts=0             bytes=0 B
  BulkUDPup6            pkts=64812         bytes=50 MB
  BulkUDPdwn6           pkts=151796        bytes=130 MB

--- 8. Big HTTPS auto-downgrade (>1000 bytes → CS1) ---
  BigHTTPSup4           pkts=10140         bytes=20 MB
  BigHTTPSdwn4          pkts=188791        bytes=267 MB
  BigHTTPSup6           pkts=51085         bytes=88 MB
  BigHTTPSdwn6          pkts=7136062       bytes=9 GB

--- 9. MicroPkt UDP rules (<150 bytes on gaming ports → CS6) ---
  MicroPktUDPup4        pkts=0             bytes=0 B
  MicroPktUDPdwn4       pkts=0             bytes=0 B
  MicroPktUDPup6        pkts=0             bytes=0 B
  MicroPktUDPdwn6       pkts=0             bytes=0 B

--- 10. SmallPkt UDP rules (150-300 bytes on gaming ports → priority DSCP) ---
  SmallPktUDPup4        pkts=0             bytes=0 B
  SmallPktUDPdwn4       pkts=0             bytes=0 B
  SmallPktUDPup6        pkts=0             bytes=0 B
  SmallPktUDPdwn6       pkts=0             bytes=0 B

--- 11. MicroPkt TCP rules ---
  MicroPktTCPup4        pkts=0             bytes=0 B
  MicroPktTCPdwn4       pkts=0             bytes=0 B
  MicroPktTCPup6        pkts=0             bytes=0 B
  MicroPktTCPdwn6       pkts=0             bytes=0 B

--- 12. SmallPkt TCP rules ---
  SmallPktTCPup4        pkts=0             bytes=0 B
  SmallPktTCPdwn4       pkts=0             bytes=0 B
  SmallPktTCPup6        pkts=0             bytes=0 B
  SmallPktTCPdwn6       pkts=0             bytes=0 B

--- 13. Browsing rules (CS0 web traffic → AF21) ---
  BrowsTCP4             pkts=337481        bytes=48 MB
  BrowsTCP6             pkts=61            bytes=13 KB
  BrowsUDP4             pkts=1789          bytes=721 KB
  BrowsUDP6             pkts=0             bytes=0 B

--- 14. Conntrack mark storage (DSCP→ctmark) ---
  StoreIPv4ctmark       pkts=6024568       bytes=4 GB
  StoreIPv6ctmark       pkts=12103492      bytes=10 GB

--- 15. CAKE qdisc summary ---

  >>> UPLOAD direction (dev eth3) <<<
    qdisc cake_mq 8040: root bandwidth 1200Mbit diffserv4 dual-dsthost nat nowash ack-filter split-gso rtt 1ms raw overhead 0 memlimit 64Mb
     Sent 979863330 bytes 7139076 pkt (dropped 462, overlimits 44348 requeues 0)
     backlog 0b 0p requeues 0
    qdisc cake 0: parent 8040:8 bandwidth 1200Mbit diffserv4 dual-dsthost nat nowash ack-filter split-gso rtt 1ms raw overhead 0 memlimit 64Mb
     Sent 135749149 bytes 1059818 pkt (dropped 19, overlimits 4251 requeues 0)

  >>> DOWNLOAD direction (dev ifb4eth3) <<<
    qdisc cake_mq 8042: root bandwidth 1400Mbit diffserv4 dual-dsthost nat nowash ack-filter split-gso rtt 1ms raw overhead 0 memlimit 64Mb
     Sent 14658539873 bytes 13084667 pkt (dropped 647795, overlimits 8540238 requeues 0)
     backlog 0b 0p requeues 0
    qdisc cake 0: parent 8042:8 bandwidth 1400Mbit diffserv4 dual-dsthost nat nowash ack-filter split-gso rtt 1ms raw overhead 0 memlimit 64Mb
     Sent 2216018599 bytes 1609160 pkt (dropped 123623, overlimits 1494460 requeues 0)

--- 16. CAKE per-tin latency (Voice = gaming tin) ---

  >>> UPLOAD (eth3) <<<
                       Bulk  Best Effort        Video        Voice
      thresh      18750Kbit      300Mbit      150Mbit       75Mbit
      target          242us         50us         50us         60us
      interval       1.19ms          1ms          1ms       1.01ms
      pk_delay         93us         11us         98us         10us
      av_delay          9us          4us         15us          7us
      backlog            0b           0b           0b           0b
      pkts           933861        20234        35624        70118
      bytes       112168296      2066074      5893875     15630828
      drops               0            0            6            0
      marks               0            0            0            0
      max_len         28760         4542        16390         1301

  >>> DOWNLOAD (ifb4eth3) <<<
                       Bulk  Best Effort        Video        Voice
      thresh      29166Kbit   466666Kbit   233333Kbit   116666Kbit
      target          207us         50us         50us         51us
      interval       1.16ms          1ms          1ms          1ms
      pk_delay        328us         11us        250us         11us
      av_delay         94us          7us         38us          7us
      backlog            0b           0b           0b           0b
      pkts          1621414        25085        60286        25998
      bytes      2306497784      7490242     76347725      3049486
      drops          123531           31           60            0
      marks               0            1            5            0
      max_len         68832        42280        66440         1851


--- 17. Overall packet count summary ---
  Upload   sent: 934 MB    dropped: 462 pkts
  Download sent: 13 GB  dropped: 647795 pkts

--- 18. Conntrack statistics for priority device ---
  Priority IPv4 : <USER-IPV4>
  Priority IPv6 : <USER-IPV6-PREFIX>
  Active conntrack entries (IPv4 src=<USER-IPV4>): 0
  Active conntrack entries (IPv4 dst=<USER-IPV4>): 0
  Active conntrack entries (IPv6 prefix <USER-IPV6-PREFIX>): 24

--- 19. ctinfo / kernel module status ---
  ct_dscpremark module : 1 loaded matches
  UCI ctinfo_enable    : 1 (1=ON, 0=OFF)

====================================================================
  Quick health check :

  ✓ Priority UDP IPv4 has matched 1526298 packets (gaming detected)
  ✓ Bulk TCP has matched 14923027 packets (web traffic classified)

  [ANONYMIZED MODE — IPs and hostname masked for safe public sharing]
  End of report. Generated 2026-04-25 16:02:30
====================================================================

1 Like