#!/bin/sh 

: '120819 PeterG <pg_scr {at} scr.for.sabi.co.UK>'

ME="`basename "$0"`"

SSRC='/etc/sabishaperc'
SSCF='/etc/sabishapecf'

: '
    Partially inspired by:

    <http://WWW.TLDP.org/HOWTO/ADSL-Bandwidth-Management-HOWTO/>
    <http://WWW.LARTC.org/lartc.html#AEN1072>
    <http://WWW.OpalSoft.net/qos/DS-28.htm>
    <http://WWW.LARTC.org/wondershaper/>
    <http://www-online.kek.jp/~yasu/ATLAS/QoS/iproute2-notes.html>
'

: '
  We shape an interface, and on it we have to types of traffic: internet
  traffic, which we can limit to two separate ceilings of outgoing and
  incoming kilobits per second, and local traffic (defined by a subnet),
  which can be limited separately to a ceiling (which usually does not
  matter as long as it is high enough) of some megabits per second. The
  defaults given here are typical for a 576kb/s ADSL line and a 100mb/s
  LAN.
'

DFEXIF='eth0'

DFKBEGRE='200'
DFKBINGR='400'

DFLNET='192.168.0.0/16'
DFMBLNET='90'

DFLOWPPORTSRC='4661 4662 4665 4672 6881 6882 6883 6884'
DFLOWPPORTDST=''

DFLOWPADDRSRC=''
DFLOWPADDRDST=''

test -x "$SSRC" && . "$SSRC"
test -x "$SSCF" && . "$SSCF"

HELP="
    -i|--nic IF		network interface is IF [$DFEXIF]

    -u|--upload KBS	max kb/s to send on IF [$DFKBEGRE]
    -d|--download KBS	max kb/s to receive on IF [$DFKBINGR]

    -l|--localnet SUBNET local traffic is to/from SUBNET [$DFLNET]
    -m|--localmbs MBS	limit traffic to/from SUBNET to MBS mb/s [$DFMBLNET]

    -t|--slowto PORTS	outgoing to PORTS is low priority [$DFLOWPPORTSRC]
    -f|--slowfrom PORTS	outgoing from PORTS is low priority [$DFLOWPPORTDST]
    -d|--slowdst ADDRS	outgoing to ADDRS is low priority [$DFLOWPADDRDST]
    -s|--slowsrc ADDRS	outgoing from ADDRS is low priority [$DFLOWPADDRSRC]

    -h|--help		print this help

    [start|status|stop]"

OPTSO='hi:u:d:l:m:t:f:a:'
OPTSL='help,nic:,upload:,download:,localnet:,localmbs:'
OPTSL="$OPTSL,slowto:,slowfrom:,slowsrc:,slowdst:"

if ARGS="`getopt -n \"$ME\" -o \"$OPTSO\" -l \"$OPTSL\" -- \"$@\"`"
then eval set -- "$ARGS"
else
  echo 1>&2 "$ME: '$1' is not valid."
  echo 1>&2 "  $ME$HELP"
  exit 1
fi

while true
do
  case "$1" in
  '-i'|'--nic')		EXIF="$2"; shift 2;;

  '-u'|'--upload')	KBEGRE="$2"; shift 2;;
  '-d'|'--download')	KBINGR="$2"; shift 2;;

  '-l'|'--localnet')	LNET="$2"; shift 2;;
  '-m'|'--localmbs')	MBLNET="$2"; shift 2;;
    
  '-t'|'--slowto')	LOWPPORTDST="$2"; shift 2;;
  '-f'|'--slowfrom')	LOWPPORTSRC="$2"; shift 2;;
  '-d'|'--slowdst')	LOWPADDRDST="$2"; shift 2;;
  '-s'|'--slowsrc')	LOWPADDRSRC="$2"; shift 2;;

  '--')			shift; break;;

  *|'-h'|'--help')
    echo 1>&2 "  $ME$HELP"
    exit 1;;
  esac
done

: ${EXIF:="$DFEXIF"}
: ${KBEGRE:="$DFKBEGRE"}
: ${KBINGR:="$DFKBINGR"}
: ${LNET:="$DFLNET"}
: ${MBLNET:="$DFMBLNET"}

: ${LOWPPORTSRC:="$DFLOWPPORTSRC"}
: ${LOWPPORTDST:="$DFLOWPPORTDST"}

