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):
-
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.
-
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.
-
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.
-
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
====================================================================