#! /bin/bash
#
# htb-script;  start and stop traffic shaping with HTB
#
# chkconfig: - 45 55
# description: this assumes server and router functionality on the box
#              on which the script runs.  It sets up traffic control to
#              and from the Internet and internal network without
#              limiting traffic to/from the server itself.
# <!-- trick an XML parser sourcing us. Rest of file is CDATA --> <![CDATA[
# Copyright (c)2003 SecurePipe, Inc. - http://www.securepipe.com/
# 
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, 
# Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#
#    Martin A. Brown <mabrown@securepipe.com>
#
# -- this script is not as well-conceived and researched as the
#    wondershaper:  http://lartc.org/wondershaper/ 
#    If you have a small LAN of users behind a "broadband" link (ADSL
#    or cable) and are tired of sacrificing interactivity to your
#    large downloads, go fetch the wondershaper.
#
# -- Here is an English summary of the script.
#
#    Remember: we can only shape what we transmit.
#
#    Definitions:
#
#     - $REALDOWN and $REALUP represent the Internet bandwidth
#       available to the router; in good QoS fashion, this should
#       be capped so that this host is the bottleneck.
#     - $SERVERS are space-separated CIDR addresses; IPs from which
#       never to limit the download rate; intended to be locally
#       hosted IP addresses on the server
#     - $USERS are space-separated CIDR addresses; internal networks
#     - $USERDOWN and $USERUP represent the aggregate capped bandwidth
#       allowed to the $USERS
#     - $EXCEPTIONS are space-separated IPs (or CIDR networks)
#       which have access to the $REALUP and $REALDOWN bandwidth.
#
#    Goals (in order of importance):
#
#     - never limit from $SERVERS to $USERS
#     - limit $USERS to max Internet upload of $USERUP on $INET
#     - limit $USERS to max Internet download of $USERDOWN on $LAN
#     - limit $EXCEPTIONS to max Internet upload of $REALUP on $INET
#     - limit $EXCEPTIONS to max Internet upload of $REALDOWN on $LAN
#
#    Assumptions:
#
#     - HTB 3 capable kernel (2.4.20 works)  
#     - tc with HTB support; see http://luxik.cdi.cz/~devik/qos/htb/
#     - iptables; not ipchains; maybe a future option?
#     - mangle table PREROUTING and POSTROUTING can be flushed
#       and used with impunity
#
#    Notes:
#
#     - outbound traffic uses fwmark to differentiate traffic classes
#       and apply traffic control after packets have been SNATted or
#       MASQUERADEd
#     - inbound traffic knows the destination IP, so uses the u32
#       classifier instead of the fw classifier (because I couldn't
#       get it to work!!)
#     - using IMQ and/or ingress policers would probably be better than
#       the crude hackery employed in this script.  All we can do once
#       we have the packets is delay them from reaching the client, so
#       it's a good thing that HTB is not a work-conserving scheduler!
#       Use this as a last resort.
#
# -- written initially, 2003-03-02; -MAB
#
# -- commented heavily, 2003-03-03; -MAB

#
# -- if you need to modify PATH so that tc with HTB support can be
#    found in the path, do it here:
#
# PATH=/usr/local/bin:$PATH
#
# -- Why not?
#
tc=$( which tc )
iptables=$( which iptables )

# -- or you could always hardcode your tc here:
#
# tc=/path/to/your/tc-htb

# -- INTERFACES ON THE SERVER 
#
#    INET = Internet-connected interface on your server
#    LAN  = internal interface/LAN connected interface
#
# INET=wan0             # -- Oooh.  Sangoma.  Yummy.
# INET=ppp0             # -- cheap ADSL; probably rp-pppoe
INET=eth1               # -- pretty common outside interface name

# -- LAN is the interface on which your internal users are reachable
#
LAN=eth0

# -- real bandwidth on Internet INTERFACE = INET
#    This could also be the max speed you want the INET interface
#    to download/upload
#
#
REALDOWN=1536kbit       # -- T1 speed; some xDSL lines
# REALDOWN=1mbit        # -- many cable lines; xDSL
# REALDOWN=768kbit      # -- xDSL and cable
# REALDOWN=384kbit      # -- xDSL
# REALDOWN=256kbit      # -- xDSL
# REALDOWN=128kbit      # -- Alright! Who's still using ISDN? :)
# REALDOWN=600kbit

