924 lines
29 KiB
Bash
924 lines
29 KiB
Bash
################################################################################
|
|
# (sqm) functions.sh
|
|
#
|
|
# These are all helper functions for various parts of SQM scripts. If you want
|
|
# to play around with your own shaper-qdisc-filter configuration look here for
|
|
# ready made tools, or examples start of on your own.
|
|
#
|
|
# Please note the SQM logger function is broken down into levels of logging.
|
|
# Use only levels appropriate to touch points in your script and realize the
|
|
# potential to overflow SYSLOG.
|
|
#
|
|
################################################################################
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License version 2 as
|
|
# published by the Free Software Foundation.
|
|
#
|
|
# Copyright (C) 2012-2019
|
|
# Michael D. Taht, Toke Høiland-Jørgensen, Sebastian Moeller
|
|
# Eric Luehrsen
|
|
#
|
|
################################################################################
|
|
|
|
# Logging verbosity
|
|
VERBOSITY_SILENT=0
|
|
VERBOSITY_ERROR=1
|
|
VERBOSITY_WARNING=2
|
|
VERBOSITY_INFO=5
|
|
VERBOSITY_DEBUG=8
|
|
VERBOSITY_TRACE=10
|
|
|
|
sqm_logger() {
|
|
local level_min
|
|
local level_max
|
|
local debug
|
|
|
|
case $1 in
|
|
''|*[!0-9]*) LEVEL=$VERBOSITY_INFO ;; # empty or non-numbers
|
|
*) LEVEL=$1; shift ;;
|
|
esac
|
|
|
|
level_min=${SQM_VERBOSITY_MIN:-$VERBOSITY_SILENT}
|
|
level_max=${SQM_VERBOSITY_MAX:-$VERBOSITY_INFO}
|
|
debug=${SQM_DEBUG:-0}
|
|
|
|
if [ "$level_max" -ge "$LEVEL" ] && [ "$level_min" -le "$LEVEL" ] ; then
|
|
if [ "$SQM_SYSLOG" -eq "1" ]; then
|
|
logger -t SQM -s "$*"
|
|
else
|
|
echo "$@" >&2
|
|
fi
|
|
fi
|
|
|
|
# this writes into SQM_START_LOG or SQM_STOP_LOG, log files are trucated in
|
|
# start-sqm/stop-sqm respectively and should only take little space
|
|
if [ "$debug" -eq "1" ]; then
|
|
echo "$@" >> "${SQM_DEBUG_LOG}"
|
|
fi
|
|
}
|
|
|
|
sqm_error() { sqm_logger $VERBOSITY_ERROR ERROR: "$@"; }
|
|
sqm_warn() { sqm_logger $VERBOSITY_WARNING WARNING: "$@"; }
|
|
sqm_log() { sqm_logger $VERBOSITY_INFO "$@"; }
|
|
sqm_debug() { sqm_logger $VERBOSITY_DEBUG "$@"; }
|
|
sqm_trace() { sqm_logger $VERBOSITY_TRACE "$@"; }
|
|
|
|
|
|
# Inspired from https://stackoverflow.com/questions/85880/determine-if-a-function-exists-in-bash
|
|
#fn_exists() { LC_ALL=C type $1 | grep -q 'is a function'; }
|
|
fn_exists() {
|
|
local FN_CANDIDATE
|
|
local CUR_LC_ALL
|
|
local TYPE_OUTPUT
|
|
local RET
|
|
FN_CANDIDATE=$1
|
|
# check that a candidate nme was given
|
|
if [ -z "${FN_CANDIDATE}" ]; then
|
|
sqm_error "fn_exists: no function name specified as first argument."
|
|
return 1
|
|
fi
|
|
sqm_debug "fn_exists: function candidate name: ${FN_CANDIDATE}"
|
|
|
|
# extract the textual type description
|
|
TYPE_OUTPUT=$( LC_ALL=C type $1 2>&1 )
|
|
sqm_debug "fn_exists: TYPE_OUTPUT: $TYPE_OUTPUT"
|
|
|
|
# OpenWrt (2019) returns 'is a function'
|
|
# Debian Buster/raspbian returns 'is a shell function'
|
|
# let's just hope no Linux system reurn 'is a shell builtin function'
|
|
echo ${TYPE_OUTPUT} | grep -q 'is .*function'
|
|
RET=$?
|
|
|
|
sqm_debug "fn_exists: return value: ${RET}"
|
|
return ${RET}
|
|
}
|
|
|
|
|
|
# Transaction logging for ipt rules to allow for gracefull final teardown
|
|
ipt_log_restart() {
|
|
[ -f "$IPT_TRANS_LOG" ] && rm -f "$IPT_TRANS_LOG"
|
|
}
|
|
|
|
|
|
# Function to negate iptables commands. Turns addition and insertion into
|
|
# deletion, and creation of new chains into deletion
|
|
# Its output has quotes around all parameters so we can preserve arguments
|
|
# containing whitespace across log file write & re-read
|
|
ipt_negate()
|
|
{
|
|
for var in "$@"; do
|
|
case "$var" in
|
|
"-A"|"-I") echo -n '"-D" ' ;;
|
|
"-N") echo -n '"-X" ' ;;
|
|
*) echo -n "\"$var\" " ;;
|
|
esac
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
ipt_log()
|
|
{
|
|
echo "$@" >> $IPT_TRANS_LOG
|
|
}
|
|
|
|
# Split a string containing an iptables command line parameter invocation, then
|
|
# run it through ipt(). This is used to turn lines read from the log file, or
|
|
# output from ipt_negate() back into proper parameters contained in $@
|
|
ipt_run_split()
|
|
{
|
|
eval "set -- $1"
|
|
ipt "$@"
|
|
}
|
|
|
|
# Read the transaction log in reverse and execute using ipt to undo changes.
|
|
# Since we logged only ipt '-D' commands, ipt won't add them again to the
|
|
# transaction log, but will include them in the syslog/debug log.
|
|
ipt_log_rewind() {
|
|
[ -f "$IPT_TRANS_LOG" ] || return 0
|
|
sed -n '1!G;h;$p' "$IPT_TRANS_LOG" |
|
|
while read line; do
|
|
[ -n "$line" ] || continue
|
|
ipt_run_split "$line"
|
|
done
|
|
}
|
|
|
|
ipt() {
|
|
local neg
|
|
|
|
for var in "$@"; do
|
|
case "$var" in
|
|
"-A"|"-I"|"-N")
|
|
# If the rule is an addition rule, we first run its negation,
|
|
# then log that negation to be used by ipt_log_rewind() on
|
|
# shutdown
|
|
neg="$(ipt_negate "$@")"
|
|
ipt_run_split "$neg"
|
|
ipt_log "$neg"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
SILENT=1 ${IPTABLES} $IPTABLES_ARGS "$@"
|
|
SILENT=1 ${IP6TABLES} $IPTABLES_ARGS "$@"
|
|
}
|
|
|
|
|
|
# wrapper to call iptables to allow debug logging
|
|
iptables_wrapper(){
|
|
cmd_wrapper iptables ${IPTABLES_BINARY} "$@"
|
|
}
|
|
|
|
# wrapper to call ip6tables to allow debug logging
|
|
ip6tables_wrapper(){
|
|
cmd_wrapper ip6tables ${IP6TABLES_BINARY} "$@"
|
|
}
|
|
|
|
# wrapper to call tc to allow debug logging
|
|
tc_wrapper(){
|
|
cmd_wrapper tc ${TC_BINARY} "$@"
|
|
}
|
|
|
|
# wrapper to call ip to allow debug logging
|
|
ip_wrapper(){
|
|
cmd_wrapper ip ${IP_BINARY} "$@"
|
|
}
|
|
|
|
# the actual command execution wrapper
|
|
cmd_wrapper(){
|
|
# $1: the symbolic name of the command for informative output
|
|
# $2: the name of the binary to call (potentially including the full path)
|
|
# $3-$end: the actual arguments for $2
|
|
local CALLERID
|
|
local CMD_BINARY
|
|
local LAST_ERROR
|
|
local RET
|
|
local ERRLOG
|
|
|
|
CALLERID=$1 ; shift 1 # extract and remove the id string
|
|
CMD_BINARY=$1 ; shift 1 # extract and remove the binary
|
|
|
|
# Handle silencing of errors from callers
|
|
ERRLOG="sqm_error"
|
|
if [ "$SILENT" -eq "1" ]; then
|
|
ERRLOG="sqm_debug"
|
|
sqm_debug "cmd_wrapper: ${CALLERID}: invocation silenced by request, FAILURE either expected or acceptable."
|
|
# The busybox shell doesn't understand the concept of an inline variable
|
|
# only applying to a single command, so we need to reset SILENT
|
|
# afterwards. Ugly, but it works...
|
|
SILENT=0
|
|
fi
|
|
|
|
sqm_trace "cmd_wrapper: COMMAND: ${CMD_BINARY} $@"
|
|
LAST_ERROR=$( ${CMD_BINARY} "$@" 2>&1 )
|
|
RET=$?
|
|
|
|
if [ "$RET" -eq "0" ] ; then
|
|
sqm_debug "cmd_wrapper: ${CALLERID}: SUCCESS: ${CMD_BINARY} $@"
|
|
else
|
|
# this went south, try to capture & report more detail
|
|
$ERRLOG "cmd_wrapper: ${CALLERID}: FAILURE (${RET}): ${CMD_BINARY} $@"
|
|
$ERRLOG "cmd_wrapper: ${CALLERID}: LAST ERROR: ${LAST_ERROR}"
|
|
fi
|
|
|
|
return $RET
|
|
}
|
|
|
|
|
|
do_modules() {
|
|
for m in $ALL_MODULES; do
|
|
[ -d /sys/module/${m} ] || ${INSMOD} $m 2>>${OUTPUT_TARGET}
|
|
done
|
|
}
|
|
|
|
# Write a state file to the filename given as $1. This version will extract all
|
|
# variable names defined in defaults.sh and since defaults.sh should contain all
|
|
# used variables this should be the complete set.
|
|
write_state_file() {
|
|
local filename
|
|
local awkscript
|
|
awkscript='match($0, /[A-Z0-9_]+=/) {print substr($0, RSTART, RLENGTH-1)}'
|
|
filename=$1
|
|
shift
|
|
awk "$awkscript" ${SQM_LIB_DIR}/defaults.sh | sort -u | while read var; do
|
|
val=$(eval echo '$'$var)
|
|
echo "$var=\"$val\""
|
|
done > $filename
|
|
}
|
|
|
|
check_state_dir() {
|
|
local PERM
|
|
local OWNER
|
|
|
|
if [ -z "${SQM_STATE_DIR}" ]; then
|
|
SQM_DEBUG=0 sqm_error '$SQM_STATE_DIR is unset - check your config!'
|
|
exit 1
|
|
fi
|
|
[ -d "${SQM_STATE_DIR}" ] || ( umask 077; mkdir -p "$SQM_STATE_DIR" ) || exit 1
|
|
|
|
if [ ! -w "${SQM_STATE_DIR}" ] || [ ! -x "${SQM_STATE_DIR}" ]; then
|
|
SQM_DEBUG=0 sqm_error "Cannot write to state dir '$SQM_STATE_DIR'"
|
|
exit 1
|
|
fi
|
|
|
|
# OpenWrt doesn't have stat; for now just skip the remaining tests if it's
|
|
# not available
|
|
command -v stat >/dev/null 2>&1 || return 0
|
|
|
|
PERM="0$(stat -L -c '%a' "${SQM_STATE_DIR}")"
|
|
if [ "$((PERM & 0002))" -ne 0 ]; then
|
|
SQM_DEBUG=0 sqm_error "State dir '$SQM_STATE_DIR' is world writable; this is unsafe, please fix"
|
|
exit 1
|
|
fi
|
|
OWNER="$(stat -L -c '%u' "${SQM_STATE_DIR}")"
|
|
if [ "$OWNER" -ne "$(id -u)" ]; then
|
|
SQM_DEBUG=0 sqm_error "State dir '$SQM_STATE_DIR' is owned by a different user; this is unsafe, please fix"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
|
|
# find the ifb device associated with a specific interface, return nothing of no
|
|
# ifb is associated with IF
|
|
get_ifb_associated_with_if() {
|
|
local CUR_IF
|
|
local CUR_IFB
|
|
local TMP
|
|
CUR_IF=$1
|
|
# Stray ' in the comment is a fix for broken editor syntax highlighting
|
|
CUR_IFB=$( $TC_BINARY -p filter show parent ffff: dev ${CUR_IF} | grep -o -E ifb'[^)\ ]+' ) # '
|
|
sqm_debug "ifb associated with interface ${CUR_IF}: ${CUR_IFB}"
|
|
|
|
# we could not detect an associated IFB for CUR_IF
|
|
if [ -z "${CUR_IFB}" ]; then
|
|
TMP=$( $TC_BINARY -p filter show parent ffff: dev ${CUR_IF} )
|
|
if [ ! -z "${TMP}" ]; then
|
|
# oops, there is output but we failed to properly parse it? Ask for a user report
|
|
sqm_error "#---- CUT HERE ----#"
|
|
sqm_error "get_ifb_associated_with_if failed to extrect the ifb name from:"
|
|
sqm_error $( $TC_BINARY -p filter show parent ffff: dev ${CUR_IF} )
|
|
sqm_error "Please report this as an issue at https://github.com/tohojo/sqm-scripts"
|
|
sqm_error "Please copy and paste everything below the cut-here line into your issue report, thanks."
|
|
else
|
|
sqm_debug "Currently no ifb is associated with ${CUR_IF}, this is normal during starting of the sqm system."
|
|
fi
|
|
fi
|
|
echo ${CUR_IFB}
|
|
}
|
|
|
|
ifb_name() {
|
|
local CUR_IF
|
|
local MAX_IF_NAME_LENGTH
|
|
local IFB_PREFIX
|
|
local NEW_IFB
|
|
CUR_IF=$1
|
|
MAX_IF_NAME_LENGTH=15
|
|
IFB_PREFIX="ifb4"
|
|
NEW_IFB=$( echo -n "${IFB_PREFIX}${CUR_IF}" | head -c $MAX_IF_NAME_LENGTH )
|
|
|
|
echo ${NEW_IFB}
|
|
}
|
|
|
|
# if required
|
|
create_new_ifb_for_if() {
|
|
local NEW_IFB
|
|
NEW_IFB=$(ifb_name $1)
|
|
create_ifb ${NEW_IFB}
|
|
RET=$?
|
|
echo $NEW_IFB
|
|
return $RET
|
|
}
|
|
|
|
|
|
# TODO: report failures
|
|
create_ifb() {
|
|
local CUR_IFB
|
|
CUR_IFB=${1}
|
|
$IP link add name ${CUR_IFB} type ifb
|
|
}
|
|
|
|
delete_ifb() {
|
|
local CUR_IFB
|
|
CUR_IFB=${1}
|
|
$IP link set dev ${CUR_IFB} down
|
|
$IP link delete ${CUR_IFB} type ifb
|
|
}
|
|
|
|
|
|
# the best match is either the IFB already associated with the current interface
|
|
# or a new named IFB
|
|
get_ifb_for_if() {
|
|
local CUR_IF
|
|
local CUR_IFB
|
|
CUR_IF=$1
|
|
# if an ifb is already associated return that
|
|
CUR_IFB=$( get_ifb_associated_with_if ${CUR_IF} )
|
|
[ -z "$CUR_IFB" ] && CUR_IFB=$( create_new_ifb_for_if ${CUR_IF} )
|
|
[ -z "$CUR_IFB" ] && sqm_warn "Could not find existing IFB for ${CUR_IF}, nor create a new IFB instead..."
|
|
echo ${CUR_IFB}
|
|
}
|
|
|
|
|
|
# Verify that a qdisc works, and optionally that it is part of a set of
|
|
# supported qdiscs. If passed a $2, this function will first check if $1 is in
|
|
# that (space-separated) list and return an error if it's not.
|
|
#
|
|
# note the ingress qdisc is different in that it requires tc qdisc replace dev
|
|
# tmp_ifb ingress instead of "root ingress"
|
|
verify_qdisc() {
|
|
local qdisc
|
|
local supported
|
|
local ifb
|
|
local root_string
|
|
local args
|
|
local IFB_MTU
|
|
local found
|
|
local randnum
|
|
qdisc=$1
|
|
supported="$2"
|
|
randnum=$(tr -cd 0-9a-f < /dev/urandom 2>/dev/null | head -c 5)
|
|
ifb=SQM_IFB_$randnum
|
|
root_string="root" # this works for most qdiscs
|
|
args=""
|
|
IFB_MTU=1514
|
|
|
|
if [ -n "$supported" ]; then
|
|
found=0
|
|
for q in $supported; do
|
|
[ "$qdisc" = "$q" ] && found=1
|
|
done
|
|
[ "$found" -eq "1" ] || return 1
|
|
fi
|
|
create_ifb $ifb || return 1
|
|
|
|
|
|
case $qdisc in
|
|
#ingress is special
|
|
ingress) root_string="" ;;
|
|
#cannot instantiate tbf without args
|
|
tbf)
|
|
IFB_MTU=$( get_mtu $ifb )
|
|
IFB_MTU=$(( ${IFB_MTU} + 14 )) # TBF's warning is confused, it says MTU but it checks MTU + 14
|
|
args="limit 1 burst ${IFB_MTU} rate 1kbps"
|
|
;;
|
|
esac
|
|
|
|
$TC qdisc replace dev $ifb $root_string $qdisc $args
|
|
res=$?
|
|
if [ "$res" = "0" ] ; then
|
|
sqm_debug "QDISC $qdisc is useable."
|
|
else
|
|
sqm_error "QDISC $qdisc is NOT useable."
|
|
fi
|
|
delete_ifb $ifb
|
|
return $res
|
|
}
|
|
|
|
|
|
get_htb_adsll_string() {
|
|
ADSLL=""
|
|
if [ "$LLAM" = "htb_private" -a "$LINKLAYER" != "none" ]; then
|
|
# HTB defaults to MTU 1600 and an implicit fixed TSIZE of 256, but HTB
|
|
# as of around 3.10.0 does not actually use a table in the kernel
|
|
ADSLL="mpu ${STAB_MPU} linklayer ${LINKLAYER} overhead ${OVERHEAD} mtu ${STAB_MTU}"
|
|
sqm_debug "ADSLL: ${ADSLL}"
|
|
fi
|
|
echo ${ADSLL}
|
|
}
|
|
|
|
get_stab_string() {
|
|
local STABSTRING
|
|
local TMP_LLAM
|
|
STABSTRING=""
|
|
TMP_LLAM=${LLAM}
|
|
if [ "${LLAM}" = "default" -a "$QDISC" != "cake" ]; then
|
|
sqm_debug "LLA: default link layer adjustment method for !cake is tc_stab"
|
|
TMP_LLAM="tc_stab"
|
|
fi
|
|
|
|
if [ "${TMP_LLAM}" = "tc_stab" -a "$LINKLAYER" != "none" ]; then
|
|
STABSTRING="stab mtu ${STAB_MTU} tsize ${STAB_TSIZE} mpu ${STAB_MPU} overhead ${OVERHEAD} linklayer ${LINKLAYER}"
|
|
sqm_debug "STAB: ${STABSTRING}"
|
|
fi
|
|
echo ${STABSTRING}
|
|
}
|
|
|
|
# cake knows how to handle ATM and per packet overhead, so expose and use this...
|
|
get_cake_lla_string() {
|
|
local STABSTRING
|
|
local TMP_LLAM
|
|
STABSTRING=""
|
|
TMP_LLAM=${LLAM}
|
|
if [ "${LLAM}" = "default" -a "$QDISC" = "cake" ]; then
|
|
sqm_debug "LLA: default link layer adjustment method for cake is cake"
|
|
TMP_LLAM="cake"
|
|
fi
|
|
|
|
if [ "${TMP_LLAM}" = "cake" -a "${LINKLAYER}" != "none" ]; then
|
|
if [ "${LINKLAYER}" = "atm" ]; then
|
|
STABSTRING="atm"
|
|
fi
|
|
|
|
STABSTRING="${STABSTRING} overhead ${OVERHEAD} mpu ${STAB_MPU}"
|
|
|
|
sqm_debug "cake link layer adjustments: ${STABSTRING}"
|
|
fi
|
|
echo ${STABSTRING}
|
|
}
|
|
|
|
|
|
# centralize the implementation for the default sqm_start sqeuence
|
|
# the individual sqm_start function only need to do the individually
|
|
# necessary checking.
|
|
# This expects the calling script to supply both an egress() and ingress() function
|
|
# and will warn if they are missing
|
|
sqm_start_default() {
|
|
#sqm_error "sqm_start_default"
|
|
[ -n "$IFACE" ] || return 1
|
|
|
|
# reset the iptables trace log
|
|
ipt_log_restart
|
|
|
|
if fn_exists sqm_prepare_script ; then
|
|
sqm_debug "sqm_start_default: starting sqm_prepare_script"
|
|
sqm_prepare_script
|
|
else
|
|
sqm_debug "sqm_start_default: no sqm_prepare_script function found, proceeding without."
|
|
fi
|
|
|
|
do_modules
|
|
verify_qdisc $QDISC || return 1
|
|
sqm_debug "sqm_start_default: Starting ${SCRIPT}"
|
|
|
|
[ -z "$DEV" ] && DEV=$( get_ifb_for_if ${IFACE} )
|
|
|
|
if [ "${UPLINK}" -ne 0 ];
|
|
then
|
|
CUR_DIRECTION="egress"
|
|
fn_exists egress && egress || sqm_warn "sqm_start_default: ${SCRIPT} lacks an egress() function"
|
|
#egress
|
|
sqm_debug "sqm_start_default: egress shaping activated"
|
|
else
|
|
sqm_debug "sqm_start_default: egress shaping deactivated"
|
|
SILENT=1 $TC qdisc del dev ${IFACE} root
|
|
fi
|
|
if [ "${DOWNLINK}" -ne 0 ];
|
|
then
|
|
CUR_DIRECTION="ingress"
|
|
verify_qdisc ingress "ingress" || return 1
|
|
fn_exists ingress && ingress || sqm_warn "sqm_start_default: ${SCRIPT} lacks an ingress() function"
|
|
#ingress
|
|
sqm_debug "sqm_start_default: ingress shaping activated"
|
|
else
|
|
sqm_debug "sqm_start_default: ingress shaping deactivated"
|
|
SILENT=1 $TC qdisc del dev ${DEV} root
|
|
SILENT=1 $TC qdisc del dev ${IFACE} ingress
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
|
|
sqm_stop() {
|
|
if [ "${DOWNLINK}" -ne 0 ]; then
|
|
$TC qdisc del dev $IFACE ingress
|
|
$TC qdisc del dev $IFACE root
|
|
[ -n "$CUR_IFB" ] && $TC qdisc del dev $CUR_IFB root
|
|
[ -n "$CUR_IFB" ] && sqm_debug "${0}: ${CUR_IFB} shaper deleted"
|
|
fi
|
|
|
|
# undo accumulated ipt commands during shutdown
|
|
ipt_log_rewind
|
|
# reset the iptables trace log
|
|
ipt_log_restart
|
|
|
|
[ -n "$CUR_IFB" ] && $IP link set dev ${CUR_IFB} down
|
|
[ -n "$CUR_IFB" ] && $IP link delete ${CUR_IFB} type ifb
|
|
[ -n "$CUR_IFB" ] && sqm_debug "${0}: ${CUR_IFB} interface deleted"
|
|
}
|
|
|
|
# Note this has side effects on the prio variable
|
|
# and depends on the interface global too
|
|
fc() {
|
|
$TC filter add dev $interface protocol ip parent $1 prio $prio u32 match ip tos $2 0xfc classid $3
|
|
prio=$(($prio + 1))
|
|
$TC filter add dev $interface protocol ipv6 parent $1 prio $prio u32 match ip6 priority $2 0xfc classid $3
|
|
prio=$(($prio + 1))
|
|
}
|
|
|
|
|
|
# allow better control over HTB's quantum variable
|
|
# this controlls how many bytes htb ties to deque from the current tier before
|
|
# switching to the next, if this is large mixing between pririty tiers will be
|
|
# lumpy, but at a lower CPU cost. In first approximation quantum should not be
|
|
# larger than burst.
|
|
get_htb_quantum() {
|
|
local HTB_MTU
|
|
local BANDWIDTH
|
|
local DURATION_US
|
|
local MIN_QUANTUM
|
|
local QUANTUM
|
|
HTB_MTU=$( get_mtu $1 )
|
|
BANDWIDTH=$2
|
|
DURATION_US=$3
|
|
|
|
sqm_debug "get_htb_quantum: 1: ${1}, 2: ${2}, 3: ${3}"
|
|
|
|
if [ -z "${DURATION_US}" ] ; then
|
|
DURATION_US=${SHAPER_QUANTUM_DUR_US} # the duration of the burst in microseconds
|
|
sqm_warn "get_htb_quantum (by duration): Defaulting to ${DURATION_US} microseconds."
|
|
fi
|
|
|
|
if [ -n "${HTB_MTU}" -a "${DURATION_US}" -gt "0" ] ; then
|
|
QUANTUM=$( get_burst ${HTB_MTU} ${BANDWIDTH} ${DURATION_US} )
|
|
fi
|
|
|
|
if [ -z "$QUANTUM" ]; then
|
|
MIN_QUANTUM=$(( ${MTU} + 48 )) # add 48 bytes to MTU for the ovehead
|
|
MIN_QUANTUM=$(( ${MIN_QUANTUM} + 47 )) # now do ceil(Min_BURST / 48) * 53 in shell integer arithmic
|
|
MIN_QUANTUM=$(( ${MIN_QUANTUM} / 48 ))
|
|
MIN_QUANTUM=$(( ${MIN_QUANTUM} * 53 )) # for MTU 1489 to 1536 this will result in MIN_BURST = 1749 Bytes
|
|
sqm_warn "get_htb_quantum: 0 bytes quantum will not work, defaulting to one ATM/AAL5 expanded MTU packet with overhead: ${MIN_QUANTUM}"
|
|
echo ${MIN_QUANTUM}
|
|
else
|
|
echo ${QUANTUM}
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
|
|
# try to define the burst parameter in the duration required to transmit a burst
|
|
# at the configured bandwidth conceptuallly the matching quantum for this burst
|
|
# should be BURST/number_of_tiers to give each htb tier a chance to dequeue into
|
|
# each burst, but that most likely will end up with a somewhat too small quantum
|
|
# note: to get htb to report the configured burst/cburt one needs to issue the
|
|
# following command (for ifbpppoe-wan):
|
|
# tc -d class show dev ifb4pppoe-wan
|
|
get_burst() {
|
|
local MTU
|
|
local BANDWIDTH
|
|
local SHAPER_BURST_US
|
|
local MIN_BURST
|
|
local BURST
|
|
MTU=$1
|
|
BANDWIDTH=$2 # note bandwidth is always given in kbps
|
|
SHAPER_BURST_US=$3
|
|
|
|
sqm_debug "get_burst: 1: ${1}, 2: ${2}, 3: ${3}"
|
|
|
|
if [ -z "${SHAPER_BURST_US}" ] ; then
|
|
SHAPER_BURST_US=1000 # the duration of the burst in microseconds
|
|
sqm_warn "get_burst (by duration): Defaulting to ${SHAPER_BURST_US} microseconds bursts."
|
|
fi
|
|
|
|
# let's assume ATM/AAL5 to be the worst case encapsulation
|
|
# and 48 Bytes a reasonable worst case per packet overhead
|
|
MIN_BURST=$(( ${MTU} + 48 )) # add 48 bytes to MTU for the ovehead
|
|
MIN_BURST=$(( ${MIN_BURST} + 47 )) # now do ceil(Min_BURST / 48) * 53 in shell integer arithmic
|
|
MIN_BURST=$(( ${MIN_BURST} / 48 ))
|
|
MIN_BURST=$(( ${MIN_BURST} * 53 )) # for MTU 1489 to 1536 this will result in MIN_BURST = 1749 Bytes
|
|
|
|
# htb/tbf expect burst to be specified in bytes, while bandwidth is in kbps
|
|
BURST=$(( ((${SHAPER_BURST_US} * ${BANDWIDTH}) / 8000) ))
|
|
|
|
if [ ${BURST} -lt ${MIN_BURST} ] ; then
|
|
sqm_log "get_burst (by duration): the calculated burst/quantum size of ${BURST} bytes was below the minimum of ${MIN_BURST} bytes."
|
|
BURST=${MIN_BURST}
|
|
fi
|
|
|
|
sqm_debug "get_burst (by duration): BURST [Byte]: ${BURST}, BANDWIDTH [Kbps]: ${BANDWIDTH}, DURATION [us]: ${SHAPER_BURST_US}"
|
|
|
|
echo ${BURST}
|
|
}
|
|
|
|
|
|
# Create optional burst parameters to leap over CPU interupts when the CPU is
|
|
# severly loaded. We need to be conservative though.
|
|
get_htb_burst() {
|
|
local HTB_MTU
|
|
local BANDWIDTH
|
|
local DURATION_US
|
|
local BURST
|
|
HTB_MTU=$( get_mtu $1 )
|
|
BANDWIDTH=$2
|
|
DURATION_US=$3
|
|
|
|
sqm_debug "get_htb_burst: 1: ${1}, 2: ${2}, 3: ${3}"
|
|
|
|
if [ -z "${DURATION_US}" ] ; then
|
|
DURATION_US=${SHAPER_BURST_DUR_US} # the duration of the burst in microseconds
|
|
sqm_warn "get_htb_burst (by duration): Defaulting to ${SHAPER_BURST_DUR_US} microseconds."
|
|
fi
|
|
|
|
if [ -n "${HTB_MTU}" -a "${DURATION_US}" -gt "0" ] ; then
|
|
BURST=$( get_burst ${HTB_MTU} ${BANDWIDTH} ${DURATION_US} )
|
|
fi
|
|
|
|
if [ -z "$BURST" ]; then
|
|
sqm_debug "get_htb_burst: Default Burst, HTB will use MTU plus shipping and handling"
|
|
else
|
|
echo burst $BURST cburst $BURST
|
|
fi
|
|
}
|
|
|
|
# For a default PPPoE link this returns 1492 just as expected but I fear we
|
|
# actually need the wire size of the whole thing not so much the MTU
|
|
get_mtu() {
|
|
CUR_MTU=$(cat /sys/class/net/$1/mtu)
|
|
sqm_debug "IFACE: ${1} MTU: ${CUR_MTU}"
|
|
echo ${CUR_MTU}
|
|
}
|
|
|
|
# Set the autoflow variable to 1 if you want to limit the number of flows
|
|
# otherwise the default of 1024 will be used for all Xfq_codel qdiscs.
|
|
|
|
get_flows() {
|
|
case $QDISC in
|
|
codel|ns2_codel|pie|*fifo|pfifo_fast) ;;
|
|
fq_codel|*fq_codel|sfq) echo flows $( get_flows_count ${1} ) ;;
|
|
esac
|
|
}
|
|
|
|
get_flows_count() {
|
|
if [ "${AUTOFLOW}" -eq "1" ]; then
|
|
FLOWS=8
|
|
[ $1 -gt 999 ] && FLOWS=16
|
|
[ $1 -gt 2999 ] && FLOWS=32
|
|
[ $1 -gt 7999 ] && FLOWS=48
|
|
[ $1 -gt 9999 ] && FLOWS=64
|
|
[ $1 -gt 19999 ] && FLOWS=128
|
|
[ $1 -gt 39999 ] && FLOWS=256
|
|
[ $1 -gt 69999 ] && FLOWS=512
|
|
[ $1 -gt 99999 ] && FLOWS=1024
|
|
case $QDISC in
|
|
codel|ns2_codel|pie|*fifo|pfifo_fast) ;;
|
|
fq_codel|*fq_codel|sfq) echo $FLOWS ;;
|
|
esac
|
|
else
|
|
case $QDISC in
|
|
codel|ns2_codel|pie|*fifo|pfifo_fast) ;;
|
|
fq_codel|*fq_codel|sfq) echo 1024 ;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
# set the target parameter, also try to only take well formed inputs
|
|
# Note, the link bandwidth in the current direction (ingress or egress)
|
|
# is required to adjust the target for slow links
|
|
get_target() {
|
|
local CUR_TARGET
|
|
local CUR_LINK_KBPS
|
|
CUR_TARGET=${1}
|
|
CUR_LINK_KBPS=${2}
|
|
[ ! -z "$CUR_TARGET" ] && sqm_debug "cur_target: ${CUR_TARGET} cur_bandwidth: ${CUR_LINK_KBPS}"
|
|
CUR_TARGET_STRING=
|
|
# either e.g. 100ms or auto
|
|
CUR_TARGET_VALUE=$( echo ${CUR_TARGET} | grep -o -e \^'[[:digit:]]\+' )
|
|
CUR_TARGET_UNIT=$( echo ${CUR_TARGET} | grep -o -e '[[:alpha:]]\+'\$ )
|
|
|
|
AUTO_TARGET=
|
|
UNIT_VALID=
|
|
|
|
case $QDISC in
|
|
*codel|*pie)
|
|
if [ ! -z "${CUR_TARGET_VALUE}" -a ! -z "${CUR_TARGET_UNIT}" ];
|
|
then
|
|
case ${CUR_TARGET_UNIT} in
|
|
# permissible units taken from: tc_util.c get_time()
|
|
s|sec|secs|ms|msec|msecs|us|usec|usecs)
|
|
CUR_TARGET_STRING="target ${CUR_TARGET_VALUE}${CUR_TARGET_UNIT}"
|
|
UNIT_VALID="1"
|
|
;;
|
|
esac
|
|
fi
|
|
# empty field in GUI or undefined GUI variable now defaults to auto
|
|
if [ -z "${CUR_TARGET_VALUE}" -a -z "${CUR_TARGET_UNIT}" ];
|
|
then
|
|
if [ ! -z "${CUR_LINK_KBPS}" ]; then
|
|
TMP_TARGET_US=$( adapt_target_to_slow_link $CUR_LINK_KBPS )
|
|
TMP_INTERVAL_STRING=$( adapt_interval_to_slow_link $TMP_TARGET_US )
|
|
CUR_TARGET_STRING="target ${TMP_TARGET_US}us ${TMP_INTERVAL_STRING}"
|
|
AUTO_TARGET="1"
|
|
sqm_debug "get_target defaulting to auto."
|
|
else
|
|
sqm_warn "required link bandwidth in kbps not passed to get_target()."
|
|
fi
|
|
fi
|
|
# but still allow explicit use of the keyword auto for backward compatibility
|
|
case ${CUR_TARGET_UNIT} in
|
|
auto|Auto|AUTO)
|
|
if [ ! -z "${CUR_LINK_KBPS}" ]; then
|
|
TMP_TARGET_US=$( adapt_target_to_slow_link $CUR_LINK_KBPS )
|
|
TMP_INTERVAL_STRING=$( adapt_interval_to_slow_link $TMP_TARGET_US )
|
|
CUR_TARGET_STRING="target ${TMP_TARGET_US}us ${TMP_INTERVAL_STRING}"
|
|
AUTO_TARGET="1"
|
|
else
|
|
sqm_warn "required link bandwidth in kbps not passed to get_target()."
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
case ${CUR_TARGET_UNIT} in
|
|
default|Default|DEFAULT)
|
|
if [ ! -z "${CUR_LINK_KBPS}" ]; then
|
|
CUR_TARGET_STRING="" # return nothing so the default target is not over-ridden...
|
|
AUTO_TARGET="1"
|
|
sqm_debug "get_target using qdisc default, no explicit target string passed."
|
|
else
|
|
sqm_warn "required link bandwidth in kbps not passed to get_target()."
|
|
fi
|
|
;;
|
|
esac
|
|
if [ ! -z "${CUR_TARGET}" ]; then
|
|
if [ -z "${CUR_TARGET_VALUE}" -o -z "${UNIT_VALID}" ]; then
|
|
[ -z "$AUTO_TARGET" ] && sqm_warn "${CUR_TARGET} is not a well formed tc target specifier; e.g.: 5ms (or s, us), or one of the strings auto or default."
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
echo $CUR_TARGET_STRING
|
|
}
|
|
|
|
# for low bandwidth links fq_codels default target of 5ms does not work too well
|
|
# so increase target for slow links (note below roughly 2500kbps a single packet
|
|
# will take more than 5 ms to be tansfered over the wire)
|
|
adapt_target_to_slow_link() {
|
|
LINK_BW=$1
|
|
# for ATM the worst case expansion including overhead seems to be 33 clls of
|
|
# 53 bytes each
|
|
MAX_DELAY=$(( 1000 * 1000 * 33 * 53 * 8 / 1000 )) # Max delay in us at 1kbps
|
|
TARGET=$(( ${MAX_DELAY} / ${LINK_BW} )) # note this truncates the decimals
|
|
|
|
# do not change anything for fast links
|
|
[ "$TARGET" -lt 5000 ] && TARGET=5000
|
|
case ${QDISC} in
|
|
*codel|pie)
|
|
echo "${TARGET}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# codel looks at a whole interval to figure out wether observed latency stayed
|
|
# below target if target >= interval that will not work well, so increase
|
|
# interval by the same amonut that target got increased
|
|
adapt_interval_to_slow_link() {
|
|
TARGET=$1
|
|
case ${QDISC} in
|
|
*codel)
|
|
# Note this is not following codel theory to well as target should
|
|
# be 5-10% of interval and the simple addition does not conserve
|
|
# that relationship
|
|
INTERVAL=$(( (100 - 5) * 1000 + ${TARGET} ))
|
|
echo "interval ${INTERVAL}us"
|
|
;;
|
|
pie)
|
|
## not sure if pie needs this, probably not
|
|
#TUPDATE=$(( (30 - 20) * 1000 + ${TARGET} ))
|
|
#echo "tupdate ${TUPDATE}us"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
# set quantum parameter if available for this qdisc
|
|
get_quantum() {
|
|
case $QDISC in
|
|
*fq_codel|fq_pie|drr) echo quantum $1 ;;
|
|
*) ;;
|
|
esac
|
|
}
|
|
|
|
# only show limits to qdiscs that can handle them...
|
|
# Note that $LIMIT contains the default limit
|
|
get_limit() {
|
|
CURLIMIT=$1
|
|
case $QDISC in
|
|
*codel|*pie|pfifo_fast|sfq|pfifo) [ -z ${CURLIMIT} ] && CURLIMIT=${LIMIT} # global default limit
|
|
;;
|
|
bfifo) [ -z "$CURLIMIT" ] && [ ! -z "$LIMIT" ] && CURLIMIT=$(( ${LIMIT} * $( cat /sys/class/net/${IFACE}/mtu ) )) # bfifo defaults to txquelength * MTU,
|
|
;;
|
|
*) sqm_warn "qdisc ${QDISC} does not support a limit"
|
|
;;
|
|
esac
|
|
sqm_debug "get_limit: $1 CURLIMIT: ${CURLIMIT}"
|
|
|
|
if [ ! -z "$CURLIMIT" ]; then
|
|
echo "limit ${CURLIMIT}"
|
|
fi
|
|
}
|
|
|
|
get_ecn() {
|
|
CURECN=$1
|
|
case ${CURECN} in
|
|
ECN)
|
|
case $QDISC in
|
|
*codel|*pie|*red)
|
|
CURECN=ecn
|
|
;;
|
|
*)
|
|
CURECN=""
|
|
;;
|
|
esac
|
|
;;
|
|
NOECN)
|
|
case $QDISC in
|
|
*codel|*pie|*red)
|
|
CURECN=noecn
|
|
;;
|
|
*)
|
|
CURECN=""
|
|
;;
|
|
esac
|
|
;;
|
|
*)
|
|
sqm_warn "ecn value $1 not handled"
|
|
;;
|
|
esac
|
|
sqm_debug "get_ECN: $1 CURECN: ${CURECN} IECN: ${IECN} EECN: ${EECN}"
|
|
echo ${CURECN}
|
|
|
|
}
|
|
|
|
# This could be a complete diffserv implementation
|
|
|
|
diffserv() {
|
|
|
|
interface=$1
|
|
prio=1
|
|
|
|
# Catchall
|
|
|
|
$TC filter add dev $interface parent 1:0 protocol all prio 999 u32 \
|
|
match ip protocol 0 0x00 flowid 1:12
|
|
|
|
# Find the most common matches fast
|
|
|
|
fc 1:0 0x00 1:12 # BE
|
|
fc 1:0 0x20 1:13 # CS1
|
|
fc 1:0 0x10 1:11 # IMM
|
|
fc 1:0 0xb8 1:11 # EF
|
|
fc 1:0 0xc0 1:11 # CS3
|
|
fc 1:0 0xe0 1:11 # CS6
|
|
fc 1:0 0x90 1:11 # AF42 (mosh)
|
|
|
|
# Arp traffic
|
|
$TC filter add dev $interface protocol arp parent 1:0 prio $prio handle 500 fw flowid 1:11
|
|
|
|
prio=$(($prio + 1))
|
|
}
|
|
|
|
eth_setup() {
|
|
ethtool -K $IFACE gso off
|
|
ethtool -K $IFACE tso off
|
|
ethtool -K $IFACE ufo off
|
|
ethtool -K $IFACE gro off
|
|
|
|
if [ -e /sys/class/net/$IFACE/queues/tx-0/byte_queue_limits ]; then
|
|
for i in /sys/class/net/$IFACE/queues/tx-*/byte_queue_limits
|
|
do
|
|
echo $(( 4 * $( get_mtu ${IFACE} ) )) > $i/limit_max
|
|
done
|
|
fi
|
|
}
|