Skip to content

Instantly share code, notes, and snippets.

@Hi7cl4w
Last active July 4, 2025 23:09
Show Gist options
  • Save Hi7cl4w/68ccd26869c0929922b953fda6e25844 to your computer and use it in GitHub Desktop.
Save Hi7cl4w/68ccd26869c0929922b953fda6e25844 to your computer and use it in GitHub Desktop.
OpenWrt HFSC QoS Script
chain user_prerouting {
type filter hook prerouting priority -1; policy accept;
# 🎯 Mark incoming game download traffic (DSCP + fwmark)
ip protocol udp udp dport 10000-30000 counter mark set 10 ip dscp set ef
ip protocol udp udp dport 9031 counter mark set 10 ip dscp set ef
}
chain user_postrouting {
type filter hook postrouting priority -1; policy accept;
# 🎯 Mark upload traffic (outgoing game packets)
ip protocol udp udp sport 10000-30000 counter meta mark set 10 ip dscp set ef
ip protocol udp udp sport 9031 counter meta mark set 10 ip dscp set ef
}
#!/bin/sh
#
# HFSC QoS Script – Gaming‑optimised version
# -----------------------------------------
# • Dual‑direction HFSC shaping
# • Game traffic gets its own class (fwmark 10)
# • Tuned FQ‑CoDel for 2‑‑30 ms RTT paths
#
# ─── Interfaces ────────────────────────────────────────────────────────────────
WAN="pppoe-wan"
LAN="br-lan"
# ─── Bandwidth limits (kbit) ───────────────────────────────────────────────────
UPRATE=85000
DOWNRATE=85000
# ─── Game‑allocated bandwidth (dynamic %) ──────────────────────────────────────
GAME_PERCENT=20
GAMEUP=$((UPRATE * GAME_PERCENT / 100)) # 17 Mbit
GAMEDOWN=$((DOWNRATE * GAME_PERCENT / 100))
# ─── Queuing defaults ─────────────────────────────────────────────────────────
OH=40
MPU=64
PACKETSIZE=350
MAXDEL=25
PFIFOMIN=5
GAMEQDISC="fq_codel" # pfifo | fq_codel | red | netem
# ─── FQ‑CoDel tuning for gaming class ─────────────────────────────────────────
FQC_TARGET="3ms" # ≥ one‑MTU @ line‑rate; 3 ms works for 2‑30 ms paths
FQC_INTERVAL="40ms" # 10–15× target keeps CoDel responsive
FQC_QUANTUM=300 # small bursts favour small game packets
FQC_LIMIT=3000 # lower than default 10 k to prevent bloat
FQC_ECN="ecn" # mark or drop – helps modern TCP stacks
# ─── RED helper (only used if GAMEQDISC=red) ───────────────────────────────────
calc_red_params() {
local RATE=$1
echo "$((RATE * MAXDEL / 3 / 8)) $((RATE * MAXDEL / 8))"
}
# ─── HFSC QoS core ─────────────────────────────────────────────────────────────
apply_hfsc_qos() {
local DEV=$1 RATE=$2 GAMERATE=$3 DIR=$4
# Clean slate
tc qdisc del dev "$DEV" root 2>/dev/null
# Root HFSC
tc qdisc add dev "$DEV" root handle 1: hfsc default 13
# Parent class – caps total rate
tc class add dev "$DEV" parent 1: classid 1:1 \
hfsc ls rate ${RATE}kbit ul rate ${RATE}kbit
# Real‑time class for game traffic
tc class add dev "$DEV" parent 1:1 classid 1:11 \
hfsc rt m1 ${GAMERATE}kbit d 25ms m2 ${GAMERATE}kbit
# Default class for everything else
tc class add dev "$DEV" parent 1:1 classid 1:13 \
hfsc ls rate $((RATE - GAMERATE))kbit ul rate ${RATE}kbit
# ── Queue discipline for game traffic ──
case "$GAMEQDISC" in
"pfifo")
LIMIT=$((PFIFOMIN + MAXDEL * RATE / 8 / PACKETSIZE))
tc qdisc add dev "$DEV" parent 1:11 handle 10: pfifo limit "$LIMIT"
;;
"fq_codel")
tc qdisc add dev "$DEV" parent 1:11 handle 10: fq_codel \
target $FQC_TARGET interval $FQC_INTERVAL \
quantum $FQC_QUANTUM limit $FQC_LIMIT $FQC_ECN
;;
"red")
set -- $(calc_red_params "$GAMERATE")
tc qdisc add dev "$DEV" parent 1:11 handle 10: red \
limit 150000 min "$1" max "$2" avpkt 500 \
bandwidth "${GAMERATE}kbit" probability 1.0
;;
"netem")
tc qdisc add dev "$DEV" parent 1:11 handle 10: \
netem delay 1ms 7ms distribution normal
;;
*)
echo "[!] Unknown GAMEQDISC '$GAMEQDISC'; falling back to pfifo."
tc qdisc add dev "$DEV" parent 1:11 handle 10: pfifo limit 50
;;
esac
# Default class gets plain fq_codel
tc qdisc add dev "$DEV" parent 1:13 handle 20: fq_codel
# Mark‑based classification (fwmark 10 → game; 13 → default)
tc filter add dev "$DEV" protocol ip parent 1:0 prio 1 handle 10 fw flowid 1:11
tc filter add dev "$DEV" protocol ip parent 1:0 prio 2 handle 13 fw flowid 1:13
}
# ─── Apply QoS on both directions ──────────────────────────────────────────────
apply_hfsc_qos "$WAN" "$UPRATE" "$GAMEUP" "uplink"
apply_hfsc_qos "$LAN" "$DOWNRATE" "$GAMEDOWN" "downlink"
# ─── Report to console ────────────────────────────────────────────────────────
echo
echo "=============================="
echo "✅ HFSC QoS Configuration Complete"
echo "------------------------------"
echo "WAN Interface : $WAN"
echo " ▸ Upload Rate : ${UPRATE} kbit"
echo " ▸ Game Reserved : ${GAMEUP} kbit (${GAME_PERCENT}% of UPRATE)"
echo
echo "LAN Interface : $LAN"
echo " ▸ Download Rate : ${DOWNRATE} kbit"
echo " ▸ Game Reserved : ${GAMEDOWN} kbit (${GAME_PERCENT}% of DOWNRATE)"
echo
echo "Game Qdisc Applied : $GAMEQDISC"
case "$GAMEQDISC" in
"pfifo")
LIMIT=$((PFIFOMIN + MAXDEL * DOWNRATE / 8 / PACKETSIZE))
echo " ▸ FIFO Limit : $LIMIT packets"
;;
"fq_codel")
echo " ▸ FQ‑CoDel target : $FQC_TARGET"
echo " ▸ interval : $FQC_INTERVAL"
echo " ▸ quantum : $FQC_QUANTUM"
echo " ▸ limit : $FQC_LIMIT packets"
;;
"red")
set -- $(calc_red_params "$GAMEDOWN")
echo " ▸ RED Min/Max : $1 / $2 bytes"
;;
"netem")
echo " ▸ NETEM Delay : 1 ms ± 7 ms (normal)"
;;
esac
echo
echo "DSCP/Mark Rules : Enabled (fwmark 10 = Game, 13 = Default)"
echo "Queue Hierarchy : 1:11 = Game / Real‑Time, 1:13 = Default"
echo "=============================="
echo

