Enforcing Usage Quotas

Quotas are an effective mechanism for controlling Internet usage. A quota is set on the maximum amount of data that a user is allowed per time period. If a user exceeds their quota then their traffic controlled so that it doesn't interfere with other users.

Quotas are particularly effective for managing peer-to-peer (p2p) activity. Peer-to-peer applications (and users) are very good at getting through access control lists by using non-standard ports, using port 80 (web) etc. Trying to maintain an effective filter to identify p2p traffic is a challenge and the resulting complex rule sets consumes significant resources in the devices attempting to perform the classification. A simple usage quota is easier to maintain and enforce and encourages users to be more responsible in their use of shared resources. Another benefit of quota based controls is that they don't encourage users to tailor p2p application setting to bypass acl rules and so the traffic is easier to monitor. Finally, a quota system can be implemented using standard network hardware, without the addition of a "traffic shaping" appliance that can become a bottleneck and point of failure.

Detection

In order to be able to manage traffic effectively, you should configure Traffic Sentinel with Zones and Groups containing the IP address space on your site (see Configuring Traffic Sentinel). This will allow you to set quotas by Zone and to determine if traffic is local or off-site.

The following script (usage.txt) identifies hosts within selected zones that have exceeded their quotas:

// Set usage policy
var interval = "8am,5pm";

var quotas = new Array();
quotas['Noe Valley'] = 2e9; // 2G bytes
quotas['Marina']     = 4e9; // 4G bytes

// Build zone list
var zones = new Array();
for(zone in quotas) zones.push("'" + zone + "'");

// Get the outbound traffic
var query = Query.topN(
   "rttraffic",
   "sourcezone,ipsource,bytes",
    "sourcezone=" + zones + "&islayer3multicast=0&destinationzone=EXTERNAL",
    interval,
    "bytes",
    100);
var table = query.run();

// See if anyone has exceeded quota and collect info
var exceeded = Table.create(
  ["Host","Bytes"],
  ["address","integer"]);
exceeded.start = table.start;
exceeded.end = table.end;

for(var r = 0; r < table.nrows; r++) {
    var zone = table.cell(r,0);
    var addr = table.cell(r,1);
    var bytes = table.cell(r,2);
    if(zone && addr) {
       if(quotas[zone] < bytes) {
          exceeded.addRow(new Array(addr,bytes));
       }
    }
}

// Add additional information from the network state
function unique(array) {
  var result = new Array();
  for(var i = 0; i < array.length; i++) {
    result[i] = array[i] ? (array[i].length == 1 ? array[i][0] : null) : null;
  }
  return result;
}

var n = Network.current();

// Lookup DNS names
exceeded.insertColumn(
  "DNS","address",unique(n.dnsMap(exceeded.column(0))),0);

// Lookup MAC addresses
exceeded.insertColumn(
  "MAC","address",unique(n.addressMap(exceeded.column(1))),2);

// Locate MAC addresses to switch ports
var interfaces = n.locationMap(exceeded.column(2));

// Get switch IP addresses and port ifNames
var switchIPs = new Array();
var ifNames = new Array();
for(var i = 0; i < interfaces.length; i++) {
  n.path = interfaces[i];
  switchIPs.push(n.agentIP());
  ifNames.push(n.ifName());
}
exceeded.insertColumn("Switch IP","agent",switchIPs,3);
exceeded.insertColumn("ifName","string",ifNames,4);

// Print results
exceeded.printCSV(false);

The quotas are expresses as a number of bytes allowed per day to the outside world (i.e. local traffic on site is not counted). Once the traffic information has been obtained, each host's byte count is compared with its quota and the hosts exceeding their quotas are collected. The script identifies the MAC address associated with each host and uses the MAC addresses to identify the switch and port connecting them to the network. The output of the script has one row for each host that has exceeded its quota. Each row has the following form:

host name, host ip, host mac, switch ip, switch ifName, bytes

For example:

p2p-guy,10.0.0.50,001279631ABB,10.0.0.254,E7,7253998337

Now that we have identified p2p-guy as a problem, the next step is to take some action to enforce the quota.

Enforcement

