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.
177 lines
5.5 KiB
Bash
Executable File
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
|