commit 86a0d2cdd153152d464f4892e15b5bdd949afd7f Author: Zoran Peričić Date: Sat Oct 10 22:41:12 2026 +0200 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/.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@ 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. diff --git a/50-qos-profiles b/50-qos-profiles new file mode 100755 index 0000000..f5e9ad1 --- /dev/null +++ b/50-qos-profiles @@ -0,0 +1,16 @@ +#!/usr/bin/bash +# NetworkManager dispatcher: apply qos-profiles when an interface with +# /etc/qos-profiles/.conf comes up, clean up when it is removed. +# Both the device and its IP interface are checked (eth0 and ppp0 for PPPoE). + +action=$2 +ifaces=("$1") +[[ -n ${DEVICE_IP_IFACE:-} && $DEVICE_IP_IFACE != "$1" ]] && ifaces+=("$DEVICE_IP_IFACE") + +for iface in "${ifaces[@]}"; do + [[ -n $iface && -f /etc/qos-profiles/$iface.conf ]] || continue + case $action in + up | reapply) /usr/libexec/qos-profiles/qos-apply start "$iface" ;; + down) [[ -e /sys/class/net/$iface ]] || /usr/libexec/qos-profiles/qos-apply stop "$iface" ;; + esac +done diff --git a/60-qos-profiles-desktop.conf b/60-qos-profiles-desktop.conf new file mode 100644 index 0000000..04adcc5 --- /dev/null +++ b/60-qos-profiles-desktop.conf @@ -0,0 +1,3 @@ +# qos-profiles-desktop: workstation. Same as the Fedora defaults, pinned explicitly. +-net.core.default_qdisc = fq_codel +-net.ipv4.tcp_congestion_control = cubic diff --git a/60-qos-profiles-router.conf b/60-qos-profiles-router.conf new file mode 100644 index 0000000..4a75e02 --- /dev/null +++ b/60-qos-profiles-router.conf @@ -0,0 +1,4 @@ +# qos-profiles-router: forwarding router. +# fq_codel on all interfaces; shape the WAN with cake in /etc/qos-profiles/.conf. +-net.core.default_qdisc = fq_codel +-net.ipv4.tcp_congestion_control = cubic diff --git a/60-qos-profiles-server.conf b/60-qos-profiles-server.conf new file mode 100644 index 0000000..40abe2f --- /dev/null +++ b/60-qos-profiles-server.conf @@ -0,0 +1,4 @@ +# qos-profiles-server: bare-metal server or VM guest. +# Traffic is mostly locally generated TCP: fq paces per socket, BBR uses the pacing. +-net.core.default_qdisc = fq +-net.ipv4.tcp_congestion_control = bbr diff --git a/60-qos-profiles-vmhost.conf b/60-qos-profiles-vmhost.conf new file mode 100644 index 0000000..9384160 --- /dev/null +++ b/60-qos-profiles-vmhost.conf @@ -0,0 +1,6 @@ +# qos-profiles-vmhost: hypervisor / container host. +# Traffic is mostly forwarded from guests: fq_codel keeps queue delay low for it. +# BBR still serves the host's own TCP and rootful containers. +# Per-guest fairness on the uplink: /etc/qos-profiles/.conf with cake. +-net.core.default_qdisc = fq_codel +-net.ipv4.tcp_congestion_control = bbr diff --git a/README.md b/README.md new file mode 100644 index 0000000..39e307f --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# qos-profiles + +Default qdisc and TCP congestion control per machine role, plus optional +per-interface qdiscs (cake, with ifb for inbound traffic). + +## Roles + +Install exactly one role subpackage: + +| Subpackage | default_qdisc | congestion control | cake NAT | Use for | +| ---------------------- | ------------- | ------------------ | -------- | -------------------------------------------- | +| `qos-profiles-server` | fq | bbr | no | bare-metal servers without guests, VM guests | +| `qos-profiles-vmhost` | fq_codel | bbr | no | hypervisors, container hosts | +| `qos-profiles-router` | fq_codel | cubic | yes | routers | +| `qos-profiles-desktop` | fq_codel | cubic | no | workstations | + +The sysctl file is `/usr/lib/sysctl.d/60-qos-profiles-.conf`; override it +in `/etc/sysctl.d/`. `default_qdisc` only applies to interfaces created after +it is set, so reboot after installing or switching roles. Congestion control +is system-wide (or per route), not per interface. + +## Per-interface qdiscs + +Needs the `qos-profiles` package; the role packages don't pull it in. +Create `/etc/qos-profiles/.conf`, see +`/usr/share/qos-profiles/example.conf` for all variables and +`/usr/share/qos-profiles/examples/` for ready-made setups: + +| Example | Setup | +| --------------------------- | -------------------------------------------------- | +| `defaults.conf` | site-wide `/etc/qos-profiles/defaults.conf` | +| `vmhost-routed-uplink.conf` | VM host, guests routed via the host uplink (EVPN) | +| `vmhost-egress-only.conf` | VM host, outbound fairness only at NIC line rate | +| `vmhost-nat-guests.conf` | VM/container host with masqueraded guests | +| `ovh-public.conf` | dual-NIC dedicated server, public NIC | +| `ovh-vrack-private.conf` | private NIC capped by the provider, outbound only | +| `dc-router-transit.conf` | datacenter router, transit uplink, no NAT | +| `router-ftth.conf` | fibre with Ethernet handoff, NAT | +| `router-vdsl-pppoe.conf` | VDSL2 with PPPoE on `ppp0`, NAT, ACK filtering | +| `router-docsis.conf` | cable, NAT, ACK filtering | +| `backup-nic-raw.conf` | raw `fq` qdisc with a per-flow cap instead of cake | + +Minimal example: + +``` +LINK_RATE=1gbit +``` + +This shapes both directions to 95% of the link rate with cake. Outbound, +traffic is shared per source host and DiffServ classes are applied +(`diffserv4`). Inbound, traffic is shared per destination host, DSCP marks +are ignored (`besteffort`) and cleared (`wash`), since they come from the +internet. `EGRESS_SHAPE_PERCENT` and `INGRESS_SHAPE_PERCENT` set the +percentage per direction. + +Configuration is read in order, later overriding earlier: + +1. `/usr/lib/qos-profiles/role-.conf` — installed role package +2. `/etc/qos-profiles/defaults.conf` — site-wide +3. `/etc/qos-profiles/.conf` — the interface + +It is applied by either: + +- `qos-profiles-networkmanager` (installed automatically with NetworkManager) + when NetworkManager brings the interface up, or +- `systemctl enable --now qos-profiles@` for interfaces NetworkManager + doesn't manage. It runs whenever the device appears. + +Both may be active; they're serialised and idempotent. To apply changes run +`systemctl restart qos-profiles@` or +`/usr/libexec/qos-profiles/qos-apply start `. Stopping restores the +default qdisc and removes the ifb device. + +Inbound shaping replaces any existing `clsact` qdisc on the interface (and its +tc/BPF filters). + +## DiffServ + +`diffserv4` sorts outbound packets by DSCP into bulk (CS1), best effort, video +(AF4x, CS3, ...) and voice (EF, CS6, ...). Higher classes get priority only up +to a share of the rate, so they can't starve the others. Marking backup or +replication traffic CS1 (e.g. nftables `ip dscp set cs1`) makes it yield to +everything else. + +## Topologies + +- **VM host, VMs routed via the host uplink (EVPN anycast gateway):** cake on + the uplink NIC gives each guest an equal share. VXLAN-encapsulated traffic on + the same NIC is seen by its outer header (the VTEP address), so it shares as + a single host. +- **VM host, VMs bridged to a switch, routing elsewhere:** the WAN bottleneck + is at the router; put cake on the router WAN (`qos-profiles-router`). The + host only needs the role package. +- **Host with separate public and private NICs:** configure the public NIC only. +- **Bond or VLAN uplink:** these devices have no queue of their own; put cake + on the physical NIC, or set a rate explicitly. diff --git a/example-backup-nic-raw.conf b/example-backup-nic-raw.conf new file mode 100644 index 0000000..9a7318a --- /dev/null +++ b/example-backup-nic-raw.conf @@ -0,0 +1,9 @@ +# Dedicated backup/storage NIC with a raw qdisc instead of cake. +# Install as: /etc/qos-profiles/.conf +# +# fq with a per-flow cap: a single large transfer can't fill the link, while +# many parallel flows still can. EGRESS replaces everything cake-related for +# that direction; inbound is left alone. + +EGRESS="fq maxrate 500mbit" +INGRESS_RATE=none diff --git a/example-dc-router-transit.conf b/example-dc-router-transit.conf new file mode 100644 index 0000000..e27ee91 --- /dev/null +++ b/example-dc-router-transit.conf @@ -0,0 +1,10 @@ +# Datacenter router, transit uplink. Hosts behind it have public addresses +# (no NAT); VM hosts are on L2 behind this router and need no shaping. +# Install as: /etc/qos-profiles/.conf (role: qos-profiles-router) +# +# Fair share per server in both directions. Cake uses a single queue: fine at +# 1 Gbit/s, check CPU load before using it above 10 Gbit/s. + +LINK_RATE=1gbit +NAT=no +CAKE_ARGS="ethernet" diff --git a/example-defaults.conf b/example-defaults.conf new file mode 100644 index 0000000..13bc993 --- /dev/null +++ b/example-defaults.conf @@ -0,0 +1,11 @@ +# Site-wide defaults, for all configured interfaces. +# Install as: /etc/qos-profiles/defaults.conf +# +# Only overrides what differs from the role package defaults; each +# /etc/qos-profiles/.conf can still override these. + +# Leave more headroom inbound, where shaping is less precise. +INGRESS_SHAPE_PERCENT=90 + +# All our uplinks are plain Ethernet. +CAKE_ARGS="ethernet" diff --git a/example-ovh-public.conf b/example-ovh-public.conf new file mode 100644 index 0000000..2eca355 --- /dev/null +++ b/example-ovh-public.conf @@ -0,0 +1,8 @@ +# Dedicated server with separate public and private NICs (e.g. OVH), public +# side. Configure only the public NIC; the private one keeps the default qdisc. +# Install as: /etc/qos-profiles/.conf +# +# Use the bandwidth the provider guarantees, which can be lower than the NIC +# speed (e.g. 1 Gbit/s NIC with 500 Mbit/s public bandwidth). + +LINK_RATE=500mbit diff --git a/example-ovh-vrack-private.conf b/example-ovh-vrack-private.conf new file mode 100644 index 0000000..77e09f0 --- /dev/null +++ b/example-ovh-vrack-private.conf @@ -0,0 +1,11 @@ +# Private network NIC capped by the provider below the NIC speed (e.g. OVH +# vRack limited to 2 Gbit/s on a 10 Gbit/s NIC), used for replication and +# backups between our own hosts. +# Install as: /etc/qos-profiles/.conf +# +# Shape outbound only, so the queue stays here instead of the provider +# dropping at its policer. Traffic is our own, so DiffServ marks are trusted: +# mark bulk transfers CS1 to let them yield. + +EGRESS_RATE=2gbit +INGRESS_RATE=none diff --git a/example-router-docsis.conf b/example-router-docsis.conf new file mode 100644 index 0000000..1e196ec --- /dev/null +++ b/example-router-docsis.conf @@ -0,0 +1,11 @@ +# Router on cable (DOCSIS), asymmetric, NAT. +# Install as: /etc/qos-profiles/.conf (role: qos-profiles-router) +# +# Cable ISPs usually provision a bit above the advertised rate; start with the +# advertised rates and lower SHAPE_PERCENT if latency still rises under load. + +EGRESS_RATE=50mbit +INGRESS_RATE=500mbit +INGRESS_SHAPE_PERCENT=90 +CAKE_ARGS="docsis" +EGRESS_CAKE_ARGS="ack-filter" diff --git a/example-router-ftth.conf b/example-router-ftth.conf new file mode 100644 index 0000000..e0a79e3 --- /dev/null +++ b/example-router-ftth.conf @@ -0,0 +1,9 @@ +# Office/home router on fibre (FTTH/GPON) with an Ethernet handoff and NAT. +# Install as: /etc/qos-profiles/.conf (role: qos-profiles-router) +# +# Use the subscribed rates. If the ISP tags the WAN with a VLAN, put this on +# the physical NIC and add "ether-vlan". + +EGRESS_RATE=250mbit +INGRESS_RATE=500mbit +CAKE_ARGS="ethernet" diff --git a/example-router-vdsl-pppoe.conf b/example-router-vdsl-pppoe.conf new file mode 100644 index 0000000..c47a0e3 --- /dev/null +++ b/example-router-vdsl-pppoe.conf @@ -0,0 +1,13 @@ +# Router on VDSL2 with PPPoE, asymmetric, NAT. +# Install as: /etc/qos-profiles/ppp0.conf (role: qos-profiles-router) +# +# Configure the PPPoE interface (ppp0), not the Ethernet below it. Use the +# DSL sync rates from the modem, they're higher than the usable rate; the +# pppoe-ptm overhead accounts for the rest. +# ack-filter drops redundant TCP ACKs on the slow upstream. + +EGRESS_RATE=20mbit +INGRESS_RATE=100mbit +INGRESS_SHAPE_PERCENT=90 +CAKE_ARGS="pppoe-ptm" +EGRESS_CAKE_ARGS="ack-filter" diff --git a/example-vmhost-egress-only.conf b/example-vmhost-egress-only.conf new file mode 100644 index 0000000..20947e3 --- /dev/null +++ b/example-vmhost-egress-only.conf @@ -0,0 +1,9 @@ +# VM host where only outbound fairness matters and the NIC line rate is the +# real limit (provider delivers the full NIC speed). +# Install as: /etc/qos-profiles/.conf (role: qos-profiles-vmhost) +# +# Cake runs unlimited: no shaping cost, the NIC's byte queue limits keep the +# queue in cake. Inbound is left alone. + +EGRESS_RATE=unlimited +INGRESS_RATE=none diff --git a/example-vmhost-nat-guests.conf b/example-vmhost-nat-guests.conf new file mode 100644 index 0000000..45339f3 --- /dev/null +++ b/example-vmhost-nat-guests.conf @@ -0,0 +1,9 @@ +# VM or container host with masqueraded guests (libvirt NAT network, rootful +# containers on a NAT bridge). +# Install as: /etc/qos-profiles/.conf (role: qos-profiles-vmhost) +# +# With NAT=yes cake looks up the conntrack entry and shares per internal +# guest address instead of the single public one. + +LINK_RATE=1gbit +NAT=yes diff --git a/example-vmhost-routed-uplink.conf b/example-vmhost-routed-uplink.conf new file mode 100644 index 0000000..9ba0488 --- /dev/null +++ b/example-vmhost-routed-uplink.conf @@ -0,0 +1,9 @@ +# VM host, guests routed through the host uplink (EVPN, anycast gateway on +# the host). Fair share per guest in both directions. +# Install as: /etc/qos-profiles/.conf (role: qos-profiles-vmhost) +# +# Outbound is shared per guest source IP, inbound per guest destination IP. +# VXLAN traffic between hosts on the same NIC shares as a single host (cake +# only sees the outer VTEP addresses). + +LINK_RATE=1gbit diff --git a/example.conf b/example.conf new file mode 100644 index 0000000..5a3b0f6 --- /dev/null +++ b/example.conf @@ -0,0 +1,50 @@ +# qos-profiles interface configuration, all variables. Ready-made setups are +# in /usr/share/qos-profiles/examples/. +# Copy to /etc/qos-profiles/.conf. Applied by the NetworkManager +# dispatcher when the interface comes up, or without NetworkManager by: +# systemctl enable --now qos-profiles@ +# +# Defaults come from the installed role package (/usr/lib/qos-profiles/) and +# /etc/qos-profiles/defaults.conf. Use a physical NIC: bond and VLAN devices +# have no queue of their own. + +# Full link rate in both directions (bit, kbit, mbit, gbit, tbit). +# Cake shapes to SHAPE_PERCENT of it. +#LINK_RATE=1gbit + +# Per-direction link rates, override LINK_RATE. +# EGRESS_RATE: "unlimited" = cake without shaping (default without LINK_RATE, +# fine while the NIC line rate is the bottleneck), "none" = off +# INGRESS_RATE: "none" = no inbound shaping (default without LINK_RATE) +#EGRESS_RATE=100mbit +#INGRESS_RATE=none + +# Percentage of the link rate to shape to, so the queue builds here and not +# in the upstream equipment. Per-direction values override SHAPE_PERCENT. +#SHAPE_PERCENT=95 +#EGRESS_SHAPE_PERCENT=95 +#INGRESS_SHAPE_PERCENT=90 + +# DiffServ classes: besteffort, diffserv3, diffserv4, diffserv8, precedence. +# Inbound marks come from the internet, so they're ignored by default and +# cleared before forwarding (INGRESS_WASH=yes). +#DIFFSERV=diffserv4 +#INGRESS_DIFFSERV=besteffort +#INGRESS_WASH=yes + +# Fairness per host behind NAT on this machine (yes/no). +#NAT=no + +# Extra cake options for both directions: link overhead (ethernet, pppoe-ptm, +# docsis, ...), rtt, or a different fairness mode (flows, triple-isolate, ...). +# Per-direction options are added after CAKE_ARGS. +#CAKE_ARGS="ethernet" +#EGRESS_CAKE_ARGS="ack-filter" +#INGRESS_CAKE_ARGS="" + +# ifb device for inbound shaping, default ifb- (max 15 characters). +#IFB=ifb-wan + +# Raw qdisc specs; when set, replace everything above for that direction. +#EGRESS="fq_codel target 3ms" +#INGRESS="cake besteffort dual-dsthost ingress bandwidth 450mbit" diff --git a/qos-apply b/qos-apply new file mode 100755 index 0000000..23ffe34 --- /dev/null +++ b/qos-apply @@ -0,0 +1,176 @@ +#!/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 diff --git a/qos-profiles-modprobe.conf b/qos-profiles-modprobe.conf new file mode 100644 index 0000000..d2fc349 --- /dev/null +++ b/qos-profiles-modprobe.conf @@ -0,0 +1,2 @@ +# qos-profiles creates ifb devices on demand; don't create unused ifb0/ifb1. +options ifb numifbs=0 diff --git a/qos-profiles.spec b/qos-profiles.spec new file mode 100644 index 0000000..8a7fadc --- /dev/null +++ b/qos-profiles.spec @@ -0,0 +1,170 @@ +Summary: Queueing discipline and TCP congestion control profiles +Name: qos-profiles +Version: 1 +Release: 1%{?dist} +License: GPL-2.0-or-later +URL: http://gitea.infomaas.com/rpms-fedora-ims/%{name} +BuildArch: noarch +Source0: README.md +Source1: qos-apply +Source2: qos-profiles@.service +Source3: qos-profiles-modprobe.conf +Source4: example.conf +Source5: 50-qos-profiles +Source10: 60-qos-profiles-server.conf +Source11: 60-qos-profiles-vmhost.conf +Source12: 60-qos-profiles-router.conf +Source13: 60-qos-profiles-desktop.conf +Source20: role-server.conf +Source21: role-vmhost.conf +Source22: role-router.conf +Source23: role-desktop.conf +Source30: example-backup-nic-raw.conf +Source31: example-dc-router-transit.conf +Source32: example-defaults.conf +Source33: example-ovh-public.conf +Source34: example-ovh-vrack-private.conf +Source35: example-router-docsis.conf +Source36: example-router-ftth.conf +Source37: example-router-vdsl-pppoe.conf +Source38: example-vmhost-egress-only.conf +Source39: example-vmhost-nat-guests.conf +Source40: example-vmhost-routed-uplink.conf + +BuildRequires: systemd-rpm-macros +Requires: gawk +Requires: iproute +Requires: iproute-tc +Requires: util-linux-core +Recommends: (%{name}-networkmanager if NetworkManager) +%{?systemd_requires} + +%description +Per-interface queueing disciplines (cake, with ifb for inbound shaping) +configured in /etc/qos-profiles/.conf and applied by the +qos-profiles@ systemd unit or the NetworkManager dispatcher. + +The role subpackages set the default qdisc, TCP congestion control and +per-interface defaults for servers, VM hosts, routers and desktops. + +%package networkmanager +Summary: NetworkManager dispatcher for qos-profiles +Requires: %{name} = %{version}-%{release} +Requires: NetworkManager + +%description networkmanager +Applies /etc/qos-profiles/.conf when NetworkManager brings the +interface up, without enabling the qos-profiles@ unit. + +%package server +Summary: Queueing profile for servers and VM guests +Provides: %{name}-role +Conflicts: %{name}-role + +%description server +Sets fq as the default qdisc and BBR as TCP congestion control, for machines +that mostly send their own TCP traffic: bare-metal servers and VM guests. + +%package vmhost +Summary: Queueing profile for VM and container hosts +Provides: %{name}-role +Conflicts: %{name}-role + +%description vmhost +Sets fq_codel as the default qdisc for traffic forwarded from guests and BBR as +TCP congestion control for the host's own traffic. Configure cake on the +uplink for fairness per guest. + +%package router +Summary: Queueing profile for routers +Provides: %{name}-role +Conflicts: %{name}-role + +%description router +Sets fq_codel as the default qdisc and CUBIC as TCP congestion control, and +enables cake NAT awareness by default. Configure cake on the WAN with the +link rate. + +%package desktop +Summary: Queueing profile for desktops +Provides: %{name}-role +Conflicts: %{name}-role + +%description desktop +Sets fq_codel as the default qdisc and CUBIC as TCP congestion control. + +%prep +cp -p %{SOURCE0} . + +%install +install -Dpm 755 %{SOURCE1} %{buildroot}%{_libexecdir}/%{name}/qos-apply +install -Dpm 644 %{SOURCE2} %{buildroot}%{_unitdir}/qos-profiles@.service +install -Dpm 644 %{SOURCE3} %{buildroot}%{_modprobedir}/%{name}.conf +install -Dpm 644 %{SOURCE4} %{buildroot}%{_datadir}/%{name}/example.conf +install -Dpm 755 %{SOURCE5} %{buildroot}%{_prefix}/lib/NetworkManager/dispatcher.d/50-qos-profiles +install -dm 755 %{buildroot}%{_sysconfdir}/%{name} +for f in %{SOURCE30} %{SOURCE31} %{SOURCE32} %{SOURCE33} %{SOURCE34} %{SOURCE35} \ + %{SOURCE36} %{SOURCE37} %{SOURCE38} %{SOURCE39} %{SOURCE40}; do + n=$(basename "$f") + install -Dpm 644 "$f" %{buildroot}%{_datadir}/%{name}/examples/"${n#example-}" +done +for f in %{SOURCE10} %{SOURCE11} %{SOURCE12} %{SOURCE13}; do + install -Dpm 644 "$f" %{buildroot}%{_sysctldir}/"$(basename "$f")" +done +for f in %{SOURCE20} %{SOURCE21} %{SOURCE22} %{SOURCE23}; do + install -Dpm 644 "$f" %{buildroot}%{_prefix}/lib/%{name}/"$(basename "$f")" +done + +%post +%systemd_post qos-profiles@.service + +%preun +%systemd_preun qos-profiles@.service + +%post server +%sysctl_apply 60-qos-profiles-server.conf + +%post vmhost +%sysctl_apply 60-qos-profiles-vmhost.conf + +%post router +%sysctl_apply 60-qos-profiles-router.conf + +%post desktop +%sysctl_apply 60-qos-profiles-desktop.conf + +%files +%doc README.md +%{_libexecdir}/%{name}/ +%{_unitdir}/qos-profiles@.service +%{_modprobedir}/%{name}.conf +%{_datadir}/%{name}/ +%dir %{_prefix}/lib/%{name} +%dir %{_sysconfdir}/%{name} + +%files networkmanager +%{_prefix}/lib/NetworkManager/dispatcher.d/50-qos-profiles + +%files server +%{_sysctldir}/60-qos-profiles-server.conf +%dir %{_prefix}/lib/%{name} +%{_prefix}/lib/%{name}/role-server.conf + +%files vmhost +%{_sysctldir}/60-qos-profiles-vmhost.conf +%dir %{_prefix}/lib/%{name} +%{_prefix}/lib/%{name}/role-vmhost.conf + +%files router +%{_sysctldir}/60-qos-profiles-router.conf +%dir %{_prefix}/lib/%{name} +%{_prefix}/lib/%{name}/role-router.conf + +%files desktop +%{_sysctldir}/60-qos-profiles-desktop.conf +%dir %{_prefix}/lib/%{name} +%{_prefix}/lib/%{name}/role-desktop.conf + +%changelog +* Sat Oct 10 2026 Zoran Pericic - 1-1 +- Initial release diff --git a/qos-profiles@.service b/qos-profiles@.service new file mode 100644 index 0000000..e7cdec6 --- /dev/null +++ b/qos-profiles@.service @@ -0,0 +1,15 @@ +[Unit] +Description=Queueing disciplines for %I +Documentation=file:///usr/share/doc/qos-profiles/README.md +BindsTo=sys-subsystem-net-devices-%i.device +After=sys-subsystem-net-devices-%i.device +ConditionPathExists=/etc/qos-profiles/%I.conf + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/libexec/qos-profiles/qos-apply start %I +ExecStop=/usr/libexec/qos-profiles/qos-apply stop %I + +[Install] +WantedBy=sys-subsystem-net-devices-%i.device diff --git a/role-desktop.conf b/role-desktop.conf new file mode 100644 index 0000000..7f08bff --- /dev/null +++ b/role-desktop.conf @@ -0,0 +1,5 @@ +# Interface defaults of qos-profiles-desktop, override in /etc/qos-profiles/. +DIFFSERV=diffserv4 +INGRESS_DIFFSERV=besteffort +INGRESS_WASH=yes +NAT=no diff --git a/role-router.conf b/role-router.conf new file mode 100644 index 0000000..435d52a --- /dev/null +++ b/role-router.conf @@ -0,0 +1,6 @@ +# Interface defaults of qos-profiles-router, override in /etc/qos-profiles/. +# nat: cake sees LAN addresses behind masquerade, for per-host fairness. +DIFFSERV=diffserv4 +INGRESS_DIFFSERV=besteffort +INGRESS_WASH=yes +NAT=yes diff --git a/role-server.conf b/role-server.conf new file mode 100644 index 0000000..8cab7a7 --- /dev/null +++ b/role-server.conf @@ -0,0 +1,5 @@ +# Interface defaults of qos-profiles-server, override in /etc/qos-profiles/. +DIFFSERV=diffserv4 +INGRESS_DIFFSERV=besteffort +INGRESS_WASH=yes +NAT=no diff --git a/role-vmhost.conf b/role-vmhost.conf new file mode 100644 index 0000000..dc29c00 --- /dev/null +++ b/role-vmhost.conf @@ -0,0 +1,5 @@ +# Interface defaults of qos-profiles-vmhost, override in /etc/qos-profiles/. +DIFFSERV=diffserv4 +INGRESS_DIFFSERV=besteffort +INGRESS_WASH=yes +NAT=no