# REALUP=384kbit        # -- for an asynchronous line, specify
# REALUP=256kbit        #    your upload speed separately
# REALUP=128kbit        #    from the download speed
# REALUP=128kbit        #    from the download speed

REALUP=$REALDOWN        # -- synchronous connections, ISDN, T1, SDSL

# -- LAN real speed
# 
# LANSPEED=10mbit       # -- 10baseT;  Yeah.  Right.  You're reading THIS!
LANSPEED=100mbit        # -- 100BaseT;  Good bet.

# -- make a space separated list of the IPs you never want to
#    limit.  If you have a DMZ just outside this routing device,
#    be sure to add that network.  Also add the IPs bound to $LAN
#    if you are using any services on this box.
#
#    SERVERS="192.168.20.1 192.168.14.2 200.220.113.0/24"
#
SERVERS=""


# -- LAN user definetions: Max download/upload speed
#
#    USERS       = Subnet where all lan users are
#    USERDOWN    = Max Download speed for users
#    USERUP      = Max Upload speed for users 
#    USERMARK    = arbitrary fwmark applied to all user traffic
#
#    USERS="192.168.20.0/24 192.168.0.0/24"
#
USERS=""

# -- Do not set $USERDOWN and $USERUP below $REALUP and $REALDOWN.
#    Things could get very strange.
#
USERDOWN=100kbit
USERUP=100kbit
USERMARK=5

# -- Exceptions to the $USERS; these IPs will have access
#    to $REALUP and $REALDOWN
#
#    EXCEPTIONS  = space separated list of IPs to exclude from
#                  $USERUP and $USERDOWN bandwidth caps
#    EXCEPTMARK  = arbitrary fwmark applied to all packets from
#                  $EXCEPTIONS must be different from $USERMARK
#
#    EXCEPTIONS="192.168.20.10 192.168.20.9"
#
EXCEPTIONS=""
EXCEPTMARK=6

# - - - - - - - - - - - -
  removeqdisc () {
# - - - - - - - - - - - -
#
# -- clean out the qdiscs, entirely
#
  $tc qdisc del dev $1 root    > /dev/null 2>&1
  $tc qdisc del dev $1 ingress > /dev/null 2>&1
}

# - - - - - - - - - - - -
  outboundshaping () {
# - - - - - - - - - - - -
#
  DEV=$1
  # -- set up the HTB and make 10 the default class; fast class!
  #
  $tc qdisc add dev $DEV root handle 1: htb default 10

  # -- top level class; all are children of this class
  #
  $tc class add dev $DEV  parent 1: classid 1:1 htb rate $REALUP

  # -- put an SFQ in the leaf class for the server so that no single
  #    conversation is allowed to dominate give this class priority
  #
  $tc class add dev $DEV  parent 1:1 classid 1:10 \
    htb rate $REALUP burst 20k
  $tc qdisc add dev $DEV parent 1:10 handle 10: sfq perturb 10
  
  # -- also put the exceptions in this class; users like MIGUEL and BOSS
  #
  $tc filter add dev $DEV parent 1:0 protocol ip handle $EXCEPTMARK \
    fw classid 1:10

  # -- Oh yes, well....we do actually have users, so make a class for
  #    them; give them an SFQ, so that no conversation dominates.
  #
  $tc class add dev $DEV  parent 1:1 classid 1:7 \
    htb rate $USERUP ceil $USERUP
  $tc qdisc add dev $DEV parent 1:7 handle 70: sfq perturb 10
  $tc filter add dev $DEV parent 1:0 protocol ip handle $USERMARK \
    fw classid 1:7

}

