#!/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/.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 yesno() { case ${!1,,} in yes) echo "$2" ;; no) echo "$3" ;; *) die "$1 must be yes or no" ;; esac } # Usage: percent percent() { [[ ${!1} =~ ^[0-9]+$ ]] && ((${!1} >= 1 && ${!1} <= 100)) || die "$1 must be 1-100" echo "${!1}" } # Usage: shaped_rate # 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 [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 " 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