: ${LOWPADDRSRC:="$DFLOWPADDRSRC"}
: ${LOWPADDRDST:="$DFLOWPADDRDST"}

: '
  >200kb/s usually is ADSL, >100kb/s usually is dual channel ISDN,
  >50kb/s usually is single channel ISDN, and the rest is modems.

  The quantum should be from 1500 to 60000 bytes (it must be bytes)
  so the "r2q" should be scaled accordingly. Also, the quantum should
  grow a bit the higher the speed.
'
if test "$KBEGRE" -gt 200
then QUAEGRE='3200'
elif test "$KBEGRE" -gt 100
then QUAEGRE='3200'
elif test "$KBEGRE" -gt 50
then QUAEGRE='1600'
else QUAEGRE='1600'
fi

: '
  The LAN traffic class has bandwidth in megabits, lets say from 1 to
  1000, usually.
'
if test "$MBLNET" -gt 100
then QUALNET='120000'
elif test "$MBLNET" -gt 10
then QUALNET='20000'
else QUALNET='3200'
fi

UQIDEROOT='1'
UQIDIROOT='ffff'

case "$1" in
'status')
  tc -s -d qdisc ls dev "$EXIF"
  echo
  tc -s -d class ls dev "$EXIF"
  echo

  tc -s -d filter ls dev "$EXIF" parent "$UQIDEROOT":
  echo
  tc -s -d filter ls dev "$EXIF" parent "$UQIDIROOT":
  ;;

'stop')
  tc qdisc del dev "$EXIF" root    2> /dev/null 1>&2
  tc qdisc del dev "$EXIF" ingress 2> /dev/null 1>&2
  ;;

'start')

: "
    Egress qdiscs and classes.
    ##########################

    From <http://LARTC.org/howto/lartc.qdisc.classful.html> and
    <http://LARTC.org/howto/lartc.qdisc.terminology.html> it is clear
    that there can be only one egress qdisc, but which one is used if
    configurable. If the egress qdisc is classful, then one can attach
    underneath it several layers of classes, and each class can have a
    qdisc (default 'fifo_fast') too. Filters can be attached to classful
    qdisc or classes and decide which subclass (not necessarily a
    directly attached one) processes the packet next.

    Each qdisc has a unique major number and a minor number of '0',
    and each class has a major number which is the same as that of its
    parent (which can be a qdisc or a class), and a unique minor number
    within that major number. The handles are always two numbers, there
    are no Dewey style multilevels. It is however conventional to assign
    classes a globally unique minor number, so that it is possible to
    make the major number of a qdisc attached under a class the same as
    the minor number of that class.

    Here our root qdisc needs to be classful, is either 'htb' or 'cbq'
    depending on what is available, and we have four classes selected
    by as many filters: local traffic, fast, normal and slow external
    traffic. The local class then has a qdisc of 'pfifo' and the others
    share in the same 'htb' or 'cbq' root qdisc.