The information produced by the detection script offers a number of enforcement options, including:

  • disable the switch port, usually only appropriate in emergencies if host is a security threat or is creating a broadcast storm etc.
  • rate limit the switch port, can cause problems if more than one host is connected to the port. Control is excessive in this case since it applies to all network activity and we only need to control off-site traffic.
  • lower the priority of the host's traffic, packets are marked as discardable should congestion occur. This is the prefered mechanism in this case since it is selective and only impacts the user on congested links.

In this case lets assume that the site access routers can use Differentiated Services Code Point (DSCP) policies to prioritise traffic. It makes sense to mark the packets as low priority on the access switches as this distributes the task of traffic classification among the edge switches, reducing the load on the core and allowing a larger number of controls to be applied. One of the benefits of this approach is that packets aren't dropped unneccessarily, priority queuing will only drop packets if there is congestion. Local traffic is unlikely to be affected and off-site traffic will only be affected during peak periods.

The following Expect script (cmdProCurve.txt) automates the steps needed to make a configuration change on a ProCurve switch. The script can be modified for different vendor's equipment.

#!/usr/bin/expect --

set usage "Usage: $argv0 -u user -p password -c command switch\n"

set user "manager"
set password "procurve"
set commands [ list ]

log_user 0
set timeout 10

# Process command line
for {set i 0} {$i < $argc} {incr i} {
  set arg [lindex $argv $i]

  switch -glob -- $arg {
    -u* { incr i; set user [ lindex $argv $i ] }
    -p* { incr i; set password [ lindex $argv $i ] }
    -c* { incr i; lappend commands [ lindex $argv $i ] }
    -* {
      puts "\nError: Unknown argument $arg\n"
      puts $usage
      exit 1
    }
    default { break }
  }
}
          
if { $i == $argc } {
  puts "\nError: no switches listed\n"
  puts $usage
  exit 1 
}     

proc sendcommand { switch user password commands } {

  # Connect to switch using ssh
  spawn ssh $switch -l $user

  expect_after {
    timeout { catch { close }; wait; return 1 }
    eof { catch { close }; wait; return 1 }
  }

  # Login and enter configuration mode
  expect {
   "(yes/no) " { 
      send "yes\r"
      expect {
        "Press any key to continue" {
          send "\r"
          expect {
            "password: " { 
               send "$password\r"
               expect {
                 "# " { send "config\r" }
                 "password: " { catch { close }; wait; return 1 }
               }
            }
            "# " { send "config\r" } 
          }
        }
        "password: " {
          send "$password\r"
          expect {
            "# " { send "config\r" }
            "password: " { catch { close }; wait; return 1 }
          }
        }
        "# " { send "config\r" }
      }
    }
    "Press any key to continue" {
       send "\r"
       expect {
         "password: " {
            send "$password\r"
            expect {
              "Press any key to continue" {
                send "\r"
                expect {
                 "# " { send "config\r" }
                }
              }
              "# " { send "config\r" }
              "password: " { catch { close }; wait; return 1 }
            }
         }
         "# " { send "config\r" }
       }
    }
    "password: " {
       send "$password\r"
       expect {
        "Press any key to continue" {
          send "\r"
          expect {
            "# " { send "config\r" }
          }
        }
        "# " { send "config\r" }
        "password: " { catch { close }; wait; return 1 }
       }
    }
    "# " { send "config\r" }
  }
  expect "(config)# "

  # Send the commands
  foreach command $commands {  
    send "$command\r"
    expect "(config)# "
  }

  # Logout
  send "logout\r"
  expect {
    "\[y/n\]\? " { 
       send "y\r" 
       expect {
         "\[y/n\]\? " { }
         "closed." { }
       }
    }
    "closed." { }
  }
}

# Iterate over switches
foreach switch [lrange $argv $i end] {
  set switch [string tolower $switch]
  sendcommand $switch $user $password $commands
}

The following command on a ProCurve switch will assign a DSCP value for a host (in this case p2p-guy):

HP ProCurve Switch 5304XL(config)# qos device-priority 10.0.0.50 dscp 001010

To use the Expect script to remotely configure a QoS control, use the following command:

