Files
op-packages/qosmate/etc/qosmate.sh
T

2023 lines
76 KiB
Bash

#!/bin/sh
# shellcheck disable=SC3043,SC1091,SC2155,SC3020,SC3010,SC2016,SC2317,SC3060,SC3057,SC3003
VERSION="1.2.0" # will become obsolete in future releases as version string is now in the init script
# uncomment to enable debug messages
# QOSMATE_DEBUG=1
_NL_='
'
DEFAULT_IFS=" ${_NL_}"
IFS="$DEFAULT_IFS"
: "${VERSION}" "${global_enabled:=}" "${nongameqdisc:=}" "${nongameqdiscoptions:=}" "${OVERHEAD:=}"
. /lib/functions.sh
# Config is loaded by the caller (qosmate init), this is a fallback just in case
[ -n "$QOSMATE_CONFIG_LOADED" ] || {
. /etc/init.d/qosmate
load_and_fix_config || exit 1
}
error_out() { log_msg -err "${@}"; }
# prints each argument to a separate line
print_msg() {
# Write to the inherited descriptors instead of redirecting to /dev/stdout: that re-opens the
# target, and when stdout is a regular file '>' truncates it.
local _arg to_stderr='' msgs_prefix=''
for _arg in "$@"
do
case "${_arg}" in
-err) to_stderr=1 msgs_prefix="Error: " ;;
-warn) to_stderr=1 msgs_prefix="Warning: " ;;
'') printf '\n' ;; # print out empty lines
*)
if [ -n "$to_stderr" ]; then
printf '%s\n' "${msgs_prefix}${_arg}" >&2
else
printf '%s\n' "${msgs_prefix}${_arg}"
fi
msgs_prefix=''
esac
done
:
}
# logs each argument separately and prints to a separate line
# optional arguments: '-err', '-warn' to set logged error level
log_msg() {
local msgs_prefix='' _arg err_l=info
local IFS="$DEFAULT_IFS"
for _arg in "$@"
do
case "${_arg}" in
"-err") err_l=err msgs_prefix="Error: " ;;
"-warn") err_l=warn msgs_prefix="Warning: " ;;
'') printf '\n' ;; # print out empty lines
*)
case "$err_l" in
err|warn) printf '%s\n' "${msgs_prefix}${_arg}" >&2 ;;
*) printf '%s\n' "${msgs_prefix}${_arg}"
esac
logger -t qosmate -p user."$err_l" "${msgs_prefix}${_arg}"
msgs_prefix=''
esac
done
:
}
config_load 'qosmate' || { error_out "Failed to get UCI config."; exit 1; }
# Check if Software Flow Offloading is enabled
SFO_ENABLED=0
[ "$(uci -q get firewall.@defaults[0].flow_offloading)" = "1" ] && SFO_ENABLED=1
# Calculated values
FIRST500MS=$((DOWNRATE * 500 / 8))
FIRST10S=$((DOWNRATE * 10000 / 8))
# Get tc stab parameters for HFSC/HTB/Hybrid
get_tc_overhead_params() {
local preset="$COMMON_LINK_PRESETS"
local overhead="$OVERHEAD"
# Detect ATM-based presets
case "$preset" in
*atm*|*adsl*|*pppoa*|*pppoe*|*bridged*|*ipoa*|conservative)
printf '%s' "stab mtu 2047 tsize 512 mpu 68 overhead ${overhead:-44} linklayer atm"
;;
docsis)
printf '%s' "stab overhead ${overhead:-25} linklayer ethernet"
;;
cake-ethernet)
printf '%s' "stab overhead ${overhead:-38} linklayer ethernet"
;;
raw)
printf '%s' "stab overhead ${overhead:-0} linklayer ethernet"
;;
*)
printf '%s' "stab overhead ${overhead:-40} linklayer ethernet"
;;
esac
}
# Get CAKE parameters from common link settings
# $1 = "hybrid": CAKE runs below an HFSC root that already accounts for the overhead
get_cake_link_params() {
local preset="$COMMON_LINK_PRESETS"
local oh="${OVERHEAD}"
local base=""
# The HFSC root carries a tc stab, which rewrites qdisc_pkt_len for the whole
# hierarchy. "raw" makes CAKE bill that already adjusted length instead of
# adding the overhead a second time.
[ "$1" = "hybrid" ] && { printf 'raw'; return; }
# Determine base keyword and default overhead
case "$preset" in
*atm*|*adsl*|*pppoa*|*pppoe*|*bridged*|*ipoa*|conservative)
base="${preset}"
: "${oh:=44}"
;;
docsis) base="docsis"; : "${oh:=25}" ;;
cake-ethernet) base="ethernet"; oh="" ;;
raw) base="raw"; : "${oh:=0}" ;;
ethernet|*) base="ethernet"; : "${oh:=40}" ;;
esac
# Build parameters
printf "%s%s%s%s" \
"$base" \
"${oh:+ overhead $oh}" \
"${MPU:+ mpu $MPU}" \
"${ETHER_VLAN_KEYWORD:+ $ETHER_VLAN_KEYWORD}"
}
# Select cake or cake_mq based on USE_MQ setting and system capabilities
# $1: interface name to check for multi-queue support
# Sets REPLY to "cake" or "cake_mq"
select_cake_qdisc() {
local iface="$1" num_queues=0
REPLY="cake"
[ "$USE_MQ" != "1" ] && return
num_queues=$(find /sys/class/net/"$iface"/queues/ -maxdepth 1 -type d -name 'tx-*' 2>/dev/null | wc -l)
if [ "$num_queues" -gt 1 ] && tc qdisc replace dev "$iface" root cake_mq 2>/dev/null; then
tc qdisc del dev "$iface" root 2>/dev/null
log_msg "Using cake_mq for $iface ($num_queues TX queues)"
REPLY="cake_mq"
else
if [ "$num_queues" -le 1 ]; then
log_msg "cake_mq not used for $iface: only $num_queues TX queue(s), using cake"
else
log_msg "cake_mq not available in kernel, using cake for $iface"
fi
fi
}
##############################
# Variable checks and dynamic rule generation
##############################
# Function to calculate different ACK rates based on the existing ACKRATE variable
calculate_ack_rates() {
if [ -n "$ACKRATE" ] && [ "$ACKRATE" -gt 0 ]; then
SLOWACKRATE=$ACKRATE
MEDACKRATE=$ACKRATE
FASTACKRATE=$((ACKRATE * 10))
XFSTACKRATE=$((ACKRATE * 100))
fi
}
# Call the function to perform the ACK rates calculations
calculate_ack_rates
# Debug function
debug_log() {
[ -n "$QOSMATE_DEBUG" ] || return 0
logger -s -t qosmate "$1" >&2
}
# Function to create NFT sets from config
create_nft_sets() {
local sets_created=""
# shellcheck disable=SC2329
create_set() {
local section="$1" name ip_list mode timeout set_flags family nft_type elements=""
config_get name "$section" name
# Only process if enabled (default: enabled)
local enabled=1
config_get_bool enabled "$section" enabled 1
[ "$enabled" -eq 0 ] && return 0
config_get mode "$section" mode "static"
config_get timeout "$section" timeout "1h"
config_get family "$section" family "ipv4"
# Get the IP list based on family
if [ "$family" = "ipv6" ]; then
config_get ip_list "$section" ip6
echo "$name ipv6" >> /tmp/qosmate_set_families
else
config_get ip_list "$section" ip4
echo "$name ipv4" >> /tmp/qosmate_set_families
fi
# Use the family parameter from the UCI configuration ("ipv4" or "ipv6")
if [ "$mode" = "dynamic" ]; then
set_flags="dynamic, timeout"
if [ "$family" = "ipv6" ]; then
debug_log "Creating dynamic IPv6 set: $name"
echo "set $name { type ipv6_addr; flags $set_flags; timeout $timeout; }"
else
debug_log "Creating dynamic IPv4 set: $name"
echo "set $name { type ipv4_addr; flags $set_flags; timeout $timeout; }"
fi
else
nft_type="ipv4_addr"
[ "$family" = "ipv6" ] && nft_type="ipv6_addr"
# auto-merge collapses overlapping entries (e.g. a range covering an
# already listed single IP), which nftables would otherwise reject
[ -n "$ip_list" ] && elements=" elements = { $(echo "$ip_list" | tr ' ' ',') };"
debug_log "Creating static $family set: $name"
echo "set $name { type $nft_type; flags interval; auto-merge;$elements }"
fi
sets_created="$sets_created $name"
}
# Clear the temporary file
rm -f /tmp/qosmate_set_families
config_foreach create_set ipset
export QOSMATE_SETS="$sets_created"
[ -n "$sets_created" ] && debug_log "Created sets: $sets_created"
}
# Create NFT sets
SETS=$(create_nft_sets)
# Create rules
# shellcheck disable=SC2329
create_nft_rule() {
# Trim leading and trailing whitespaces and tabs in variable $1
trim_spaces() {
local tr_in tr_out
eval "tr_in=\"\${$1}\""
tr_out="${tr_in%"${tr_in##*[! ]}"}"
tr_out="${tr_out#"${tr_out%%[! ]*}"}"
eval "$1=\"\${tr_out}\""
}
is_set_ref() {
case "$1" in "@"*) return 0; esac
return 1
}
# checks whether string is an ipv6 mask
is_ipv6_mask() {
case "$1" in
::*/::*) ;;
*) return 1
esac
local inp="${1#"::"}"
case "${inp%"/::"*}" in *"/"*) return 1; esac
return 0
}
# Function to check if a single IP address is IPv6
# Note: This assumes the input is a single IP, not a space-separated list
# Handles CIDR notation (e.g. ::/0 or 192.168.1.0/24)
is_ipv6() {
local ip="${1%/*}" # Remove CIDR suffix if present
case "$ip" in
*:*) return 0 ;;
*) return 1 ;;
esac
}
local config="$1"
local proto class counter name enabled trace
config_get proto "$config" proto
config_get class "$config" class
config_get_bool counter "$config" counter 0
config_get_bool trace "$config" trace 0
config_get name "$config" name
config_get_bool enabled "$config" enabled 1 # Default to enabled if not set
# Check if the rule is enabled
[ "$enabled" = "0" ] && return 0
# Convert class to lowercase
class=$(echo "$class" | tr 'A-Z' 'a-z')
# Ensure class is not empty
if [ -z "$class" ]; then
print_msg -err "Class for rule '$config' is empty."
return 1
fi
# Function to get set family
get_set_family() {
local setname="$1"
[ -f /tmp/qosmate_set_families ] && awk -v set="$setname" '$1 == set {print $2}' /tmp/qosmate_set_families
}
# Function to separate IPs by family
separate_ips_by_family() {
local ips="$3" \
ip prefix setname \
ipv4_result="" \
ipv6_result=""
# Debug log (uncomment for troubleshooting)
# debug_log "separate_ips_by_family: Processing IPs: '$ips'"
for ip in $ips; do
# Preserve != prefix
prefix=""
case "$ip" in '!='*)
prefix="!="
ip="${ip#"!="}"
esac
# debug_log " Checking IP: '$ip'
# Check if it's a set reference
if is_set_ref "$ip"; then
setname="${ip#"@"}"
if [ "$(get_set_family "$setname")" = "ipv6" ]; then
ipv6_result="${ipv6_result}${ipv6_result:+ }${prefix}${ip}"
# debug_log " -> IPv6 set: $setname"
else
ipv4_result="${ipv4_result}${ipv4_result:+ }${prefix}${ip}"
# debug_log " -> IPv4 set: $setname"
fi
# Check for IPv6 suffix format
elif is_ipv6_mask "$ip"; then
ipv6_result="${ipv6_result}${ipv6_result:+ }${prefix}${ip}"
# debug_log " -> IPv6 suffix format"
# Regular IP check
elif is_ipv6 "$ip"; then
ipv6_result="${ipv6_result}${ipv6_result:+ }${prefix}${ip}"
# debug_log " -> IPv6 address"
else
ipv4_result="${ipv4_result}${ipv4_result:+ }${prefix}${ip}"
# debug_log " -> IPv4 address"
fi
done
# debug_log " Results: IPv4='$ipv4_result', IPv6='$ipv6_result'"
eval "${1}=\"\${ipv4_result}\" ${2}=\"\${ipv6_result}\""
}
# Check and separate source and destination IPs
local src_ip dest_ip \
src_ip_v4='' src_ip_v6='' dest_ip_v4='' dest_ip_v6='' \
has_ipv4=0 has_ipv6=0 \
ip_val ip_type
for ip_type in src_ip dest_ip; do
config_get "${ip_type}" "$config" "${ip_type}"
eval "ip_val=\"\${$ip_type}\""
if [ -n "$ip_val" ]; then
separate_ips_by_family "${ip_type}_v4" "${ip_type}_v6" "$ip_val"
eval "
[ -n \"\${${ip_type}_v4}\" ] && has_ipv4=1
[ -n \"\${${ip_type}_v6}\" ] && has_ipv6=1
"
fi
done
# Log if mixed IPv4/IPv6 addresses are found
if [ "$has_ipv4" -eq 1 ] && [ "$has_ipv6" -eq 1 ]; then
log_msg "" "Info: Mixed IPv4/IPv6 addresses in rule '$name' ($config). Splitting into separate rules." >&2
fi
# If no IP address was specified, we assume the rule applies to both IPv4 and IPv6
if [ -z "$src_ip" ] && [ -z "$dest_ip" ] && [ "$has_ipv4" -eq 0 ] && [ "$has_ipv6" -eq 0 ]; then
debug_log "Rule '$name' ($config): No IP specified, generating rules for both IPv4 and IPv6."
has_ipv4=1
has_ipv6=1
fi
# Function to handle multiple values with IP family awareness
gen_rule() {
add_res_rule() {
if [ -z "$res_set_neg" ] && [ -z "$res_set_pos" ]; then
error_out "no valid $1 found in '$values'. Rule skipped."
return 1
fi
if [ -n "$res_set_neg" ]; then
result="${result}${result:+ }${prefix} != { ${res_set_neg} }"
fi
if [ -n "$res_set_pos" ]; then
result="${result}${result:+ }${prefix} { ${res_set_pos} }"
fi
:
}
local value setname family suffix mask comp_op negation \
result='' res_set_neg='' res_set_pos='' has_ipv4='' has_ipv6='' set_ref_seen='' ipv6_mask_seen='' reg_val_seen='' \
values="$1" \
prefix="$2"
for value in $values; do
if [ -n "$set_ref_seen" ] || [ -n "$ipv6_mask_seen" ]; then
error_out "invalid entry '$values'. When using nftables set reference or ipv6 mask, other values are not allowed."
return 1
fi
# Check if value starts with '!=' and preserve the '!=' prefix
negation=
comp_op="=="
case "$value" in '!='*)
negation=" !="
comp_op="!="
value="${value#"!="}"
esac
# Handle set references (@setname)
if is_set_ref "$value"; then
if [ -n "$reg_val_seen" ]; then
error_out "invalid entry '$values'. When using nftables set reference or ipv6 mask, other values are not allowed."
return 1
fi
set_ref_seen=1
setname="${value#@}"
family="$(get_set_family "$setname")"
debug_log "Set $setname has family: $family"
if [ "$family" = "ipv6" ]; then
prefix="${prefix//ip /ip6 }"
fi
result="${prefix}${negation} @${setname}"
continue
fi
# Check for IPv6 suffix format (::suffix/::mask)
if is_ipv6_mask "$value"; then
if [ -n "$reg_val_seen" ]; then
error_out "invalid entry '$values'. When using nftables set reference or ipv6 mask, other values are not allowed."
return 1
fi
ipv6_mask_seen=1
# Extract suffix and mask
suffix="${value%%"/::"*}"
mask="${value#"${suffix}/"}"
# Force IPv6 prefix and create bitwise AND|NOT match
result="${prefix//ip /ip6 } & ${mask} ${comp_op} ${suffix}"
continue
fi
# Validate prefix type
case "$prefix" in
"ip saddr"|"ip daddr"|"ip6 saddr"|"ip6 daddr"|"th sport"|"th dport"|"meta l4proto")
;;
*)
error_out "unexpected prefix '$prefix'."
return 1
;;
esac
case "$prefix" in *addr*)
if is_ipv6 "$value"; then
has_ipv6=1
else
has_ipv4=1
fi
esac
# Collect values
if [ -n "$negation" ]; then
res_set_neg="${res_set_neg}${res_set_neg:+,}${value}"
else
res_set_pos="${res_set_pos}${res_set_pos:+,}${value}"
fi
reg_val_seen=1
done
if [ -n "$set_ref_seen" ] || [ -n "$ipv6_mask_seen" ]; then
printf '%s\n' "$result"
return 0
fi
# If mixed, log and signal error
if [ -n "$has_ipv4" ] && [ -n "$has_ipv6" ]; then
error_out "Mixed IPv4/IPv6 addresses within a set: { $values }. Rule skipped."
return 1
fi
# Update prefix based on IP type
if [ -n "$has_ipv6" ]; then
prefix="${prefix//ip /ip6 }"
fi
# Construct the final rule
case "$prefix" in
*addr*)
# IP address rules
add_res_rule addresses || return 1
;;
"th sport"|"th dport")
# Port rules
add_res_rule ports || return 1
;;
"meta l4proto")
# Protocol rules
add_res_rule protocols || return 1
;;
esac
printf '%s\n' "$result"
}
# Initialize rule string
local rule_cmd=""
# Handle multiple protocols
if [ -n "$proto" ]; then
local proto_result
if ! proto_result="$(gen_rule "$proto" "meta l4proto")"; then
# Skip rule
return 0
fi
rule_cmd="$rule_cmd $proto_result"
fi
# Note: Source and Destination IP handling is now done per-family in the rule generation below
# Use connection tracking for source and destination ports
local port port_type port_res port_seen=''
for port_type in src_port dest_port; do
config_get port "$config" "$port_type"
if [ -n "$port" ]; then
if ! port_res="$(gen_rule "$port" "th ${port_type%%"${port_type#?}"}port")"; then
# Skip rule
return 0
fi
rule_cmd="$rule_cmd $port_res"
port_seen=1
fi
done
# Build final rule(s) based on has_ipv4 and has_ipv6 flags
local final_rule_v4=""
local final_rule_v6=""
local common_rule_part="$rule_cmd"
trim_spaces common_rule_part # Trim common parts
# Generate IPv4 rule if needed
if [ "$has_ipv4" -eq 1 ]; then
local rule_cmd_v4="$common_rule_part"
# Add IPv4-specific IP addresses
if [ -n "$src_ip_v4" ]; then
local src_result
if ! src_result="$(gen_rule "$src_ip_v4" "ip saddr")"; then
# Skip rule
return 0
fi
rule_cmd_v4="$rule_cmd_v4 $src_result"
fi
if [ -n "$dest_ip_v4" ]; then
local dest_result
if ! dest_result="$(gen_rule "$dest_ip_v4" "ip daddr")"; then
# Skip rule
return 0
fi
rule_cmd_v4="$rule_cmd_v4 $dest_result"
fi
# Ensure we only add parts if there's something to match on (IP/Port/Proto)
if [ -n "$proto" ] || [ -n "$src_ip_v4" ] || [ -n "$dest_ip_v4" ] || [ -n "$port_seen" ]; then
rule_cmd_v4="$rule_cmd_v4 ip dscp set $class"
fi
[ "$counter" -eq 1 ] && rule_cmd_v4="$rule_cmd_v4 counter"
[ "$trace" -eq 1 ] && rule_cmd_v4="$rule_cmd_v4 meta nftrace set 1"
[ -n "$name" ] && rule_cmd_v4="$rule_cmd_v4 comment \"ipv4_$name\""
trim_spaces rule_cmd_v4 # Trim final rule
# Ensure the rule is not just a semicolon
if [ -n "$rule_cmd_v4" ] && [ "$rule_cmd_v4" != ";" ]; then
final_rule_v4="$rule_cmd_v4;"
fi
fi
# Generate IPv6 rule if needed
if [ "$has_ipv6" -eq 1 ]; then
local rule_cmd_v6="$common_rule_part"
# Add IPv6-specific IP addresses
if [ -n "$src_ip_v6" ]; then
local src_result
if ! src_result="$(gen_rule "$src_ip_v6" "ip6 saddr")"; then
# Skip rule
return 0
fi
rule_cmd_v6="$rule_cmd_v6 $src_result"
fi
if [ -n "$dest_ip_v6" ]; then
local dest_result
if ! dest_result="$(gen_rule "$dest_ip_v6" "ip6 daddr")"; then
# Skip rule
return 0
fi
rule_cmd_v6="$rule_cmd_v6 $dest_result"
fi
# Ensure we only add parts if there's something to match on (IP/Port/Proto)
if [ -n "$proto" ] || [ -n "$src_ip_v6" ] || [ -n "$dest_ip_v6" ] || [ -n "$port_seen" ]; then
rule_cmd_v6="$rule_cmd_v6 ip6 dscp set $class"
fi
[ "$counter" -eq 1 ] && rule_cmd_v6="$rule_cmd_v6 counter"
[ "$trace" -eq 1 ] && rule_cmd_v6="$rule_cmd_v6 meta nftrace set 1"
[ -n "$name" ] && rule_cmd_v6="$rule_cmd_v6 comment \"ipv6_$name\""
trim_spaces rule_cmd_v6 # Trim final rule
# Ensure the rule is not just a semicolon
if [ -n "$rule_cmd_v6" ] && [ "$rule_cmd_v6" != ";" ]; then
final_rule_v6="$rule_cmd_v6;"
fi
fi
# Output the generated rules (if any)
[ -n "$final_rule_v4" ] && echo "$final_rule_v4"
[ -n "$final_rule_v6" ] && echo "$final_rule_v6"
}
generate_dynamic_nft_rules() {
# Check global enable setting
if [ "$global_enabled" = "1" ]; then
config_foreach create_nft_rule rule
else
echo "# QoSmate rules are globally disabled"
fi
}
##############################
# Rate Limit Functions
##############################
# Build nftables device match conditions from target values with direction support
# Detects IP/IPv6 addresses and generates appropriate match statements
# Args: $1=target_values, $2=direction (saddr/daddr), $3=result_var_name
# shellcheck disable=SC2329
build_device_conditions_for_direction() {
local target_values="$1" direction="$2" result_var="$3"
local result="" ipv4_pos="" ipv4_neg="" ipv6_pos="" ipv6_neg=""
local value negation v
for value in $target_values; do
negation=""
v="$value"
# Check for negation prefix
case "$v" in
'!='*)
negation="!="
v="${v#!=}"
;;
esac
# Check for set reference (@setname)
case "$v" in
'@'*)
# Set reference - determine family and use correct prefix
local setname="${v#@}"
local set_family
set_family="$(awk -v set="$setname" '$1 == set {print $2}' /tmp/qosmate_set_families 2>/dev/null)"
local ip_prefix='ip'
[ "$set_family" = "ipv6" ] && ip_prefix='ip6'
if [ -n "$negation" ]; then
result="${result}${result:+ }${ip_prefix} ${direction} != @${setname}"
else
result="${result}${result:+ }${ip_prefix} ${direction} @${setname}"
fi
;;
*)
# Detect address type and collect for set notation
# Skip MAC addresses (not supported)
if printf '%s' "$v" | grep -qE '^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$'; then
log_msg -warn "MAC address '$v' in rate limit rule ignored (not supported)"
elif printf '%s' "$v" | grep -q ':' && ! printf '%s' "$v" | grep -qE '^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$'; then
# IPv6 address (contains colon, not a MAC address)
if [ -n "$negation" ]; then
ipv6_neg="${ipv6_neg}${ipv6_neg:+,}${v}"
else
ipv6_pos="${ipv6_pos}${ipv6_pos:+,}${v}"
fi
else
# IPv4 address or CIDR
if [ -n "$negation" ]; then
ipv4_neg="${ipv4_neg}${ipv4_neg:+,}${v}"
else
ipv4_pos="${ipv4_pos}${ipv4_pos:+,}${v}"
fi
fi
;;
esac
done
# Build set-based conditions
if [ -n "$ipv4_neg" ]; then
result="${result}${result:+ }ip ${direction} != { ${ipv4_neg} }"
fi
if [ -n "$ipv4_pos" ]; then
result="${result}${result:+ }ip ${direction} { ${ipv4_pos} }"
fi
if [ -n "$ipv6_neg" ]; then
result="${result}${result:+ }ip6 ${direction} != { ${ipv6_neg} }"
fi
if [ -n "$ipv6_pos" ]; then
result="${result}${result:+ }ip6 ${direction} { ${ipv6_pos} }"
fi
eval "${result_var}=\"\${result}\""
}
# Generate rate limit rules from UCI config
generate_ratelimit_rules() {
local rules=""
# Process each ratelimit section
# shellcheck disable=SC2329
process_ratelimit_section() {
local section="$1"
local name enabled download_limit upload_limit burst_factor
local target_values meter_suffix download_kbytes upload_kbytes
local download_burst upload_burst
config_get_bool enabled "$section" enabled 1
[ "$enabled" -eq 0 ] && return 0
config_get name "$section" name
[ -z "$name" ] && {
log_msg -warn "Rate limit section '$section' has no name - skipping"
return 0
}
config_get download_limit "$section" download_limit "0"
config_get upload_limit "$section" upload_limit "0"
config_get burst_factor "$section" burst_factor "1.0"
config_get target_values "$section" target
# Validate: need at least one target and one limit
[ -z "$target_values" ] && {
log_msg -warn "Rate limit rule '$name' has no target devices - skipping"
return 0
}
[ "$download_limit" -eq 0 ] && [ "$upload_limit" -eq 0 ] && {
log_msg -warn "Rate limit rule '$name' has no bandwidth limits - skipping"
return 0
}
# Sanitize name for meter usage (only alphanumeric and underscore)
meter_suffix="$(printf '%s' "$name" | tr ' ' '_' | tr -cd 'a-zA-Z0-9_')"
[ -z "$meter_suffix" ] && meter_suffix="unnamed_${section}"
# Convert Kbit/s to kbytes/second (1 Kbit/s = 0.125 kbytes/s)
download_kbytes=$((download_limit / 8))
upload_kbytes=$((upload_limit / 8))
# Calculate burst using robust decimal parsing
# If burst_factor is 0, we don't add burst parameter at all (strict rate limit)
local download_burst_param='' upload_burst_param=''
# Parse burst_factor robustly (handle cases like "1.", ".5", "0.25", etc.)
case "$burst_factor" in
0|0.0|0.00)
# No burst - strict limiting
;;
*.*)
# Has decimal point
local burst_int="${burst_factor%.*}"
local burst_dec="${burst_factor#*.}"
# Handle missing parts
[ -z "$burst_int" ] && burst_int='0'
[ -z "$burst_dec" ] && burst_dec='0'
# Pad or truncate decimal to 2 digits for centiprecision
case "${#burst_dec}" in
1) burst_dec="${burst_dec}0" ;; # 0.5 -> 50
2) ;; # 0.25 -> 25
*) burst_dec="${burst_dec:0:2}" ;; # 0.125 -> 12
esac
# Calculate: burst = rate * (int + dec/100)
local download_burst=$((download_kbytes * burst_int + download_kbytes * burst_dec / 100))
local upload_burst=$((upload_kbytes * burst_int + upload_kbytes * burst_dec / 100))
[ "$download_burst" -gt 0 ] && download_burst_param=" burst ${download_burst} kbytes"
[ "$upload_burst" -gt 0 ] && upload_burst_param=" burst ${upload_burst} kbytes"
;;
*)
# Integer only (e.g. "1", "2")
local download_burst=$((download_kbytes * burst_factor))
local upload_burst=$((upload_kbytes * burst_factor))
download_burst_param=" burst ${download_burst} kbytes"
upload_burst_param=" burst ${upload_burst} kbytes"
;;
esac
# Separate targets by IP family
local targets_v4='' targets_v6='' value prefix setname set_family
for value in $target_values; do
# Preserve != prefix
prefix=''
case "$value" in
'!='*)
prefix='!='
value="${value#!=}"
;;
esac
# Check if it's a set reference
case "$value" in
'@'*)
setname="${value#@}"
set_family="$(awk -v set="$setname" '$1 == set {print $2}' /tmp/qosmate_set_families 2>/dev/null)"
if [ "$set_family" = "ipv6" ]; then
targets_v6="${targets_v6}${targets_v6:+ }${prefix}${value}"
else
targets_v4="${targets_v4}${targets_v4:+ }${prefix}${value}"
fi
;;
*)
# Check if IPv6 (contains colon and not MAC)
if printf '%s' "$value" | grep -q ':' && ! printf '%s' "$value" | grep -qE '^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$'; then
targets_v6="${targets_v6}${targets_v6:+ }${prefix}${value}"
else
targets_v4="${targets_v4}${targets_v4:+ }${prefix}${value}"
fi
;;
esac
done
# Generate IPv4 rules
if [ -n "$targets_v4" ]; then
if [ "$download_limit" -gt 0 ]; then
local download_conditions_v4=''
build_device_conditions_for_direction "$targets_v4" "daddr" download_conditions_v4
[ -n "$download_conditions_v4" ] && rules="${rules}
# ${name} - Download limit (IPv4)
${download_conditions_v4} meter ${meter_suffix}_dl4 { ip daddr limit rate over ${download_kbytes} kbytes/second${download_burst_param} } counter drop comment \"${name} download\""
fi
if [ "$upload_limit" -gt 0 ]; then
local upload_conditions_v4=''
build_device_conditions_for_direction "$targets_v4" "saddr" upload_conditions_v4
[ -n "$upload_conditions_v4" ] && rules="${rules}
# ${name} - Upload limit (IPv4)
${upload_conditions_v4} meter ${meter_suffix}_ul4 { ip saddr limit rate over ${upload_kbytes} kbytes/second${upload_burst_param} } counter drop comment \"${name} upload\""
fi
fi
# Generate IPv6 rules
if [ -n "$targets_v6" ]; then
if [ "$download_limit" -gt 0 ]; then
local download_conditions_v6=''
build_device_conditions_for_direction "$targets_v6" "daddr" download_conditions_v6
[ -n "$download_conditions_v6" ] && rules="${rules}
# ${name} - Download limit (IPv6)
${download_conditions_v6} meter ${meter_suffix}_dl6 { ip6 daddr limit rate over ${download_kbytes} kbytes/second${download_burst_param} } counter drop comment \"${name} download\""
fi
if [ "$upload_limit" -gt 0 ]; then
local upload_conditions_v6=''
build_device_conditions_for_direction "$targets_v6" "saddr" upload_conditions_v6
[ -n "$upload_conditions_v6" ] && rules="${rules}
# ${name} - Upload limit (IPv6)
${upload_conditions_v6} meter ${meter_suffix}_ul6 { ip6 saddr limit rate over ${upload_kbytes} kbytes/second${upload_burst_param} } counter drop comment \"${name} upload\""
fi
fi
}
# Process all ratelimit sections from UCI
config_foreach process_ratelimit_section ratelimit
# Output rate limit chain if rules exist
if [ -n "$rules" ]; then
printf '\n%s\n%s\n%s%s\n%s\n' \
" # Rate Limit Chain" \
" chain ratelimit {" \
" type filter hook forward priority 0; policy accept;" \
"${rules}" \
" }"
fi
}
# Generate dynamic rules
DYNAMIC_RULES=$(generate_dynamic_nft_rules)
# Check if ACKRATE is greater than 0
if [ "$ACKRATE" -gt 0 ]; then
ack_rules="\
meta length < 100 tcp flags ack add @xfst4ack {ct id . ct direction limit rate over ${XFSTACKRATE}/second} counter jump drop995
meta length < 100 tcp flags ack add @fast4ack {ct id . ct direction limit rate over ${FASTACKRATE}/second} counter jump drop95
meta length < 100 tcp flags ack add @med4ack {ct id . ct direction limit rate over ${MEDACKRATE}/second} counter jump drop50
meta length < 100 tcp flags ack add @slow4ack {ct id . ct direction limit rate over ${SLOWACKRATE}/second} counter jump drop50"
else
ack_rules="# ACK rate regulation disabled as ACKRATE=0 or not set."
fi
# Check if UDPBULKPORT is set
if [ -n "$UDPBULKPORT" ]; then
udpbulkport_rules="\
meta l4proto udp ct original proto-src \$udpbulkport counter jump mark_cs1
meta l4proto udp ct original proto-dst \$udpbulkport counter jump mark_cs1"
else
udpbulkport_rules="# UDP Bulk Port rules disabled, no ports defined."
fi
# Check if TCPBULKPORT is set
if [ -n "$TCPBULKPORT" ]; then
tcpbulkport_rules="\
meta l4proto tcp ct original proto-dst \$tcpbulkport counter jump mark_cs1"
else
tcpbulkport_rules="# TCP Bulk Port rules disabled, no ports defined."
fi
# Check if VIDCONFPORTS is set
if [ -n "$VIDCONFPORTS" ]; then
vidconfports_rules="\
meta l4proto udp ct original proto-dst \$vidconfports counter jump mark_af42"
else
vidconfports_rules="# VIDCONFPORTS Port rules disabled, no ports defined."
fi
# Check if REALTIME4 and REALTIME6 are set
if [ -n "$REALTIME4" ]; then
realtime4_rules="\
meta l4proto udp ip daddr \$realtime4 ip dscp set cs5 counter
meta l4proto udp ip saddr \$realtime4 ip dscp set cs5 counter"
else
realtime4_rules="# REALTIME4 rules disabled, address not defined."
fi
if [ -n "$REALTIME6" ]; then
realtime6_rules="\
meta l4proto udp ip6 daddr \$realtime6 ip6 dscp set cs5 counter
meta l4proto udp ip6 saddr \$realtime6 ip6 dscp set cs5 counter"
else
realtime6_rules="# REALTIME6 rules disabled, address not defined."
fi
# Check if LOWPRIOLAN4 and LOWPRIOLAN6 are set
if [ -n "$LOWPRIOLAN4" ]; then
lowpriolan4_rules="\
meta l4proto udp ip daddr \$lowpriolan4 ip dscp set cs0 counter
meta l4proto udp ip saddr \$lowpriolan4 ip dscp set cs0 counter"
else
lowpriolan4_rules="# LOWPRIOLAN4 rules disabled, address not defined."
fi
if [ -n "$LOWPRIOLAN6" ]; then
lowpriolan6_rules="\
meta l4proto udp ip6 daddr \$lowpriolan6 ip6 dscp set cs0 counter
meta l4proto udp ip6 saddr \$lowpriolan6 ip6 dscp set cs0 counter"
else
lowpriolan6_rules="# LOWPRIOLAN6 rules disabled, address not defined."
fi
# Check if UDP rate limiting should be applied
if [ "$UDP_RATE_LIMIT_ENABLED" -eq 1 ]; then
udp_rate_limit_rules="\
meta l4proto udp ip dscp > cs2 add @udp_meter {ct id . ct direction limit rate over 450/second} counter ip dscp set cs0 counter
meta l4proto udp ip6 dscp > cs2 add @udp_meter {ct id . ct direction limit rate over 450/second} counter ip6 dscp set cs0 counter"
else
udp_rate_limit_rules="# UDP rate limiting is disabled."
fi
# Check if TCP upgrade for slow connections should be applied
if [ "$TCP_UPGRADE_ENABLED" -eq 1 ]; then
tcp_upgrade_rules="
meta l4proto tcp ip dscp != cs1 add @slowtcp {ct id . ct direction limit rate 150/second burst 150 packets } ip dscp set af42 counter
meta l4proto tcp ip6 dscp != cs1 add @slowtcp {ct id . ct direction limit rate 150/second burst 150 packets} ip6 dscp set af42 counter"
else
tcp_upgrade_rules="# TCP upgrade for slow connections is disabled"
fi
# Conditionally defining TCP down-prioritization rules based on enabled flags
# Both thresholds are derived from DOWNRATE. With DOWNRATE=0 first10s would be 0 and the
# rule would down-prioritize every TCP connection, so the rules are skipped in that case.
if [ "$TCP_DOWNPRIO_INITIAL_ENABLED" -eq 1 ] && [ "$DOWNRATE" -gt 0 ]; then
downprio_initial_rules="meta l4proto tcp ct bytes < \$first500ms jump mark_500ms"
else
downprio_initial_rules="# Initial TCP down-prioritization disabled"
fi
if [ "$TCP_DOWNPRIO_SUSTAINED_ENABLED" -eq 1 ] && [ "$DOWNRATE" -gt 0 ]; then
downprio_sustained_rules="meta l4proto tcp ct bytes > \$first10s jump mark_10s"
else
downprio_sustained_rules="# Sustained TCP down-prioritization disabled"
fi
if [ "$DOWNRATE" -eq 0 ] && { [ "$TCP_DOWNPRIO_INITIAL_ENABLED" -eq 1 ] || [ "$TCP_DOWNPRIO_SUSTAINED_ENABLED" -eq 1 ]; }; then
log_msg -warn "TCP down-prioritization disabled: its byte thresholds are derived from DOWNRATE, which is 0."
fi
# Conditionally defining TCPMSS rules based on UPRATE and DOWNRATE
# A rate of 0 means "this direction is not shaped", not "slow link", so no clamping then
if [ "$UPRATE" -gt 0 ] && [ "$UPRATE" -lt 3000 ]; then
# Clamp MSS between 536 and 1500
# Use iifname (ingress from WAN) to clamp SYN-ACK from server, limiting our upload packet size
SAFE_MSS=$(( MSS > 1500 ? 1500 : (MSS < 536 ? 536 : MSS) ))
RULE_SET_TCPMSS_UP="meta iifname \"$WAN\" tcp flags syn tcp option maxseg size set $SAFE_MSS counter;"
else
RULE_SET_TCPMSS_UP=''
fi
if [ "$DOWNRATE" -gt 0 ] && [ "$DOWNRATE" -lt 3000 ]; then
# Clamp MSS between 536 and 1500
# Use oifname (egress to WAN) to clamp SYN to server, limiting our download packet size
SAFE_MSS=$(( MSS > 1500 ? 1500 : (MSS < 536 ? 536 : MSS) ))
RULE_SET_TCPMSS_DOWN="meta oifname \"$WAN\" tcp flags syn tcp option maxseg size set $SAFE_MSS counter;"
else
RULE_SET_TCPMSS_DOWN=''
fi
##############################
# Inline Rules Check
##############################
INLINE_FILE="/etc/qosmate.d/inline_dscptag.nft"
INLINE_INCLUDE=""
if [ -s "$INLINE_FILE" ]; then
TMP_CHECK_FILE="/tmp/qosmate_inline_sh_check.nft"
{
printf '%s\n\t%s\n' "table inet __qosmate_sh_ctx {" "chain __dscptag_sh_ctx {"
cat "$INLINE_FILE"
printf "\n\t%s\n%s\n" "}" "}"
} > "$TMP_CHECK_FILE"
if nft --check --file "$TMP_CHECK_FILE" 2>/dev/null; then
INLINE_INCLUDE="include \"$INLINE_FILE\""
fi
rm -f "$TMP_CHECK_FILE"
fi
##############################
# dscptag.nft
##############################
## Check if the folder does not exist
if [ ! -d "/usr/share/nftables.d/ruleset-post" ]; then
mkdir -p "/usr/share/nftables.d/ruleset-post"
fi
cat << DSCPEOF > /usr/share/nftables.d/ruleset-post/dscptag.nft
define udpbulkport = {$UDPBULKPORT}
define tcpbulkport = {$TCPBULKPORT}
define vidconfports = {$VIDCONFPORTS}
define realtime4 = {$REALTIME4}
define realtime6 = {$REALTIME6}
define lowpriolan4 = {$LOWPRIOLAN4}
define lowpriolan6 = {$LOWPRIOLAN6}
define downrate = $DOWNRATE
define uprate = $UPRATE
define first500ms = $FIRST500MS
define first10s = $FIRST10S
define wan = "$WAN"
table inet dscptag # forward declaration so the next command always works
delete table inet dscptag # clear all the rules
table inet dscptag {
map priomap { type dscp : classid ;
elements = {ef : 1:11, cs5 : 1:11, cs6 : 1:11, cs7 : 1:11,
cs4 : 1:12 , af41 : 1:12, af42 : 1:12,
cs2 : 1:14 , af11 : 1:14 , cs1 : 1:15, cs0 : 1:13}
}
# Create sets first
${SETS}
set xfst4ack { typeof ct id . ct direction
flags dynamic;
timeout 5m
}
set fast4ack { typeof ct id . ct direction
flags dynamic;
timeout 5m
}
set med4ack { typeof ct id . ct direction
flags dynamic;
timeout 5m
}
set slow4ack { typeof ct id . ct direction
flags dynamic;
timeout 5m
}
set udp_meter {typeof ct id . ct direction
flags dynamic;
timeout 5m
}
set slowtcp {typeof ct id . ct direction
flags dynamic;
timeout 5m
}
chain drop995 {
numgen random mod 1000 ge 995 return
drop
}
chain drop95 {
numgen random mod 1000 ge 950 return
drop
}
chain drop50 {
numgen random mod 1000 ge 500 return
drop
}
chain mark_500ms {
ip dscp < cs4 ip dscp != cs1 ip dscp set cs0 counter return
ip6 dscp < cs4 ip6 dscp != cs1 ip6 dscp set cs0 counter
}
chain mark_10s {
ip dscp < cs4 ip dscp set cs1 counter return
ip6 dscp < cs4 ip6 dscp set cs1 counter
}
chain mark_cs0 {
ip dscp set cs0 return
ip6 dscp set cs0
}
chain mark_cs1 {
ip dscp set cs1 return
ip6 dscp set cs1
}
chain mark_af42 {
ip dscp set af42 return
ip6 dscp set af42
}
chain dscptag {
type filter hook $NFT_HOOK priority $NFT_PRIORITY; policy accept;
iif "lo" accept
$(if { [ "$ROOT_QDISC" = "hfsc" ] || [ "$ROOT_QDISC" = "hybrid" ] || [ "$ROOT_QDISC" = "htb" ]; } && [ "$WASHDSCPDOWN" -eq 1 ]; then
echo "# wash all the DSCP on ingress ... "
echo " counter jump mark_cs0"
fi
)
# TCP MSS clamping for slow connections (must be before ingress accept)
$RULE_SET_TCPMSS_UP
$RULE_SET_TCPMSS_DOWN
# Skip rule processing for ingress packets since they're already classified by tc-ctinfo
meta iifname "$WAN" accept
$udpbulkport_rules
$tcpbulkport_rules
$ack_rules
$vidconfports_rules
$realtime4_rules
$realtime6_rules
$lowpriolan4_rules
$lowpriolan6_rules
$udp_rate_limit_rules
# down prioritize the first 500ms of tcp packets
$downprio_initial_rules
# downgrade tcp that has transferred more than 10 seconds worth of packets
$downprio_sustained_rules
$tcp_upgrade_rules
# --- user inline rules begin ---
$INLINE_INCLUDE
# --- user inline rules end ---
${DYNAMIC_RULES}
## classify for the HFSC queues:
meta priority set ip dscp map @priomap counter
meta priority set ip6 dscp map @priomap counter
# Store DSCP in conntrack for restoration on ingress
ct mark set ip dscp or 128 counter
ct mark set ip6 dscp or 128 counter
$(if { [ "$ROOT_QDISC" = "hfsc" ] || [ "$ROOT_QDISC" = "hybrid" ] || [ "$ROOT_QDISC" = "htb" ]; } && [ "$WASHDSCPUP" -eq 1 ]; then
echo "# wash all DSCP on egress ... "
echo "meta oifname \$wan jump mark_cs0"
fi
)
}
$(generate_ratelimit_rules)
}
DSCPEOF
## Set up ctinfo downstream shaping
if [ "$SHAPE_INGRESS" = 1 ]; then
print_msg "" "Setting up ctinfo downstream shaping..."
# Set up ingress handle for WAN interface
tc qdisc add dev "$WAN" handle ffff: ingress
# Create IFB interface (multi-queue when USE_MQ is enabled for cake_mq ingress support)
# Match the WAN TX queue count so egress and ingress CAKE instances are symmetric
ifb_mq_args=""
if [ "$USE_MQ" = "1" ]; then
wan_tx_queues=$(find /sys/class/net/"$WAN"/queues/ -maxdepth 1 -type d -name 'tx-*' 2>/dev/null | wc -l)
[ "$wan_tx_queues" -gt 1 ] && ifb_mq_args="numtxqueues $wan_tx_queues"
fi
# shellcheck disable=SC2086 # ifb_mq_args needs word splitting (e.g. "numtxqueues 4" → two args)
ip link add name "ifb-$WAN" $ifb_mq_args type ifb
ip link set "ifb-$WAN" up
# Redirect ingress traffic from WAN to IFB and restore DSCP from conntrack
tc filter add dev "$WAN" parent ffff: protocol all matchall action ctinfo dscp 63 128 mirred egress redirect dev "ifb-$WAN"
LAN=ifb-$WAN
else
# Rate 0 disables this direction: drop a previously created ingress path
print_msg "" "Ingress shaping disabled (DOWNRATE=0) - removing ingress path."
LAN=''
tc qdisc del dev "ifb-$WAN" root > /dev/null 2>&1
tc qdisc del dev "$WAN" ingress > /dev/null 2>&1
ip link del "ifb-$WAN" > /dev/null 2>&1
fi
# Minimum jitter estimate; only meaningful for a direction that is actually shaped
jitter_up="not shaped (UPRATE=0)"
jitter_down="not shaped (DOWNRATE=0)"
[ "$SHAPE_EGRESS" = 1 ] && jitter_up="$(((1500*8)*3/UPRATE)) ms"
[ "$SHAPE_INGRESS" = 1 ] && jitter_down="$(((1500*8)*3/DOWNRATE)) ms"
cat <<EOF
This script prioritizes the UDP packets from / to a set of gaming
machines into a real-time HFSC queue with guaranteed total bandwidth
Based on your settings:
Game upload guarantee = $GAMEUP kbps
Game download guarantee = $GAMEDOWN kbps
Download direction only works if you install this on a *wired* router
and there is a separate AP wired into your network, because otherwise
there are multiple parallel queues for traffic to leave your router
heading to the LAN.
Based on your link total bandwidth, the **minimum** amount of jitter
you should expect in your network is about:
UP = $jitter_up
DOWN = $jitter_down
In order to get lower minimum jitter you must upgrade the speed of
your link, no queuing system can help.
Please note for your display rate that:
at 30Hz, one on screen frame lasts: 33.3 ms
at 60Hz, one on screen frame lasts: 16.6 ms
at 144Hz, one on screen frame lasts: 6.9 ms
This means the typical gamer is sensitive to as little as on the order
of 5ms of jitter. To get 5ms minimum jitter you should have bandwidth
in each direction of at least:
$((1500*8*3/5)) kbps
The queue system can ONLY control bandwidth and jitter in the link
between your router and the VERY FIRST device in the ISP
network. Typically you will have 5 to 10 devices between your router
and your gaming server, any of those can have variable delay and ruin
your gaming, and there is NOTHING that your router can do about it.
EOF
##############################
# QoS Setup Functions
##############################
# 1 - device
# 2 - class enum
# 3 - family (ipv4|ipv6)
add_tc_filter() {
local class_id dsfield hex_match proto prio match_str \
dev="$1" \
class_enum="$2" \
family="$3"
case "$class_enum" in
cs0|CS0) class_id=1:13 dsfield=0x00 hex_match=0x0000 ;; # 0 -> Default
ef|EF) class_id=1:11 dsfield=0xb8 hex_match=0x0B80 ;; # 46
cs1|CS1) class_id=1:15 dsfield=0x20 hex_match=0x0200 ;; # 8
cs2|CS2) class_id=1:14 dsfield=0x40 hex_match=0x0400 ;; # 16
cs4|CS4) class_id=1:12 dsfield=0x80 hex_match=0x0800 ;; # 32
cs5|CS5) class_id=1:11 dsfield=0xa0 hex_match=0x0A00 ;; # 40
cs6|CS6) class_id=1:11 dsfield=0xc0 hex_match=0x0C00 ;; # 48
cs7|CS7) class_id=1:11 dsfield=0xe0 hex_match=0x0E00 ;; # 56
af11|AF11) class_id=1:14 dsfield=0x28 hex_match=0x0280 ;; # 10
af41|AF41) class_id=1:12 dsfield=0x88 hex_match=0x0880 ;; # 34
af42|AF42) class_id=1:12 dsfield=0x90 hex_match=0x0900 ;; # 36
*) # TODO: throw an error
esac
case "$family" in
ipv4)
proto=ip prio=10 match_str="ip dsfield $dsfield 0xfc"
;;
ipv6)
proto=ipv6 prio=11 match_str="u16 $hex_match 0x0FC0 at 0"
;;
esac
# shellcheck disable=SC2086
tc filter add dev "$dev" parent 1: protocol "$proto" prio "$prio" u32 match $match_str classid "$class_id"
}
# Function to setup the specific game qdisc (pfifo, red, fq_codel, netem, etc.)
# Arguments: $1:DEV, $2:RATE, $3:GAMERATE, $4:QDISC_TYPE, $5:DIR, $6:MTU, ... HFSC params ...
setup_game_qdisc() {
local DEV="$1" RATE="$2" GAMERATE="$3" QDISC_TYPE="$4" DIR="$5" MTU="$6"
local MAXDEL="$7" PFIFOMIN="$8" PACKETSIZE="$9"
local netemdelayms="${10}" netemjitterms="${11}" netemdist="${12}" NETEM_DIRECTION="${13}" pktlossp="${14}"
# Ensure rates/packetsize are non-zero to avoid errors in calculations
[ "$RATE" -le 0 ] && RATE=1
[ "$GAMERATE" -le 0 ] && GAMERATE=1
[ "$PACKETSIZE" -le 0 ] && PACKETSIZE=1
# Calculate REDMIN and REDMAX based on gamerate and MAXDEL
local REDMIN=$((GAMERATE * MAXDEL / 3 / 8))
local REDMAX=$((GAMERATE * MAXDEL / 8))
# Calculate BURST: (min + min + max)/(3 * avpkt) as per RED documentation
local BURST=$(( (REDMIN + REDMIN + REDMAX) / (3 * 500) )); [ $BURST -lt 2 ] && BURST=2
# for fq_codel
local INTVL=$((100+2*1500*8/RATE))
local TARG=$((540*8/RATE+4))
# Delete previous qdisc on this handle if it exists (optional, but good practice)
tc qdisc del dev "$DEV" parent 1:11 handle 10: > /dev/null 2>&1
case $QDISC_TYPE in
"drr")
tc qdisc add dev "$DEV" parent 1:11 handle 10: drr
tc class add dev "$DEV" parent 10: classid 10:1 drr quantum 8000
tc qdisc add dev "$DEV" parent 10:1 handle 11: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" probability 1.0 burst $BURST
tc class add dev "$DEV" parent 10: classid 10:2 drr quantum 4000
tc qdisc add dev "$DEV" parent 10:2 handle 12: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" probability 1.0 burst $BURST
tc class add dev "$DEV" parent 10: classid 10:3 drr quantum 1000
tc qdisc add dev "$DEV" parent 10:3 handle 13: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" probability 1.0 burst $BURST
;;
"qfq")
tc qdisc add dev "$DEV" parent 1:11 handle 10: qfq
tc class add dev "$DEV" parent 10: classid 10:1 qfq weight 8000
tc qdisc add dev "$DEV" parent 10:1 handle 11: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" probability 1.0 burst $BURST
tc class add dev "$DEV" parent 10: classid 10:2 qfq weight 4000
tc qdisc add dev "$DEV" parent 10:2 handle 12: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" probability 1.0 burst $BURST
tc class add dev "$DEV" parent 10: classid 10:3 qfq weight 1000
tc qdisc add dev "$DEV" parent 10:3 handle 13: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" probability 1.0 burst $BURST
;;
"pfifo")
tc qdisc add dev "$DEV" parent 1:11 handle 10: pfifo limit $((PFIFOMIN+MAXDEL*RATE/8/PACKETSIZE))
;;
"bfifo")
tc qdisc add dev "$DEV" parent 1:11 handle 10: bfifo limit $((MAXDEL * GAMERATE / 8))
#tc qdisc add dev "$DEV" parent 1:11 handle 10: bfifo limit $((MAXDEL * RATE / 8))
;;
"red")
tc qdisc add dev "$DEV" parent 1:11 handle 10: red limit 150000 min $REDMIN max $REDMAX avpkt 500 bandwidth "${RATE}kbit" burst $BURST probability 1.0
## send game packets to 10:, they're all treated the same
;;
"fq_codel")
tc qdisc add dev "$DEV" parent "1:11" handle 10: fq_codel memory_limit $((RATE*200/8)) interval "${INTVL}ms" target "${TARG}ms" quantum $((MTU * 2))
;;
"netem")
# Only apply NETEM if this direction is enabled
if [ "$NETEM_DIRECTION" = "both" ] || \
{ [ "$NETEM_DIRECTION" = "egress" ] && [ "$DIR" = "wan" ]; } || \
{ [ "$NETEM_DIRECTION" = "ingress" ] && [ "$DIR" = "lan" ]; }; then
NETEM_CMD="tc qdisc add dev \"$DEV\" parent 1:11 handle 10: netem limit $((4+9*RATE/8/500))"
# If jitter is set but delay is 0, force minimum delay of 1ms
if [ "$netemjitterms" -ne 0 ] && [ "$netemdelayms" -eq 0 ]; then
netemdelayms=1
fi
# Add delay parameter if set (either original or forced minimum)
if [ "$netemdelayms" -ne 0 ]; then
NETEM_CMD="$NETEM_CMD delay ${netemdelayms}ms"
# Add jitter if set
if [ "$netemjitterms" -ne 0 ]; then
NETEM_CMD="$NETEM_CMD ${netemjitterms}ms"
NETEM_CMD="$NETEM_CMD distribution $netemdist"
fi
fi
# Add packet loss if set
if [ "$pktlossp" != "none" ] && [ -n "$pktlossp" ]; then
NETEM_CMD="$NETEM_CMD loss $pktlossp"
fi
eval "$NETEM_CMD"
else
# Use pfifo as fallback when NETEM is not applied in this direction
tc qdisc add dev "$DEV" parent 1:11 handle 10: pfifo limit $((PFIFOMIN+MAXDEL*RATE/8/PACKETSIZE))
fi
;;
*)
print_msg -err "Unsupported game qdisc type '$QDISC_TYPE'. Using pfifo fallback."
# pfifo fallback limit calculation
tc qdisc add dev "$DEV" parent 1:11 handle 10: pfifo limit $((PFIFOMIN+MAXDEL*RATE/8/PACKETSIZE))
;;
esac
}
# Function to setup HFSC qdisc structure
# Arguments: $1:DEV, $2:RATE, $3:GAMERATE, $4:GAME_QDISC_TYPE, $5:DIR
setup_hfsc() {
local DEV="$1" RATE="$2" GAMERATE="$3" GAME_QDISC_TYPE="$4" DIR="$5"
local MTU=1500
tc qdisc del dev "$DEV" root > /dev/null 2>&1
# Get overhead parameters from CAKE configuration
local TC_OH_PARAMS
TC_OH_PARAMS=$(get_tc_overhead_params)
# Apply root qdisc
# shellcheck disable=SC2086
tc qdisc replace dev "$DEV" handle 1: root ${TC_OH_PARAMS} hfsc default 13
# DUR calculation
local DUR=$((5*1500*8/RATE)); [ $DUR -lt 25 ] && DUR=25
# Router traffic class (only on LAN/IFB)
if [ "$DIR" = "lan" ]; then
tc class add dev "$DEV" parent 1: classid 1:2 hfsc ls m1 50000kbit d "${DUR}ms" m2 10000kbit
fi
# Main link class
tc class add dev "$DEV" parent 1: classid 1:1 hfsc ls m2 "${RATE}kbit" ul m2 "${RATE}kbit"
# gameburst calculation
local gameburst=$((GAMERATE*10)); [ $gameburst -gt $((RATE*97/100)) ] && gameburst=$((RATE*97/100));
# Define HFSC Classes
tc class add dev "$DEV" parent 1:1 classid 1:11 hfsc rt m1 "${gameburst}kbit" d "${DUR}ms" m2 "${GAMERATE}kbit" # Realtime
tc class add dev "$DEV" parent 1:1 classid 1:12 hfsc ls m1 "$((RATE*70/100))kbit" d "${DUR}ms" m2 "$((RATE*30/100))kbit" # Fast
tc class add dev "$DEV" parent 1:1 classid 1:13 hfsc ls m1 "$((RATE*20/100))kbit" d "${DUR}ms" m2 "$((RATE*45/100))kbit" # Normal (Default)
tc class add dev "$DEV" parent 1:1 classid 1:14 hfsc ls m1 "$((RATE*7/100))kbit" d "${DUR}ms" m2 "$((RATE*15/100))kbit" # Low Prio
tc class add dev "$DEV" parent 1:1 classid 1:15 hfsc ls m1 "$((RATE*3/100))kbit" d "${DUR}ms" m2 "$((RATE*10/100))kbit" # Bulk
# Attach Qdiscs
setup_game_qdisc "$DEV" "$RATE" "$GAMERATE" "$GAME_QDISC_TYPE" "$DIR" \
"$MTU" "$MAXDEL" "$PFIFOMIN" "$PACKETSIZE" \
"$netemdelayms" "$netemjitterms" "$netemdist" "$NETEM_DIRECTION" "$pktlossp"
# Attach non-game qdiscs
local INTVL=$((100+2*1500*8/RATE))
local TARG=$((540*8/RATE+4))
for i in 12 13 14 15; do
if [ "$nongameqdisc" = "cake" ]; then
# shellcheck disable=SC2086 # nongameqdiscoptions needs word splitting (e.g. "besteffort ack-filter")
tc qdisc add dev "$DEV" parent "1:$i" cake $nongameqdiscoptions
elif [ "$nongameqdisc" = "fq_codel" ]; then
tc qdisc add dev "$DEV" parent "1:$i" fq_codel memory_limit "$((RATE*200/8))" interval "${INTVL}ms" target "${TARG}ms" quantum "$((MTU * 2))"
else
print_msg -err "Unsupported qdisc for non-game traffic: $nongameqdisc"
exit 1
fi
done
# Apply DSCP Filters (on ingress always, on egress only when SFO active)
# Ingress always needs filters, egress needs them only with SFO
# Without SFO: nftables priomap handles egress classification
# With SFO: nftables bypassed, tc filters needed for classification
if [ "$DIR" = "lan" ] || [ "$SFO_ENABLED" = "1" ]; then
# Delete existing filters first
tc filter del dev "$DEV" parent 1: prio 1 > /dev/null 2>&1
tc filter del dev "$DEV" parent 1: prio 2 > /dev/null 2>&1 # Also delete prio 2
local family class_enum
for family in ipv4 ipv6; do
for class_enum in ef cs5 cs6 cs7 cs4 af41 af42 cs2 af11 cs1 cs0; do
add_tc_filter "$DEV" "$class_enum" "$family"
done
done
fi
:
}
qdisc_setup_failed() {
[ -n "$1" ] && error_out "$1"
error_out "Failed to set up $ROOT_QDISC."
# *** Any additional error handling needed? ***
exit 1
}
# Appends option to ${CAKE_OPTS}
# 1: parameter: nat|wash|ack_filter|*
# 2: selector (1|0)
# for wash, nat, ack-filter: selector value '1' translates to prefix '', any other value translates to prefix 'no[-]'
# for other options: selector value '1' translates to 'don't skip option', any other value translates to 'skip option'
append_cake_opt() {
[ ${#} = 2 ] || { error_out "append_cake_opt: invalid args '$*'."; return 1; }
local prefix='' \
param="$1" selector="$2"
[ -n "$param" ] || return 0
[ "$selector" != 1 ] &&
case "$param" in
wash|nat) prefix='no' ;;
ack-filter) prefix='no-' ;;
*) return 0 ;;
esac
CAKE_OPTS="${CAKE_OPTS} ${prefix}${param}"
:
}
# Function to setup CAKE qdisc
setup_cake() {
tc qdisc del dev "$WAN" root > /dev/null 2>&1
tc qdisc del dev "$LAN" root > /dev/null 2>&1
# Get CAKE link parameters
local ack_filter_egress_val cake_link_params="$(get_cake_link_params)"
# Select cake or cake_mq based on WAN capabilities (IFB mirrors WAN queue count)
local CAKE_QDISC_EGR CAKE_QDISC_IGR CAKE_OPTS
select_cake_qdisc "$WAN"
CAKE_QDISC_EGR="$REPLY"
CAKE_QDISC_IGR="$REPLY"
# Egress (Upload) CAKE setup
if [ "$SHAPE_EGRESS" = 1 ]; then
case "$ACK_FILTER_EGRESS" in
# 'auto' needs a known download rate; keep the filter off without ingress shaping
auto) ack_filter_egress_val=$(( DOWNRATE > 0 && (DOWNRATE / UPRATE) >= 15 )) ;;
*[!0-9]*|'') qdisc_setup_failed "Invalid value '$ACK_FILTER_EGRESS' for ACK_FILTER_EGRESS." ;;
*) ack_filter_egress_val=$ACK_FILTER_EGRESS ;;
esac
CAKE_OPTS="bandwidth ${UPRATE}kbit"
# shellcheck disable=SC2086
append_cake_opt "$PRIORITY_QUEUE_EGRESS" "1" &&
append_cake_opt "dual-srchost" "$HOST_ISOLATION" &&
append_cake_opt "rtt ${RTT}ms" "${RTT:+1}" &&
append_cake_opt "$cake_link_params" "1" &&
append_cake_opt "$LINK_COMPENSATION" "1" &&
append_cake_opt "$EXTRA_PARAMETERS_EGRESS" "1" &&
append_cake_opt "nat" "$NAT_EGRESS" &&
append_cake_opt "wash" "$WASHDSCPUP" &&
append_cake_opt "ack-filter" "$ack_filter_egress_val" &&
tc qdisc add dev "$WAN" root handle 1: "$CAKE_QDISC_EGR" $CAKE_OPTS || qdisc_setup_failed
debug_log "EGRESS $CAKE_QDISC_EGR opts: '$CAKE_OPTS'"
fi
# Ingress (Download) CAKE setup
if [ "$SHAPE_INGRESS" = 1 ]; then
CAKE_OPTS="bandwidth ${DOWNRATE}kbit ingress"
# shellcheck disable=SC2086
append_cake_opt "autorate-ingress" "$AUTORATE_INGRESS" &&
append_cake_opt "$PRIORITY_QUEUE_INGRESS" "1" &&
append_cake_opt "dual-dsthost" "$HOST_ISOLATION" &&
append_cake_opt "rtt ${RTT}ms" "${RTT:+1}" &&
append_cake_opt "$cake_link_params" "1" &&
append_cake_opt "$LINK_COMPENSATION" "1" &&
append_cake_opt "$EXTRA_PARAMETERS_INGRESS" "1" &&
append_cake_opt "nat" "$NAT_INGRESS" &&
append_cake_opt "wash" "$WASHDSCPDOWN" &&
tc qdisc add dev "$LAN" root "$CAKE_QDISC_IGR" $CAKE_OPTS || qdisc_setup_failed
debug_log "INGRESS $CAKE_QDISC_IGR opts: '$CAKE_OPTS'"
fi
# Write active cake qdisc type for autorate daemon
printf '%s\n' "$CAKE_QDISC_EGR" > /tmp/qosmate/cake_type
}
# Helper function to set up hybrid qdisc on an interface
# Arguments: $1:DEV, $2:RATE, $3:GAMERATE, $4:DIR
setup_hybrid() {
local DEV="$1" RATE="$2" GAMERATE="$3" DIR="$4"
local MTU=1500
# Calculate parameters
local DUR=$((5*1500*8/RATE)); [ $DUR -lt 25 ] && DUR=25
local gameburst=$((GAMERATE*10)); [ $gameburst -gt $((RATE*97/100)) ] && gameburst=$((RATE*97/100));
# Setup root HFSC qdisc (default to 1:13 - CAKE class)
local TC_OH_PARAMS
TC_OH_PARAMS=$(get_tc_overhead_params)
# Ensure previous root is deleted before replacing
tc qdisc del dev "$DEV" root > /dev/null 2>&1
tc qdisc replace dev "$DEV" handle 1: root ${TC_OH_PARAMS} hfsc default 13
# Router traffic class (only on LAN/IFB)
if [ "$DIR" = "lan" ]; then
tc class add dev "$DEV" parent 1: classid 1:2 hfsc ls m1 50000kbit d "${DUR}ms" m2 10000kbit
fi
# Main link class
tc class add dev "$DEV" parent 1: classid 1:1 hfsc ls m2 "${RATE}kbit" ul m2 "${RATE}kbit"
# Class 1:11 - High priority realtime (HFSC RT + gameqdisc)
tc class add dev "$DEV" parent 1:1 classid 1:11 hfsc rt m1 "${gameburst}kbit" d "${DUR}ms" m2 "${GAMERATE}kbit"
# Attach game qdisc (using $gameqdisc from HFSC config)
setup_game_qdisc "$DEV" "$RATE" "$GAMERATE" "$gameqdisc" "$DIR" \
"$MTU" "$MAXDEL" "$PFIFOMIN" "$PACKETSIZE" \
"$netemdelayms" "$netemjitterms" "$netemdist" "$NETEM_DIRECTION" "$pktlossp"
# Class 1:13 - CAKE class (most traffic - default)
local cake_rate=$((RATE - GAMERATE)); [ $cake_rate -le 0 ] && cake_rate=1
tc class add dev "$DEV" parent 1:1 classid 1:13 hfsc ls m1 "${cake_rate}kbit" d "${DUR}ms" m2 "${cake_rate}kbit"
# Attach CAKE qdisc - use "hybrid" mode to match HFSC overhead
local cake_link_params="$(get_cake_link_params "hybrid")"
local CAKE_OPTS=""
tc qdisc del dev "$DEV" parent 1:13 handle 13: > /dev/null 2>&1
# shellcheck disable=SC2086
if [ "$DIR" = "wan" ]; then
CAKE_OPTS="besteffort" # Default for non-realtime in hybrid
append_cake_opt "dual-srchost" "$HOST_ISOLATION" &&
append_cake_opt "$EXTRA_PARAMETERS_EGRESS" "1" &&
append_cake_opt "nat" "$NAT_EGRESS" &&
append_cake_opt "wash" "$WASHDSCPUP"
else # lan (ingress)
CAKE_OPTS="besteffort ingress" # Default for non-realtime in hybrid
append_cake_opt "dual-dsthost" "$HOST_ISOLATION" &&
append_cake_opt "$EXTRA_PARAMETERS_INGRESS" "1" &&
append_cake_opt "nat" "$NAT_INGRESS" &&
append_cake_opt "wash" "$WASHDSCPDOWN"
fi &&
append_cake_opt "rtt ${RTT}ms" "${RTT:+1}" &&
append_cake_opt "$cake_link_params" "1" &&
append_cake_opt "$LINK_COMPENSATION" "1" &&
tc qdisc replace dev "$DEV" parent 1:13 handle 13: cake $CAKE_OPTS || qdisc_setup_failed
debug_log "$DIR HYBRID cake opts: '$CAKE_OPTS'"
# Class 1:15 - Bulk traffic (HFSC LS + fq_codel)
# Use HFSC limits: m1 3%, m2 10%
local bulk_rate_m1=$((RATE*3/100)); [ $bulk_rate_m1 -le 0 ] && bulk_rate_m1=1
local bulk_rate_m2=$((RATE*10/100)); [ $bulk_rate_m2 -le 0 ] && bulk_rate_m2=1
tc class add dev "$DEV" parent 1:1 classid 1:15 hfsc ls m1 "${bulk_rate_m1}kbit" d "${DUR}ms" m2 "${bulk_rate_m2}kbit"
# Attach fq_codel (using calculations and options from HFSC config)
local INTVL=$((100+2*1500*8/RATE))
local TARG=$((540*8/RATE+4))
tc qdisc del dev "$DEV" parent 1:15 handle 15: > /dev/null 2>&1
tc qdisc replace dev "$DEV" parent 1:15 handle 15: fq_codel memory_limit $((RATE*200/8)) interval "${INTVL}ms" target "${TARG}ms" quantum $((MTU * 2))
# Apply DSCP Filters (on ingress always, on egress only when SFO active)
if [ "$DIR" = "lan" ] || [ "$SFO_ENABLED" = "1" ]; then
# Delete existing filters
tc filter del dev "$DEV" parent 1: prio 1 > /dev/null 2>&1
tc filter del dev "$DEV" parent 1: prio 2 > /dev/null 2>&1
local class_enum
# IPv4 Filters (prio 1)
# EF, CS5, CS6, CS7 -> Realtime
# CS1 -> Bulk
for class_enum in ef cs5 cs6 cs7 cs1; do
add_tc_filter "$DEV" "$class_enum" "ipv4"
done
# Default rule sends to 1:13 (CAKE)
# IPv6 Filters (prio 2)
for class_enum in ef cs5 cs6 cs7 cs1 cs0; do
add_tc_filter "$DEV" "$class_enum" "ipv6"
done
fi
}
# Helper functions for HTB dynamic parameter calculation
# Calculate optimal HTB quantum based on rate
calculate_htb_quantum() {
local rate="$1"
local duration_us="${2:-1000}" # Default 1ms = 1000µs
local MTU=1500
# Duration-based calculation (SQM-style)
# rate in kbit/s, duration in µs, result in bytes
local quantum=$(((duration_us * rate) / 8000))
# ATM-aware minimum
if [ "$COMMON_LINK_PRESETS" = "atm" ]; then
local min_quantum=$(((MTU + 48 + 47) / 48 * 53))
[ $quantum -lt $min_quantum ] && quantum=$min_quantum
else
[ $quantum -lt $MTU ] && quantum=$MTU
fi
# Maximum reasonable quantum (200KB)
[ $quantum -gt 200000 ] && quantum=200000
echo $quantum
}
# Calculate HTB burst size based on rate and target latency
calculate_htb_burst() {
local rate="$1"
local duration_us="${2:-10000}" # Default 10ms = 10000µs
# burst in bytes for given duration
local burst=$(((duration_us * rate) / 8000))
# Minimum burst should be at least 1 MTU
[ $burst -lt 1500 ] && burst=1500
echo $burst
}
# Function to setup HTB qdisc (simple.qos style with 3 classes)
setup_htb() {
local DEV="$1" RATE="$2" DIR="$3"
local MTU=1500
# Ensure rate is valid
[ "$RATE" -le 0 ] && RATE=1
# Delete existing qdisc
tc qdisc del dev "$DEV" root > /dev/null 2>&1
# Get overhead parameters from CAKE configuration
local TC_OH_PARAMS
TC_OH_PARAMS=$(get_tc_overhead_params)
# Setup HTB root with default to best effort (class 13)
tc qdisc add dev "$DEV" root handle 1: $TC_OH_PARAMS htb default 13
# Calculate HTB quantum for root (all use same quantum)
local HTB_QUANTUM="$(calculate_htb_quantum "$RATE")"
# Root class gets modest burst since we typically configure 80-90% of physical rate
# This allows brief bursts into the headroom without causing bufferbloat
local ROOT_BURST="$(calculate_htb_burst "$RATE" 1000)" # 1ms burst
local ROOT_CBURST="$(calculate_htb_burst "$RATE" 1000)" # 1ms cburst
# Create main rate limiting class
tc class add dev "$DEV" parent 1: classid 1:1 htb \
quantum "$HTB_QUANTUM" \
rate "${RATE}kbit" ceil "${RATE}kbit" \
burst "$ROOT_BURST" cburst "$ROOT_CBURST"
# Smart calculation that scales smoothly across all bandwidths
# Formula: percent = 15 + (50000 / RATE), capped between 5-40%
#
# This creates a hyperbolic curve that provides:
# - High percentage (up to 40%) for very low bandwidth connections
# - Smooth decrease as bandwidth increases
# - Stabilizes around 15% for high bandwidth connections
#
# Examples:
# - 1 Mbit: 15 + 50 = 65% → capped at 40% → 400 kbit → min 800 kbit
# - 5 Mbit: 15 + 10 = 25% → 1250 kbit
# - 10 Mbit: 15 + 5 = 20% → 2000 kbit
# - 50 Mbit: 15 + 1 = 16% → 8000 kbit
# - 100 Mbit: 15 + 0.5 = 15.5% → 15500 kbit
#
# Visualization:
# 40% |*
# | *
# 30% | *
# | *
# 20% | * * * * *
# 15% | * * * * * * * *
# +---------------------------------> Bandwidth
# 1 5 10 20 50 100 200 Mbit
#
# Two safety mechanisms ensure adequate priority bandwidth:
# 1. Percentage-based: Scales with total bandwidth
# 2. Absolute minimum: 800 kbit for gaming/VoIP needs
# Calculate sliding percentage (higher % for lower rates)
local percent=$((15 + 50000 / RATE))
[ $percent -gt 40 ] && percent=40 # Cap at 40%
[ $percent -lt 5 ] && percent=5 # Floor at 5%
local percent_based=$((RATE * percent / 100))
local absolute_min=800 # Gaming/VoIP minimum
# Take the maximum of percentage-based and absolute minimum
local PRIO_RATE_MIN=$percent_based
[ $absolute_min -gt $PRIO_RATE_MIN ] && PRIO_RATE_MIN=$absolute_min
# Calculate ceiling - ensure it's at least min + some headroom
local PRIO_CEIL=$((RATE / 3)) # Start with 33%
# Ensure ceiling is at least min rate + 10%
local min_ceiling=$((PRIO_RATE_MIN * 110 / 100))
[ $PRIO_CEIL -lt $min_ceiling ] && PRIO_CEIL=$min_ceiling
# Calculate BE and BK rates
local BE_MIN_RATE=$((RATE / 6)) # 16% guaranteed
local BK_MIN_RATE=$((RATE / 6)) # 16% guaranteed
# Adjust if total mins exceed available bandwidth
local total_min=$((PRIO_RATE_MIN + BE_MIN_RATE + BK_MIN_RATE))
if [ $total_min -gt $((RATE * 90 / 100)) ]; then
# Scale down proportionally
BE_MIN_RATE=$((BE_MIN_RATE * RATE * 90 / 100 / total_min))
BK_MIN_RATE=$((BK_MIN_RATE * RATE * 90 / 100 / total_min))
fi
# BE/BK ceiling - almost full rate minus a small reserve
local BE_CEIL=$((RATE - 16))
# Calculate individual burst values for each class
# Priority class burst - based on its own rate
local PRIO_BURST="$(calculate_htb_burst $PRIO_RATE_MIN 10000)" # 10ms burst for rate
local PRIO_CBURST="$(calculate_htb_burst $PRIO_RATE_MIN 5000)" # 5ms burst for ceiling
[ "$PRIO_CBURST" -lt 1500 ] && PRIO_CBURST=1500
# Priority class (1:11) - for realtime/gaming traffic
tc class add dev "$DEV" parent 1:1 classid 1:11 htb \
quantum "$HTB_QUANTUM" \
rate "${PRIO_RATE_MIN}kbit" ceil "${PRIO_CEIL}kbit" \
burst "$PRIO_BURST" cburst "$PRIO_CBURST" prio 1
# Calculate BE burst values - based on its own guaranteed rate
local BE_BURST="$(calculate_htb_burst $BE_MIN_RATE 10000)" # 10ms burst for rate
local BE_CBURST="$(calculate_htb_burst $BE_MIN_RATE 5000)" # 5ms burst for ceiling
[ "$BE_CBURST" -lt 1500 ] && BE_CBURST=1500
# Best Effort class (1:13) - default traffic
tc class add dev "$DEV" parent 1:1 classid 1:13 htb \
quantum "$HTB_QUANTUM" \
rate "${BE_MIN_RATE}kbit" ceil "${BE_CEIL}kbit" \
burst "$BE_BURST" cburst "$BE_CBURST" prio 2
# Calculate BK burst values - based on its own guaranteed rate
local BK_BURST="$(calculate_htb_burst $BK_MIN_RATE 10000)" # 10ms burst for rate
local BK_CBURST="$(calculate_htb_burst $BK_MIN_RATE 5000)" # 5ms burst for ceiling
[ "$BK_CBURST" -lt 1500 ] && BK_CBURST=1500
# Background/Bulk class (1:15) - low priority
tc class add dev "$DEV" parent 1:1 classid 1:15 htb \
quantum "$HTB_QUANTUM" \
rate "${BK_MIN_RATE}kbit" ceil "${BE_CEIL}kbit" \
burst "$BK_BURST" cburst "$BK_CBURST" prio 3
# Attach leaf qdiscs
# Calculate fq_codel parameters
local INTVL=$((100+2*1500*8/RATE))
local TARG=$((540*8/RATE+4))
# Priority class gets fq_codel with aggressive settings
tc qdisc add dev "$DEV" parent 1:11 handle 110: fq_codel \
interval "${INTVL}ms" target "${TARG}ms" \
quantum 300
# Best effort with standard settings
tc qdisc add dev "$DEV" parent 1:13 handle 130: fq_codel \
interval "${INTVL}ms" target "${TARG}ms" \
quantum 1500
# Background with larger target
tc qdisc add dev "$DEV" parent 1:15 handle 150: fq_codel \
interval "$((INTVL*2))ms" target "$((TARG*2))ms" \
quantum 300
# Apply DSCP filters (on ingress always, on egress only when SFO active)
if [ "$DIR" = "lan" ] || [ "$SFO_ENABLED" = "1" ]; then
# Delete existing filters
tc filter del dev "$DEV" parent 1: prio 1 > /dev/null 2>&1
tc filter del dev "$DEV" parent 1: prio 2 > /dev/null 2>&1
# IPv4 filters (prio 1)
# Priority class: EF, CS5, CS6, CS7 -> 1:11
# Background class: CS1 -> 1:15
for class_enum in ef cs5 cs6 cs7 cs1; do
add_tc_filter "$DEV" "$class_enum" "ipv4"
done
# IPv6 filters (prio 2)
for class_enum in ef cs5 cs6 cs7 cs1 cs0; do
add_tc_filter "$DEV" "$class_enum" "ipv6"
done
fi
}
##############################
# Main Logic
##############################
# Validate gameqdisc choice (used by HFSC and Hybrid)
if [ "$ROOT_QDISC" = "hfsc" ] || [ "$ROOT_QDISC" = "hybrid" ]; then
case "$gameqdisc" in
drr|qfq|pfifo|bfifo|red|fq_codel|netem) ;; # Supported qdiscs
*)
print_msg -warn "Unsupported gameqdisc '$gameqdisc' selected in config. Reverting to 'pfifo'."
gameqdisc="pfifo" # Revert to a simple default as fallback
;;
esac
fi
# A rate of 0 disables that direction. Remove leftovers of a disabled direction so that
# switching a rate to 0 takes effect even without a full service restart.
[ "$SHAPE_EGRESS" = 1 ] || {
print_msg "" "Egress shaping disabled (UPRATE=0) - removing root qdisc on $WAN."
tc qdisc del dev "$WAN" root > /dev/null 2>&1
}
[ "$SHAPE_EGRESS" = 1 ] || [ "$SHAPE_INGRESS" = 1 ] ||
log_msg -warn "No shaping active: both UPRATE and DOWNRATE are 0. Only nftables DSCP marking is applied."
# Main logic for selecting and applying the QoS system
case "$ROOT_QDISC" in
hfsc)
print_msg "Applying HFSC queueing discipline."
# Call the renamed function (formerly setqdisc)
[ "$SHAPE_EGRESS" = 1 ] && setup_hfsc "$WAN" "$UPRATE" "$GAMEUP" "$gameqdisc" wan
[ "$SHAPE_INGRESS" = 1 ] && setup_hfsc "$LAN" "$DOWNRATE" "$GAMEDOWN" "$gameqdisc" lan
;;
hybrid)
print_msg "Applying Hybrid (HFSC+CAKE) queueing discipline."
# Setup WAN (egress/upload) and LAN (ingress/download) directly
[ "$SHAPE_EGRESS" = 1 ] && setup_hybrid "$WAN" "$UPRATE" "$GAMEUP" "wan"
[ "$SHAPE_INGRESS" = 1 ] && setup_hybrid "$LAN" "$DOWNRATE" "$GAMEDOWN" "lan"
# cake_mq not supported as child qdisc under HFSC
printf '%s\n' "cake" > /tmp/qosmate/cake_type
;;
cake)
print_msg "Applying CAKE queueing discipline."
setup_cake
;;
htb)
print_msg "Applying HTB queueing discipline."
[ "$SHAPE_EGRESS" = 1 ] && setup_htb "$WAN" "$UPRATE" "wan"
[ "$SHAPE_INGRESS" = 1 ] && setup_htb "$LAN" "$DOWNRATE" "lan"
;;
*) # Fallback for unsupported ROOT_QDISC
print_msg -err "Unsupported ROOT_QDISC: '$ROOT_QDISC'. Check /etc/config/qosmate."
print_msg -warn "Falling back to default HFSC mode with pfifo game qdisc."
ROOT_QDISC="hfsc"
gameqdisc="pfifo" # Safe default for fallback
# Apply the fallback configuration using the renamed function
[ "$SHAPE_EGRESS" = 1 ] && setup_hfsc "$WAN" "$UPRATE" "$GAMEUP" "$gameqdisc" wan
[ "$SHAPE_INGRESS" = 1 ] && setup_hfsc "$LAN" "$DOWNRATE" "$GAMEDOWN" "$gameqdisc" lan
;;
esac
## Set up ctinfo for upstream (egress) - SFO compatibility
# Restore DSCP values from conntrack for egress packets
# Only needed when Software Flow Offloading is active
if [ "$SFO_ENABLED" = "1" ]; then
if [ "$SHAPE_EGRESS" = 1 ]; then
print_msg "" "Software Flow Offloading detected - enabling SFO compatibility mode..."
tc filter add dev "$WAN" parent 1: prio 1 protocol all matchall action ctinfo dscp 63 128 continue
else
# The filter attaches to the egress root qdisc, which does not exist without egress shaping
print_msg "" "Software Flow Offloading detected, but egress shaping is disabled - skipping SFO filter."
fi
else
print_msg "" "Software Flow Offloading disabled - dynamic rules fully functional..."
fi
print_msg "DONE!"
# Conditional output of tc status
if [ "$ROOT_QDISC" = "hfsc" ] && [ "$gameqdisc" = "red" ]; then
print_msg "Can not output tc -s qdisc because it crashes on OpenWrt when using RED qdisc, but things are working!"
# Add check for hybrid mode with red gameqdisc
elif [ "$ROOT_QDISC" = "hybrid" ] && [ "$gameqdisc" = "red" ]; then
print_msg "Can not output tc -s qdisc because it crashes on OpenWrt when using RED qdisc in hybrid mode, but things are working!"
else
# Check if tc command exists before trying to run it
if command -v tc >/dev/null; then
[ "$SHAPE_EGRESS" = 1 ] && { print_msg "--- Egress ($WAN) ---"; tc -s qdisc show dev "$WAN"; }
[ "$SHAPE_INGRESS" = 1 ] && { print_msg "--- Ingress ($LAN) ---"; tc -s qdisc show dev "$LAN"; }
else
print_msg "Warning: 'tc' command not found. Cannot display QoS status."
fi
fi
exit 0