"

  UQIDEFLOW='1'

  UQIDELNET='2'
  UQIDEREST='3'
  UQIDEFAST='31'
  UQIDENORM='32'
  UQIDESLOW='33'

  QDISCEROOT="$UQIDEROOT":0
  QDISCELNET="$UQIDELNET":0
  QDISCEFAST="$UQIDEFAST":0
  QDISCENORM="$UQIDENORM":0
  QDISCESLOW="$UQIDESLOW":0

  CLASSELNET="$UQIDEFLOW":"$UQIDELNET"
  CLASSEREST="$UQIDEFLOW":"$UQIDEREST"
  CLASSEFAST="$UQIDEFLOW":"$UQIDEFAST"
  CLASSENORM="$UQIDEFLOW":"$UQIDENORM"
  CLASSESLOW="$UQIDEFLOW":"$UQIDESLOW"

  : '
    Limited class, for non local traffic.
    #####################################

    Shape all traffic outgoing to non local address to under "$KBEGRE"/s,
    to prevent huge queues in the modem/router that cause long latency.
  '

  {
    : '
      First compute the widths of the three classes allocated within
      the total "$KBEGRE"/s banwidth.
    '
    KBEFAST="`expr \"$KBEGRE\" '*' 50 / 100`"
    KBENORM="`expr \"$KBEGRE\" '*' 30 / 100`"
    KBESLOW="`expr \"$KBEGRE\" '*' 20 / 100`"

    R2QEGRE="`expr \"$KBENORM\" '*' 1000 / 8 / \"$QUAEGRE\"`"

   : '
      First try to do it with HTB, if that fails,
      do everything with CBQ."
   '

   if tc qdisc add dev "$EXIF" root handle "$QDISCEROOT" \
	htb default "$UQIDENORM" r2q "$R2QEGRE"
   then
      : '
        There is some interesting notes on HTB, and quanta etc. at
          http://WWW.Docum.org/docum.org/
	  http://Luxik.CDI.CZ/~devik/qos/htb/
        for example:
          http://WWW.Docum.org/docum.org/faq/cache/31.html
	  http://Luxik.CDI.CZ/~devik/qos/htb/manual/userg.htm#burst
      '
      if  
	tc class add dev "$EXIF" parent "$QDISCEROOT" classid "$CLASSEREST" \
	  htb rate "$KBEGRE"kbit burst 20kbit quantum "$QUAEGRE"
      then
	: '
 	  High priority class.
 	  ####################
	'
	tc class add dev "$EXIF" parent "$CLASSEREST" classid "$CLASSEFAST" \
	  htb rate "$KBEFAST"kbit ceil "$KBEGRE"kbit quantum "$QUAEGRE" \
	    burst 20kbit prio 0

	: '
 	  Default/average class.
 	  ######################
	'
	tc class add dev "$EXIF" parent "$CLASSEREST" classid "$CLASSENORM" \
	  htb rate "$KBENORM"kbit ceil "$KBEGRE"kbit quantum "$QUAEGRE" \
	    burst 20kbit prio 1

	: '
 	  Low priority class.
 	  ###################
	'
	tc class add dev "$EXIF" parent "$CLASSEREST" classid "$CLASSESLOW" \
	  htb rate "$KBESLOW"kbit ceil "$KBEGRE"kbit quantum "$QUAEGRE" \
	    burst 20kbit prio 2
      fi
    elif tc qdisc add dev "$EXIF" root handle "$QDISCEROOT" \
	  cbq avpkt 1000 bandwidth 10mbit
    then
      if  
	tc class add dev "$EXIF" parent "$QDISCEROOT" \
	  classid "$CLASSEREST" cbq rate "$KBEGRE"kbit \
	    allot 1500 prio 5 bounded isolated
      then
	: '
	  High priority class.
	  ####################
	'
	tc class add dev "$EXIF" parent "$CLASSEREST" classid "$CLASSEFAST" \
	  cbq rate "$KBEGRE"kbit allot 1600 avpkt 1000 prio 0

	: '
	  Default/average class.
	  ######################
	'
	tc class add dev "$EXIF" parent "$CLASSEREST" classid "$CLASSENORM" \
	  cbq rate "$KBENORM"kbit allot 1600 avpkt 1000 prio 1

	: '
	  Low priority class.
	  ###################
	'
	tc class add dev "$EXIF" parent "$CLASSEREST" classid "$CLASSESLOW" \
	  cbq rate "$KBESLOW"kbit allot 1600 avpkt 1000 prio 2
      fi
    else
      echo 1>&2 "$ME: create root class failed on '$EXIF' as 'htb' or 'cbq'"
      exit 1
    fi
  }

  : 'Append to the three subclasses some perturbators for fairness.'

  tc qdisc add dev "$EXIF" parent "$CLASSEFAST" \
    handle "$QDISCEFAST" sfq quantum "$QUAEGRE" perturb 2
  tc qdisc add dev "$EXIF" parent "$CLASSENORM" \
    handle "$QDISCENORM" sfq quantum "$QUAEGRE" perturb 2
  tc qdisc add dev "$EXIF" parent "$CLASSESLOW" \
    handle "$QDISCESLOW" sfq quantum "$QUAEGRE" perturb 2

  : '
    Unlimited class, for local network.
    ###################################
  '

  R2QLNET="`expr \"$MBLNET\" '*' 1000 '*' 1000 / 8 / \"$QUALNET\"`"

  case "$LNET" in ?*)
    tc class add dev "$EXIF" parent "$QDISCEROOT" \
      classid "$CLASSELNET" htb rate "$MBLNET"mbit quantum "$QUALNET"

    tc qdisc add dev "$EXIF" parent "$CLASSELNET" \
      handle "$QDISCELNET" pfifo
  esac

  : '
    Egress traffic filtering
    ########################

    Note the order of the filters matters (probably, but also check the
    "pref" optin, which may be inter-filter or intra-filter).
  '

  : '
    Local traffic.
    ##############
  '
  case "$LNET" in ?*)
    tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 1 \
	 u32 match ip dst "$LNET" \
	flowid "$CLASSELNET";;
  esac

  {
    : '
      Low priority traffic.
      #####################
    '

    : 'TOS Maximize Throughput.'
    tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 10 \
	u32 match ip tos 0x08 0xff \
	flowid "$CLASSESLOW"

    : 'Low priority ports or address ranges.'

    for PORT in $LOWPPORTDST
    do tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 10 \
	u32 match ip dport "$PORT" 0xffff \
	flowid "$CLASSESLOW"
    done

    for PORT in $LOWPPORTSRC
    do tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 11 \
	u32 match ip sport "$PORT" 0xffff \
	flowid "$CLASSESLOW"
    done

    for ADDR in $LOWPADDRSRC
    do tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 11 \
	 u32 match ip src "$ADDR" \
	flowid "$CLASSESLOW"
    done

    for ADDR in $LOWPADDRDST
    do tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 11 \
	u32 match ip dst "$ADDR" \
	flowid "$CLASSESLOW"
    done

    : '
      High priority traffic.
      ######################
    '

    : 'TOS Minimum Delay'
    tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 20 \
	u32 match ip tos 0x10 0xff \
	flowid "$CLASSEFAST"

    : 'ICMP (ip protocol 1)'
    tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 20 \
	u32 match ip protocol 1 0xff \
	flowid "$CLASSEFAST"

    : 'small TCP packets (<64 bytes, ACKs mostly)'
    tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 20 \
	u32 match ip protocol 6 0xff \
	    match u8 0x05 0x0f at 0 \
	    match u16 0x0000 0xffc0 at 2 \
	flowid "$CLASSEFAST"

    : '
      Normal priority traffic.
      ########################
    '

    : '
      Everything else is non interactive non bottom priority
      and is middle priority.
    '
    tc filter add dev "$EXIF" parent "$QDISCEROOT" \
      protocol ip pref 99 \
	u32 match ip tos 0 0 \
	flowid "$CLASSENORM"
  }

  : '
    Ingress traffic control.
    ########################
  '

  : "
    From <http://LARTC.org/howto/lartc.qdisc.terminology.html> it is
    clear that only one 'qdisc' may ever be attached on ingress, and this
    is the eponymous 'ingress' 'qdisc'. All customization is done using
    filters attached directly under that one.
  "

  UQIDIFLOW='4'
  UQIDILNET='5'
  UQIDIREST='6'

  QDISCIROOT="$UQIDIROOT":
  CLASSILNET="$UQIDIFLOW":"$UQIDILNET"
  CLASSIREST="$UQIDIFLOW":"$UQIDIREST"

  : '
    ISPs tend to have huge queues to game benchmarks for large downloads.
    Slow downloads down to somewhat less than the real speed to prevent 
    queuing at the router of our ISP.
  '

  if true
  then
    tc qdisc add dev "$EXIF" handle "$QDISCIROOT" ingress

    case "$LNET" in ?*)
      tc filter add dev "$EXIF" parent "$QDISCIROOT" \
	protocol ip pref 1 \
	  u32 match ip src "$LNET" \
	  police rate "$MBLNET"mbit burst 90kbit mtu 9000b drop \
	flowid "$CLASSILNET";;
    esac

    : '
	There is something bad here in the EL5 kernel, where strange
	thing happen in the "u32 match", and the syntax below seems
	to sort of workaround that.
    '
    tc filter add dev "$EXIF" parent "$QDISCIROOT" \
      protocol ip pref 2 \
	u32 match ip src 0/0 \
	police rate "$KBINGR"kbit burst 20kbit mtu 9000b drop \
      flowid "$CLASSIREST"
  fi;;

*)
  echo 1>&2 "$ME: argument '$1' is not valid."
  echo 1>&2 "  $ME$HELP"
  exit 1;;
esac