./cmdProCurve -u manager -p secret \
-c "qos device-priority 10.0.0.50 dscp 001010" 10.0.0.254

The following command removes the control:

./cmdProCurve -u manager -p secret \
-c "no qos device-priority 10.0.0.50" 10.0.0.254

Automation

The following TCL script (enforce.txt) brings together the detection and enforcement mechanisms to automatically enforce the quotas:

#!/usr/bin/tcl

set usage "Usage: $argv0 \[-q quota\] \[-c cmd\] \[-d dscp\] \[-u\] \[-v\]\n"

set dscp         "001010"
set user         "manager"
set password     "procurve"
set controlsfile "controls.txt"
set quota        "/home/pp/quotas/usage.txt"
set cmd          "./cmdProCurve"
set undo         0
set verbose      0
set test         0

# Process command line
for {set i 0} {$i < $argc} {incr i} {
  set arg [ lindex $argv $i]

  switch -glob -- $arg {
    -q* { incr i; set quota [ lindex $argv $i ] }
    -c* { incr i; set cmd [ lindex $argv $i ] }
    -d* { incr i; set dscp [ lindex $argv $i ] }
    -u* { set undo 1 }
    -v* { set verbose 1 }
    -t* { set test 1 }
    -* {
      puts "\nError: Unknown argument $arg\n"
      puts $usage
      exit 1
    }
    default { break }
  }
}

# Load currently active controls
set controls [ list ]
array set hosts { }

if { [ file exists $controlsfile ] } {
  set fp [ open $controlsfile r ]
  while { -1 != [ gets $fp line ] } {
    lappend controls $line
    set parts [ split $line "," ]
    set host [ lindex $parts 0 ]
    set hosts($host) 1
  }
  close $fp
}

if { $undo == 0 } {
  # Query for latest usage data
  set rows [split [exec /usr/local/inmsf/bin/query $quota] "\n"]
  foreach row $rows {
    set parts [ split $row "," ]
    set host [ lindex $parts 1 ]
    set mac [ lindex $parts 2 ]
    set switch [ lindex $parts 3 ]
    if { ![ info exists hosts($host) ] } {
      # Apply new control
      lappend controls "$host,$mac,$switch"
      set control "qos device-priority $host dscp $dscp"
      if { $verbose == 1 } { puts "$switch -> $control" }
      if { $test == 0 } { exec $cmd -c $control $switch }
    }
  }

  # Write out controls file
  set fp [ open $controlsfile w ]
  foreach row $controls { puts $fp $row }
  close $fp
} else {
  # Remove existing controls
  foreach row $controls {
    set parts [ split $row "," ]
    set host [ lindex $parts 0 ]
    set mac [ lindex $parts 1 ]
    set switch [ lindex $parts 2 ]
    set control "no qos device-priority $host"
    if { $verbose == 1 } { puts "$switch -> $control" }
    if { $test == 0 } { exec $cmd -c $control $switch }
  }
  # Remove controls file
  file delete $controlsfile
}

The script first reads a file that contains the controls it has already applied. It then runs the quota query and applies controls to any new hosts that have exceeded their quota. The new controls are then added to the controls file. The script also provides an undo option in which it removes all the controls that have been applied.

The following steps use the crontab command to automatically run the quota script every 10 minutes during the day to update the controls and once at the end of the day to remove them:

crontab -e

and then add the following lines to your list of tasks:

0,10,20,30,40,50 8-17 * * * /usr/local/quota/enforce
0 18 * * * /usr/local/quota/enforce -u

Communication

Communication is an important element in improving the effectiveness of this type of policy. The goal is to change user behavior and information is a powerful tool that will help users understand how their network usage affects the network (and other users).

The tutorial, Integrating with a Customer Portal shows how traffic information extracted from Traffic Sentinel can be incorporated into existing customer support web sites. In this case it would be helpful to provide users with a running total of their off-site traffic, how close they are to exceeding their quota, as well as a breakdown of their usage by protocol.

Related Topics
  • Help > Report > Script Online help describing javascript API.
  • Tutorial: Scripting Queries
  • Tutorial: Configuring Traffic Sentinel
  • Tutorial: Creating Report Templates
  • Tutorial: Integration with a Customer Portal