# - - - - - - - - - - - -
  inboundshaping () {
# - - - - - - - - - - - -
#
  DEV=$1
  # -- set up the HTB and make 10 the default class to limit downstream
  #    bandwidth
  #
  $tc qdisc add dev $DEV root handle 1: htb default 10

  # -- top level class; all are children of this class
  #
  $tc class add dev $DEV parent 1: classid 1:1 htb rate $LANSPEED

  # -- default class for all downloads; with an SFQ inside so
  #    nobody can dominate
  #
  $tc class add dev $DEV  parent 1:1 classid 1:10 \
    htb rate $USERDOWN ceil $USERDOWN
  $tc qdisc add dev $DEV parent 1:10 handle 10: sfq perturb 10
  for USERNET in $USERS ; do 
    $tc filter add dev $DEV parent 1:0 protocol ip \
      u32 match ip dst $USERNET flowid 1:10
  done
  # -- put an SFQ in the leaf class for the real download speed
  #    conversation is allowed to dominate give this class priority
  #
  $tc class add dev $DEV parent 1:1 classid 1:7 \
    htb rate $REALDOWN ceil $REALDOWN burst 20k
  $tc qdisc add dev $DEV parent 1:7 handle 7: sfq perturb 10

  # -- also put the exceptions in this class; users like MIGUEL and BOSS
  #    so they get the full download speed of the link; "prio 8" is important
  #    this selects the priority of the filter with respect to other filters
  #    we want this to be a higher priority than the $USERS, but a lower
  #    priority than $SERVERS traffic
  #
  for smartuser in $EXCEPTIONS; do
    $tc filter add dev $DEV parent 1:0 protocol ip \
      u32 match ip dst $smartuser/32 flowid 1:7
  done

  # -- we MUST make sure that packets to/from the server do not get
  #    captured in the default class!
  $tc class add dev $DEV parent 1:1 classid 1:8 \
    htb rate $LANSPEED burst 50k
  $tc qdisc add dev $DEV parent 1:8 handle 8: sfq perturb 10
  for SERVERNET in $SERVERS ; do
    for USERNET in $USERS ; do
      $tc filter add dev $DEV parent 1:0 protocol ip \
        u32 match ip src $SERVERNET \
        match ip dst $USERNET flowid 1:8
    done
  done

}

# - - - - - - - - - - - -
 marking () {
# - - - - - - - - - - - -
#
  # -- we're, rather rudely, going to empty the mangling tables
  #
  $iptables -t mangle -F PREROUTING
  $iptables -t mangle -F POSTROUTING

  # -- add some rules so that our "smartusers" get more bandwidth
  #
  for smartuser in $EXCEPTIONS ; do
    $iptables -t mangle -I PREROUTING -i $LAN -s $smartuser \
     -j MARK --set-mark $EXCEPTMARK
    $iptables -t mangle -I POSTROUTING -o $LAN -d $smartuser \
     -j MARK --set-mark $EXCEPTMARK
  done

  # -- here we set up the marking; ALL internal IPs go in one group
  #
  for USERNET in $USERS ; do
    $iptables -t mangle -I PREROUTING -i $LAN -s $USERNET \
      -j MARK --set-mark $USERMARK
    $iptables -t mangle -I POSTROUTING -o $LAN -d $USERNET \
      -j MARK --set-mark $USERMARK
  done

}

# - - - - - - - - - - - -
  htbstatus () {
# - - - - - - - - - - - -
#
  output=$1
  test "$output" != "class" &&        echo " -- UPLOAD queuing disciplines --"
  test "$output" != "class" && $tc -s qdisc show dev $INET
  test "$output" != "qdisc" && echo ; echo " -- UPLOAD classes --"
  test "$output" != "qdisc" && $tc -s class show dev $INET
  test "$output" != "class" &&        echo " -- DOWNLOAD queuing disciplines --"
  test "$output" != "class" && $tc -s qdisc show dev $LAN
  test "$output" != "qdisc" && echo ; echo " -- DOWNLOAD classes --"
  test "$output" != "qdisc" && $tc -s class show dev $LAN
}

# - - - - - - - - - - - -
# main ()
# - - - - - - - - - - - -
#

# see how we were called
case "$1" in 
      start) 
             removeqdisc $INET
             removeqdisc $LAN
             outboundshaping $INET
             inboundshaping $LAN
             marking
             ;;
       stop) 
             $iptables -t mangle -F PREROUTING
             $iptables -t mangle -F POSTROUTING
             removeqdisc $LAN
             removeqdisc $INET
             ;;
    restart) 
             $0 stop
             $0 start
             ;;
     status) 
             shift
             htbstatus $@
             ;;
          *) echo "usage: $0 {start|stop|restart|status}"
             ;;
esac

# ]]>  <!-- the line above closes the XML CDATA from above -->
# - end of htb-script