OpenWrt HFSC QoS Script

This script configures HFSC-based traffic shaping on OpenWrt to optimize latency-sensitive traffic (e.g., gaming), with special handling for DSCP marking and class-based queuing.

📌 Features

  • Upload/download shaping using tc and HFSC
  • DSCP marking support (via optional nftables)
  • pfifo queue for game traffic
  • Class separation: real-time (gaming), default traffic
  • Works with PPPoE over bridged VLAN from ISP

⚙️ Configuration

Install Command

You can install the required packages via SSH with:

opkg update
opkg install tc kmod-sched kmod-sched-core kmod-sched-red kmod-ifb ip-full nftables

Edit /etc/hfsc-qos.sh to fit your network. Key parameters:

WAN="pppoe-wan"      # WAN interface name
LAN="br-lan"         # LAN bridge interface
UPRATE=95000         # Upload speed in kbit (95% of 100Mbps)
DOWNRATE=95000       # Download speed in kbit
GAMEUP=18500         # Reserved upload for gaming
GAMEDOWN=18500       # Reserved download for gaming
gameqdisc="pfifo"    # Queue type for gaming (pfifo, fq_codel, etc.)
OH=40                # Overhead in bytes (for PPPoE + VLAN)
MPU=64               # Minimum packet unit

🧪 Installation

  1. Place Script
cp hfsc-qos.sh /etc/hfsc-qos.sh
chmod +x /etc/hfsc-qos.sh
  1. Init Script

