By WickedYoda
Tested on: Flint 3, Flint 2, Flint 4 / GL-BE14000, Slate 7, Slate 7 Pro, Beryl 7, Flint 3e / GL-BE6500
Repo: https://github.com/wickedyoda/Glinet-Bandwidth-script
If you’ve ever had one person on your network max out every bit of upload bandwidth while everyone else’s video calls stuttered, you’ve felt the pain of “flat” networking. Every device gets equal treatment — until one of them doesn’t.
This weekend I built a script that changes that. It gives each VLAN and SSID on your GL.iNet router its own bandwidth bucket with a configurable priority, so your work laptops can keep working even when someone else is uploading a 4K video.
Here’s what QoS and SQM actually mean, and how the script implements them on OpenWrt.
Part 1: What QoS and SQM Actually Are
QoS (Quality of Service)
QoS is traffic shaping with priority. Imagine your internet connection is a highway:
- Without QoS: Every car drives in the same lane. One semi-truck going slow blocks everything behind it.
- With QoS: You get an HOV lane for emergency vehicles, a passing lane for fast traffic, and a slow lane for trucks. Each lane has its own speed limit, but they all share the same road.
In router terms, QoS divides your WAN upload/download bandwidth into queues (HTB classes) and assigns each queue a priority. High-priority traffic (VoIP, SSH, gaming) gets forwarded first. Low-priority traffic (background updates, cloud backups) gets the leftovers.
The two most common QoS mechanisms on OpenWrt:
| Mechanism | What it does | Best for |
|---|---|---|
| HTB (Hierarchical Token Bucket) | Divides bandwidth into parent/child classes with hard rate limits and priorities | Per-VLAN or per-device priority |
| fq_codel | Fights bufferbloat by dynamically managing queue depth | Per-queue fairness within a class |
SQM (Smart Queue Management)
SQM is a specific type of QoS that focuses on one thing: reducing bufferbloat.
Bufferbloat happens when your router’s queue fills up with packets waiting to be sent. A single large download can fill the queue, and every other packet — even a tiny VoIP packet — has to wait behind it. The result: ping spikes from 20ms to 500ms+ while a download is running.
SQM solves this with CAKE (Common Applications Kept Enhanced), which:
- Measures your actual WAN bandwidth (not the ISP-advertised number)
- Sets a rate limit slightly below your real throughput
- Uses a smart queueing discipline that prioritizes small, interactive packets
- Keeps ping stable regardless of how much bandwidth is being used
The net result: your download can max out your connection and your Zoom call stays smooth.
The Key Difference
| Feature | QoS | SQM |
|---|---|---|
| Granularity | Per-VLAN, per-interface, per-device | Per-WAN interface |
| Priority | Explicit priority levels (high/med/low) | Implicit — small packets win |
| Best use case | Multi-VLAN homes, offices, IoT segregation | Single-WAN bufferbloat reduction |
| Complexity | Higher — requires class/queue config | Lower — one setting per WAN |
Most people need both: SQM for the WAN link, and QoS to make sure the guest network doesn’t eat the IoT network’s lunch.
Part 2: How the Script Modifies QoS/SQM for Priority
What the script does
The script (glinet-vlan-qos.sh) is a pure shell/OpenWrt implementation. No Python, no Node, no extra dependencies beyond what’s already on the router. It touches three subsystems:
- tc — traffic control (HTB qdisc + classes + CAKE leaves)
- nft — firewall marks (classify traffic by ingress interface)
- Persistence hooks — /etc/gl-switch.d/ and /etc/rc.local
The HTB hierarchy
When you run glinet-vlan-qos.sh start, it builds an HTB tree on each bridge/interface:
root qdisc (handle 1: on br-lan, 2: on br-iot, 3: on br-guest, 4: on tailscale0)
├── class 1:1 — parent/root class, rate = full interface bandwidth
│ ├── class 1:10 — LAN + Tailscale, prio 1 (highest)
│ │ └── CAKE leaf (besteffort triple-isolate) — fq_codel-style fairness
│ ├── class 1:20 — IoT, prio 2 (medium)
│ │ └── CAKE leaf (besteffort)
│ └── class 1:30 — Guest, prio 3 (lowest)
│ └── CAKE leaf (besteffort)
The priority numbers (prio 1, prio 2, prio 3) are what control the actual scheduling order. HTB always dequeues from the lowest-numbered class first. So if LAN and Guest both have packets waiting, LAN goes first.
Each class has a rate (guaranteed minimum) and a ceil (maximum burst). By default on a Flint 3:
| Class | Rate | Ceil | Priority |
|---|---|---|---|
| LAN + Tailscale | 200 Mbps | 200 Mbps | 1 (highest) |
| IoT | 50 Mbps | 50 Mbps | 2 |
| Guest | 20 Mbps | 20 Mbps | 3 (lowest) |
These are configurable in /etc/gl-qos-vlan.conf — edit the QOS_*_BW_UP and QOS_*_BW_DOWN variables to whatever your plan actually delivers.
The nft marks
HTB classes handle egress shaping, but we also need to make sure packets are classified into the right class when they enter the router. That’s where nft comes in:
table inet gl-qos {
chain preraw {
type filter hook prerouting priority mangle; policy accept;
iifname “br-iot” meta mark set 0x00020000 → IoT class
iifname “br-guest” meta mark set 0x00030000 → Guest class
iifname “tailscale0” meta mark set 0x00040000 → Tailscale class
iifname “br-lan” meta mark set 0x00010000 → LAN class
}
}
When a packet arrives on br-iot, the nft rule stamps it with the mark 0x00020000. The HTB u32 filter (on Flint 3 / tc-full) then matches that mark and routes the packet into class 1:20. On Flint 2-class devices without u32 support, the classification happens at the bridge level instead — the script detects this automatically and skips the u32 filter.
The CAKE leaf
Each HTB class gets a CAKE qdisc attached as a leaf. CAKE isn’t just for WAN — it works on LAN bridges too, and it provides:
- Flow isolation: Each TCP/UDP flow gets its own internal queue, so one device can’t monopolize a class
- AQM: Active Queue Management drops packets before the queue fills, keeping latency low
- Triple-isolate mode: On the LAN class, this isolates flows by both source and destination, preventing cross-device interference
SQM mode
If you choose SQM mode in the setup wizard, the script skips the per-VLAN HTB hierarchy entirely and instead applies a single CAKE qdisc on the WAN interface (eth0 or wan). The rate is set to 95% of your measured WAN bandwidth to avoid oversubscription.
SQM mode is simpler but doesn’t give you per-VLAN priority. It’s ideal if you only have one flat LAN and just want the bufferbloat gone.
Persistence
The script offers two persistence options:
Option 1 — Persistent (PERSISTENT=1, default):
- Creates /etc/gl-switch.d/vlan-qos.sh — GL.iNet’s network-restart hook runs this after every network change
- Appends a start call to /etc/rc.local — runs on boot
- Survives firmware upgrades, UI config changes, and network restarts
- Does not survive factory reset (expected)
Option 2 — Manual only (PERSISTENT=0):
- No auto-start hooks installed
- Survives UI config changes but not reboots
- Use for testing or temporary QoS
Part 3: Testing on Multiple Models
I tested this over the weekend on every GL.iNet model I could get my hands on. The script auto-detects the model at runtime and adjusts accordingly.
What works on every model
- HTB root qdisc on br-lan, br-iot, br-guest
- HTB classes with rate/ceil/prio
- CAKE leaf qdiscs
- nft mark rules
- /etc/gl-qos-vlan.conf configuration file
- Persistence hooks
- start|stop|restart|status|detect|install|uninstall commands
Tested Model-specific differences
| Model | OpenWrt | tc variant | u32 filters | Notes |
|---|---|---|---|---|
| Flint 3 | 23.05-SNAPSHOT | tc-full | ✅ Yes | Full feature set, tested extensively |
| Flint 2 | 21.02-SNAPSHOT | tc-tiny | ❌ No | Falls back to bridge-level classification |
| Flint 4 / GL-BE14000 | 21.02-SNAPSHOT | tc-tiny | ❌ No | Same path as Flint 2 |
| Slate 7 | 23.05-SNAPSHOT | tc-full | ✅ Yes | Same as Flint 3 |
| Slate 7 Pro / GL-BE10000 | 21.02-SNAPSHOT | tc-tiny | ❌ No | Same as Flint 2 |
| Beryl 7 / GL-MT3600BE | 21.02-SNAPSHOT | tc-tiny | ❌ No | Same as Flint 2 |
| Flint 3e / GL-BE6500 | 23.05-SNAPSHOT | tc-full | ✅ Yes | Same as Flint 3 |
The tc-tiny vs tc-full distinction matters because tc-tiny strips out the u32 filter module to save flash space. The script detects this at runtime — if tc filter add … u32 fails, it falls back to nft-based classification only. No user intervention needed.
Verification
After applying QoS, run:
glinet-vlan-qos.sh status
You should see:
- HTB qdisc on each bridge
- CAKE leaf qdiscs on each class
- nft table inet gl-qos with mark rules
Or check manually:
# HTB classes
tc class show dev br-lan
# CAKE qdiscs
tc qdisc show dev br-lan
# nft marks
nft list table inet gl-qos
Part 4: What This Means in Practice
Here’s a real scenario. My home network has:
- br-lan: Work laptops, gaming PC, phones — needs low latency
- br-iot: Smart bulbs, thermostats, cameras — tolerates delays
- br-guest: Visitors’ devices — lowest priority, bandwidth-gated
- tailscale0: Remote admin/mesh VPN — same priority as LAN
Without QoS, a firmware update on 12 IoT devices could saturate upload and make SSH sessions lag. With QoS:
- SSH into the router stays snappy (LAN class, prio 1)
- IoT devices can still talk to their cloud services (IoT class, 50 Mbps guaranteed)
- A guest streaming Netflix gets shaped to 20 Mbps (Guest class, prio 3)
- Tailscale tunnels stay responsive (same class as LAN)
The bandwidth numbers are configurable per-model. The defaults are conservative — you can tune them to match your actual plan.
Part 5: Installation
# One-liner
curl -fsSL https://raw.githubusercontent.com/wickedyoda/Glinet-Bandwidth-script/main/install.sh | sh
# Run the setup wizard
glinet-vlan-qos-setup.sh
The wizard walks you through model selection, mode selection (QoS vs SQM), persistence, and VLAN/SSID priority ordering.
Requirements
- GL.iNet Flint 2, Flint 3, Flint 4, Slate 7, Slate 7 Pro, Beryl 7, or Flint 3e
- OpenWrt 21.02+
- tc-full or tc-tiny, kmod-sched-cake, sqm-scripts
- Root SSH access
Uninstall
glinet-vlan-qos.sh uninstall
Removes the script, persistence hooks, HTB qdiscs, and nft marks.
Part 6: The Weekend Testing Notes
This wasn’t a quick hack. I spent the actual weekend testing on real hardware:
- Model auto-detection was the hardest part. GL.iNet’s board.json model IDs are inconsistent across firmware versions, so the script tries three detection methods: board.json ID, /etc/openwrt_release target strings, and /etc/glversion firmware version matching. If all three fail, it falls back to Flint 2-safe defaults and warns you.
- Persistence through firmware upgrades required GL.iNet’s /etc/gl-switch.d/ hook directory, which runs scripts after every network config change. Combined with /etc/rc.local for boot, the QoS config survives everything except factory reset.
- The u32 filter problem on tc-tiny devices required a complete fallback path. Instead of u32 source-IP matching, the script relies entirely on nft ingress marks. This is actually cleaner — nft marks are set once on ingress and the HTB classes reference them.
- The setup wizard iterates on available interfaces at runtime. If br-iot doesn’t exist on your device, it’s simply skipped — no errors, no manual config editing needed.
Troubleshooting
Model detection fails:
cat /etc/board.json | grep model
export QOS_MODEL=flint3 # or flint2
glinet-vlan-qos.sh start
QoS not applying on Flint 2/Flint 4:
tc filter show dev br-lan # expect no u32 filters — this is normal
tc class show dev br-lan # should show HTB classes
Persistence not working:
ls -la /etc/gl-switch.d/vlan-qos.sh
/etc/gl-switch.d/vlan-qos.sh # test manually
Disclaimer
This is an unofficial script. I’ve tested it on the models and firmware versions listed above, but you’re responsible for any changes to your router. By using this script, you understand the risks and accept full responsibility.
Full disclaimer: Privacy Policy, Terms of Use, Disclaimer and Limitation of Liability
If you hit issues, open a ticket on GitHub: https://github.com/wickedyoda/Glinet-Bandwidth-script/issues
License
GPLv3 — see LICENSE for details.
Built over a weekend. Tested on real hardware. Priority guaranteed (maybe ;).