Files
qos-profiles/qos-apply
T
zpericic 86a0d2cdd1 Initial import of qos-profiles
Role subpackages set the system-wide default qdisc and TCP congestion
control via /usr/lib/sysctl.d, without pulling in the base package:

  server   fq + bbr        bare metal, VM guests
  vmhost   fq_codel + bbr  hypervisors, container hosts
  router   fq_codel + cubic
  desktop  fq_codel + cubic

The base package applies cake per interface from
/etc/qos-profiles/<iface>.conf, layered over role defaults and
/etc/qos-profiles/defaults.conf:

- LINK_RATE with per-direction overrides, shaped to SHAPE_PERCENT
  (default 95, per direction overridable)
- outbound fairness per source host and diffserv4; inbound via ifb,
  fairness per destination host, besteffort with DSCP wash
- NAT awareness, extra cake args (shared and per direction), raw
  qdisc overrides
- config parsed without executing it, validated before any change,
  previous state undone on each run

Applied by the qos-profiles@<iface> unit (bound to the device) or the
qos-profiles-networkmanager dispatcher subpackage, installed
automatically on hosts with NetworkManager. Includes ready-made
examples for VM hosts, OVH dual-NIC servers, DC and home routers.
2026-10-10 22:41:12 +02:00

177 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/bash
# Apply or remove per-interface qdiscs (cake, with ifb for inbound shaping).
#
# Configuration is read, later files overriding earlier ones, from:
# /usr/lib/qos-profiles/role-*.conf defaults of the installed role package
# /etc/qos-profiles/defaults.conf site-wide defaults
# /etc/qos-profiles/<iface>.conf the interface
# See /usr/share/doc/qos-profiles/README.md for the variables.
set -euo pipefail
shopt -s nullglob
KEYS=(LINK_RATE EGRESS_RATE INGRESS_RATE
SHAPE_PERCENT EGRESS_SHAPE_PERCENT INGRESS_SHAPE_PERCENT
DIFFSERV INGRESS_DIFFSERV INGRESS_WASH NAT
CAKE_ARGS EGRESS_CAKE_ARGS INGRESS_CAKE_ARGS IFB EGRESS INGRESS)
CONF_DIR=/etc/qos-profiles
ROLE_DIR=/usr/lib/qos-profiles
STATE_DIR=/run/qos-profiles
die() {
echo "${0##*/}: $*" >&2
exit 1
}
# Parse KEY=value lines without executing them. Values may be "quoted",
# 'quoted' or bare; a trailing # comment is allowed.
load() {
local file=$1 line key val n=0
while IFS= read -r line || [[ -n $line ]]; do
n=$((n + 1))
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
[[ $line =~ ^[[:space:]]*([A-Z_]+)=(.*)$ ]] || die "$file:$n: invalid line"
key=${BASH_REMATCH[1]}
val=${BASH_REMATCH[2]}
[[ " ${KEYS[*]} " == *" $key "* ]] || die "$file:$n: unknown variable $key"
if [[ $val =~ ^\"([^\"]*)\"[[:space:]]*(#.*)?$ || $val =~ ^\'([^\']*)\'[[:space:]]*(#.*)?$ ]]; then
val=${BASH_REMATCH[1]}
elif [[ $val =~ ^([^[:space:]#\"\']*)[[:space:]]*(#.*)?$ ]]; then
val=${BASH_REMATCH[1]}
else
die "$file:$n: invalid value for $key"
fi
printf -v "$key" '%s' "$val"
done <"$file"
}
# Usage: yesno <variable> <keyword if yes> <keyword if no>
yesno() {
case ${!1,,} in
yes) echo "$2" ;;
no) echo "$3" ;;
*) die "$1 must be yes or no" ;;
esac
}
# Usage: percent <variable>
percent() {
[[ ${!1} =~ ^[0-9]+$ ]] && ((${!1} >= 1 && ${!1} <= 100)) || die "$1 must be 1-100"
echo "${!1}"
}
# Usage: shaped_rate <link rate> <percent>
# Link rate (e.g. 1gbit, 2.5Gbit, 500mbit) -> percent of it in kbit.
shaped_rate() {
awk -v r="${1,,}" -v p="$2" 'BEGIN {
n = r; sub(/[a-z]+$/, "", n); u = substr(r, length(n) + 1)
m["bit"] = 1; m[""] = 1; m["kbit"] = 1e3; m["mbit"] = 1e6; m["gbit"] = 1e9; m["tbit"] = 1e12
if (n !~ /^[0-9]+(\.[0-9]+)?$/ || !(u in m) || n == 0) exit 1
printf "%dkbit\n", n * m[u] * p / 100 / 1000
}' || die "invalid rate: $1"
}
# Build the qdisc for one direction into the named array.
# Usage: build <array> <raw spec> <link rate> <percent> <cake args> <diffserv> <isolation> [extra...]
build() {
local -n spec=$1
local raw=$2 rate=$3 pct=$4 args=$5 diffserv=$6 isolation=$7 bw extra
shift 7
spec=()
if [[ -n $raw ]]; then
read -ra spec <<<"$raw"
return
fi
[[ -n $rate && $rate != none ]] || return 0
spec=(cake "$diffserv" "$isolation" "$@")
if [[ $rate == unlimited ]]; then
spec+=(unlimited)
else
bw=$(shaped_rate "$rate" "$pct")
spec+=(bandwidth "$bw")
fi
read -ra extra <<<"$CAKE_ARGS $args"
spec+=("${extra[@]}")
}
# Undo whatever the last start applied, as recorded in the state file.
cleanup() {
local state=$STATE_DIR/$dev key val
[[ -f $state ]] || return 0
while IFS='=' read -r key val; do
case $key in
egress) tc qdisc del dev "$dev" root 2>/dev/null || true ;;
ifb)
tc qdisc del dev "$dev" clsact 2>/dev/null || true
ip link del dev "$val" 2>/dev/null || true
;;
esac
done <"$state"
rm -f "$state"
}
start() {
local -a egress ingress
local state=$STATE_DIR/$dev ifb egress_pct ingress_pct wash nat
[[ -d /sys/class/net/$dev ]] || die "no such interface: $dev"
: "${EGRESS_SHAPE_PERCENT:=$SHAPE_PERCENT}" "${INGRESS_SHAPE_PERCENT:=$SHAPE_PERCENT}"
egress_pct=$(percent EGRESS_SHAPE_PERCENT)
ingress_pct=$(percent INGRESS_SHAPE_PERCENT)
wash=$(yesno INGRESS_WASH wash nowash)
nat=$(yesno NAT nat nonat)
# Egress without any rate runs cake unlimited (the NIC is the bottleneck);
# ingress without a rate is not shaped, cake needs one there.
build egress "$EGRESS" "${EGRESS_RATE:-${LINK_RATE:-unlimited}}" \
"$egress_pct" "$EGRESS_CAKE_ARGS" "$DIFFSERV" dual-srchost "$nat"
build ingress "$INGRESS" "${INGRESS_RATE:-$LINK_RATE}" \
"$ingress_pct" "$INGRESS_CAKE_ARGS" "$INGRESS_DIFFSERV" dual-dsthost "$nat" \
ingress "$wash"
ifb=${IFB:-ifb-$dev}
((${#ingress[@]} == 0 || ${#ifb} <= 15)) ||
die "ifb name '$ifb' too long, set IFB= in $CONF_DIR/$dev.conf"
cleanup
if ((${#egress[@]})); then
tc qdisc replace dev "$dev" root "${egress[@]}"
echo egress=1 >>"$state"
fi
if ((${#ingress[@]})); then
ip link add name "$ifb" type ifb
echo "ifb=$ifb" >>"$state"
ip link set dev "$ifb" up
tc qdisc replace dev "$ifb" root "${ingress[@]}"
tc qdisc replace dev "$dev" clsact
tc filter add dev "$dev" ingress protocol all matchall \
action mirred egress redirect dev "$ifb"
fi
}
[[ $# -eq 2 ]] || die "usage: ${0##*/} start|stop <interface>"
action=$1
dev=$2
[[ $dev =~ ^[^/[:space:]]{1,15}$ && $dev != . && $dev != .. ]] || die "invalid interface name: $dev"
LINK_RATE= EGRESS_RATE= INGRESS_RATE=
SHAPE_PERCENT=95 EGRESS_SHAPE_PERCENT= INGRESS_SHAPE_PERCENT=
DIFFSERV=diffserv4 INGRESS_DIFFSERV=besteffort INGRESS_WASH=yes NAT=no
CAKE_ARGS= EGRESS_CAKE_ARGS= INGRESS_CAKE_ARGS= IFB= EGRESS= INGRESS=
for f in "$ROLE_DIR"/role-*.conf "$CONF_DIR/defaults.conf" "$CONF_DIR/$dev.conf"; do
[[ -f $f ]] && load "$f"
done
# Serialise the systemd unit and the NetworkManager dispatcher.
mkdir -p "$STATE_DIR"
exec 9>"$STATE_DIR/$dev.lock"
flock 9
case $action in
start) start ;;
stop) cleanup ;;
*) die "unknown action: $action" ;;
esac