With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command...

  1. Basics
  2. New Ways To Set Data
  3. New Ways To Alter Data
  4. Data Expiration
  5. Advanced Data Expiration
  6. Subtables
  7. Counting
  8. The Fine Print
  9. Examples

In this last part of the series on the usage of the new table command, we discuss various examples of the use of the table command.

Examples

Now that we've covered all of the pieces of the table command, let's see some larger real-world uses of making them work together, not only simplfying existing iRules, but making new things possible.

Limiting Connections To A VIP

On the Counting page, we gave one powerful example iRule that would have been too bulky and/or unreliable to do before: limit a VIP to a certain number of connections. That example can be easily modified to limit those connections by IP address as well. If you compare it to this example

when CLIENT_ACCEPTED {
    set tbl "connlimit:[IP::client_addr]"
    set key "[TCP::client_port]"

    if { [table keys -subtable $tbl -count] > 1000 } {
        event CLIENT_CLOSED disable
        reject
    } else {
        table set -subtable $tbl $key "ignored" 180
        set timer [after 60000 -periodic { table lookup -subtable $tbl $key }]
    }
}
when CLIENT_CLOSED {
    after cancel $timer
    table delete -subtable $tbl $key
}

you can see that only the first two lines changed! We've gone from one global subtable to a subtable for each IP address. This is an example of the power of subtables and using them for scoping.

Blocking DNS Flood Attacks

This example to blacklist IPs if they make too many DNS queries per second, uses global arrays to keep track of the number of requests made in the current second, as well as the blacklist of IPs.

when RULE_INIT {
  set ::maxquery 100
  set ::holdtime 600
}
when CLIENT_DATA {
  set srcip [IP::remote_addr]
  set curtime [clock second]
  if { [ info exists ::blacklist($srcip) ] } {
    if { $::holdtime > [expr ${curtime} - $::blacklist($srcip) ] } {
      drop
      return
    }
    unset ::blacklist($srcip)
  }
  if { [ info exists ::usertable(time,$srcip)] and $curtime == $::usertable(time,$srcip) } {
    incr ::usertable(freq,$srcip)
    if { $::usertable(freq,$srcip) > $::maxquery } {
      set ::blacklist($srcip) $curtime
      unset ::usertable(freq,$srcip)
      unset ::usertable(time,$srcip)
      drop
      return
    }
  } else {
    set ::usertable(freq,$srcip) 1
    set ::usertable(time,$srcip) $curtime
  }
}

It has to do all of the work to remove old data from those arrays manually, so not only can that cause all those array entries to just accumulate in memory, but it demotes the VIP it gets attached to (because it uses global variables) .  If we use the table command instead as shown in the following code, a lot of the complexity is handled by the system, so it's now cleaner, simpler, and CMP-compatible. A win all around!

when RULE_INIT {
  set static::maxquery 100
  set static::holdtime 600
}
when CLIENT_DATA {
    set srcip [IP::remote_addr]
    if { [table lookup -subtable "blacklist" $srcip] != "" } {
        drop
        return
    }
    set curtime [clock second]
    set key "count:$srcip:$curtime"
    set count [table incr $key]
    table lifetime $key 2
    if { $count > $static::maxquery } {
        table add -subtable "blacklist" $srcip "blocked" indef $static::holdtime
        table delete $key
        drop
        return
    }
}

Limiting POST Requests Per User

Another example to limit POSTs per-user uses even more very tricky global array manipulation.

when RULE_INIT {
  set ::windowSecs 10
}
when HTTP_REQUEST {
  if { [HTTP::method] eq "POST" } {
    if { ![HTTP::header exists Authorization] } {
      HTTP::respond 401
      return
    }
    set myUserID [getfield [b64decode [substr [HTTP::header "Authorization"] 6 end]] ":" 1]
    set myMaxRate [findclass $myUserID $::MaxPOSTRates3 " "]
    if { $myMaxRate ne "" }{
      set currentTime [clock seconds]
      if { [info exists ::timestamp($myUserID)] } {
        set i [ expr $currentTime - $::timestamp($myUserID) ]
        if { $i >  $::windowSecs } {
          set i $::windowSecs
        }
        while { $i > 0} {
          for {set j  $::windowSecs} {$j > 0} {set j [expr $j - 1]} {
            set k [ expr $j - 1 ]
            set ::count($myUserID.$j) $::count($myUserID.$k)
          }
          set ::count($myUserID.0) 0
          incr i -1
        }
        set k 0
        for {set i 0} { $i < $::windowSecs} {incr i} {
          incr k $::count($myUserID.$i)
        }
        if {$k < $myMaxRate } {
          incr ::count($myUserID.0)
          set ::timestamp($myUserID) $currentTime
        } else {
          HTTP::respond 302 Location http://asdf:44444/
        }
      } else {
        for {set i 0} {$i < $::windowSecs} {incr i} {
          set ::count($myUserID.$i) 0
        }
        set ::timestamp($myUserID) $currentTime
        set ::count($myUserID.0) 1
      }
    }
  }
}

It can be very difficult to understand, to say nothing of trying to maintain or alter it. With the table command to do all of the heavy lifting for us, it becomes much simpler, lighter-weight, and CMP-compatible.

when RULE_INIT {
  set static::windowSecs 10
}
when HTTP_REQUEST {
  if { [HTTP::method] eq "POST" } {
    if { ! [HTTP::header exists Authorization] } {
      HTTP::respond 401
      return
    }
    set myUserID [getfield [b64decode [substr [HTTP::header "Authorization"] 6 end]] ":" 1]
    set myMaxRate [findclass $myUserID $::MaxPOSTRates3 " "]
    if { $myMaxRate ne "" } {
      set reqnum [table incr "req:$myUserId"]
      set tbl "countpost:$myUserId"
      table set -subtable $tbl $reqnum "ignored" indef $static::windowSecs
      if { [table keys -subtable $tbl -count] > $myMaxRate } {
        HTTP::respond 302 Location http://asdf:44444/
        return
      }
    }
  }
}

That's pretty impressive, if I do say so myself. And once again, because the table command will expire the entries automatically, there's no need to worry about old data taking up memory.

We cannot wait to see all of the new (or old!) problems you solve with the help of the table command. Don't forget to show off your work in the iRules CodeShare!