Create /etc/init.d/hfsc-qos with the following content:

#!/bin/sh /etc/rc.common

START=95
STOP=15

start() {
  logger -t hfsc-qos "Starting HFSC QoS..."
  /etc/hfsc-qos.sh
}

stop() {
  logger -t hfsc-qos "Stopping HFSC QoS..."
  tc qdisc del dev pppoe-wan root 2>/dev/null
  tc qdisc del dev br-lan root 2>/dev/null
}

Make it executable:

chmod +x /etc/init.d/hfsc-qos

🚀 Enable at Startup

/etc/init.d/hfsc-qos enable
/etc/init.d/hfsc-qos start

You can also use:

/etc/init.d/hfsc-qos restart
/etc/init.d/hfsc-qos stop

🔍 Checking Status

Run:

tc -s qdisc show dev pppoe-wan
tc -s qdisc show dev br-lan

Look for packet counters, drops, and class stats to verify shaping is working.


📌 Optional: DSCP Marking with nftables

Add DSCP tagging to gaming packets by including rules in your nftables config. This integrates with /usr/share/nftables.d/ruleset-post/dscptag.nft.

Ensure it is fetched or created and included in your firewall.


📬 Support

If you're using tc, nftables, and pppoe, this script gives you a solid starting point for low-latency, class-based QoS.

Test your latency under load with:

ping -i 0.2 8.8.8.8

📁 Files

  • /etc/hfsc-qos.sh — Main script
  • /etc/init.d/hfsc-qos — Startup/init control script

cat << 'EOF' > /etc/hfsc-qos.sh
#!/bin/sh

# Interface definitions
WAN="pppoe-wan"
LAN="br-lan"

# Bandwidth configuration in kbit
UPRATE=95000
DOWNRATE=95000
GAMEUP=18500
GAMEDOWN=18500

# Qdisc options
gameqdisc="pfifo"   # Options: pfifo, fq_codel, etc.
OH=40               # PPPoE + VLAN overhead
MPU=64              # Minimum packet unit

# Load IFB module
modprobe ifb
ip link add ifb0 type ifb 2>/dev/null
ip link set dev ifb0 up

# Reset Qdisc
tc qdisc del dev $WAN root 2>/dev/null
tc qdisc del dev ifb0 root 2>/dev/null

# Ingress shaping
tc qdisc add dev $WAN handle ffff: ingress
tc filter add dev $WAN parent ffff: protocol ip u32 match u32 0 0 action mirred egress redirect dev ifb0

# Egress (upload) HFSC setup
tc qdisc add dev $WAN root handle 1: hfsc default 20
tc class add dev $WAN parent 1: classid 1:1 hfsc sc rate ${UPRATE}kbit ul rate ${UPRATE}kbit

# Gaming class - upload
tc class add dev $WAN parent 1:1 classid 1:10 hfsc rt m1 ${GAMEUP}kbit d 25ms m2 ${GAMEUP}kbit
tc qdisc add dev $WAN parent 1:10 handle 10: $gameqdisc

# Default class - upload
tc class add dev $WAN parent 1:1 classid 1:20 hfsc sc rate $((UPRATE - GAMEUP))kbit ul rate $((UPRATE - GAMEUP))kbit
tc qdisc add dev $WAN parent 1:20 handle 20: fq_codel

# Ingress (download) HFSC setup
tc qdisc add dev ifb0 root handle 1: hfsc default 20
tc class add dev ifb0 parent 1: classid 1:1 hfsc sc rate ${DOWNRATE}kbit ul rate ${DOWNRATE}kbit

# Gaming class - download
tc class add dev ifb0 parent 1:1 classid 1:10 hfsc rt m1 ${GAMEDOWN}kbit d 25ms m2 ${GAMEDOWN}kbit
tc qdisc add dev ifb0 parent 1:10 handle 10: $gameqdisc

# Default class - download
tc class add dev ifb0 parent 1:1 classid 1:20 hfsc sc rate $((DOWNRATE - GAMEDOWN))kbit ul rate $((DOWNRATE - GAMEDOWN))kbit
tc qdisc add dev ifb0 parent 1:20 handle 20: fq_codel

# DSCP matching (optional, adjust as needed)
tc filter add dev $WAN protocol ip parent 1: prio 1 u32 match ip dsfield 0x2e 0xff flowid 1:10
tc filter add dev ifb0 protocol ip parent 1: prio 1 u32 match ip dsfield 0x2e 0xff flowid 1:10
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment