virtual server connection rate limit with tables

Problem this snippet solves:

Summary: Limit the rate of connections to a virtual server to prevent overloading of pool members

iRule Methodology:

For a given time interval, ensure that the rate of connections doesn't exceed a configurable threshold. Uses the table command to track number of new connections per second.

Code :

#
# Name: virtual server connection rate limiting rule
#
# Purpose: Limit the rate of connections to a virtual server to prevent overloading of pool members
#
# Methodology: For a given time interval, ensure the rate of connections doesn't exceed a configurable threshold
#
# Requirements: 
#   - LTM v10.1 or higher to use the table command
#   - IP address datagroup containing a whitelist of IPs/subnets that should not be limited/counted (vsratelimit_whitelist_class)
#   - Addition of this iRule to the virtual server.
#
# Warning: Due to a bug (fixed in 10.2.2), TMM can crash if this iRule is reconfigured or removed 
#   from the virtual server while traffic is pending.  For more info see: 
#   Changing an iRule assignment on a virtual server may cause TMM to panic. 
#   http://support.f5.com/kb/en-us/solutions/public/11000/100/sol11149.html
#
when RULE_INIT {

   # Log debug to /var/log/ltm? 1=yes, 0=no.
   set static::conn_debug 1

   # Maximum connection rate (new connections / time interval)
   # To avoid the bug described in SOL11149, you could move this configuration to a datagroup
   set static::conn_rate 10

   # Time interval (in seconds)
   # This shouldn't need to be changed.
   # We track connections opened in the last X seconds
   set static::interval 1

   log local0. "Configured to enforce a rate of [expr {$static::conn_rate / $static::interval}]\
      cps ($static::conn_rate connections / $static::interval second)"

   # IP datagroup name which contains IP addresses/subnets to not track/rate limit
   set static::whitelist_class vsratelimit_whitelist_class

   # Generate a subtable name unique to this iRule and virtual server
   #   We use the virtual server name with this prefix (vsratelimit_)
   set static::tbl "vsratelimit"
}
when CLIENT_ACCEPTED {

   # Don't process any of this iRule if the client IP is in the whitelist datagroup
   if {[class match [IP::client_addr] equals vsratelimit_whitelist_class]}{

      # Exit this event from this iRule
      return
   }

   # Track this connection in the subtable using the client IP:port as a key
   set key "[IP::client_addr]:[TCP::client_port]"

   # Save the virtual server-specific subtable name (vsratelimit_)
   set tbl ${static::tbl}_[virtual name]

   # Check if we're under the connection limit for the virtual server
   set current [table keys -subtable $tbl -count]
   if { $current >= $static::conn_rate } {

      # We're over the rate limit, so reset the connection 
      if { $static::conn_debug }{ log local0. "$key: Connection to [IP::local_addr]:[TCP::local_port]\
         ([virtual name]). At limit, rejecting (current: $current / max: $static::conn_rate)" }
      #reject
      # apachebench stops when it gets a reset, so you can test with TCP::close to send a FIN and close the connection cleanly.
      TCP::close

   } else {

      # We're under the virtual server connection rate limit, 
      # so add an entry for this client IP:port with a lifetime of X seconds
      table set -subtable $tbl $key " " indefinite $static::interval
      if { $static::conn_debug }{ log local0. "$key: Connection to [IP::local_addr]:[TCP::local_port]\
         ([virtual name]). Under limit, allowing (current: [table keys -subtable $tbl -count] / max: $static::conn_rate)" }
   }
}
Published Mar 18, 2015
Version 1.0

Was this article helpful?

4 Comments

  • This can be bypassed by reusing source ports. Use this instead for the key:

    set key [clock clicks]:[TMM::cmp_group]:[TMM::cmp_unit]
    
  • Hi Amy, Is the [clock clicks]:[TMM::cmp_group]:[TMM::cmp_unit] granular enough for a site with high connection rate?

     

  • That's a great question. It is - though if you're using a current version, you might prefer the built-in rate-limiting. There's a setting for it on virtual servers.

    Here's why it works: Tcl says [clock clicks] should be the highest resolution counter available, so every call (for a given TMM - more on that in a second) will return a unique value. I checked using this iRule:

    when RULE_INIT {
      log local0. [clock clicks]
      log local0. [clock clicks]
    }
    

    and got these log messages, which look like microseconds:

    Mar 28 08:28:16 localhost info tmm[18961]: Rule /Common/log_clicks : 1553786896032566
    Mar 28 08:28:16 localhost info tmm[18961]: Rule /Common/log_clicks : 1553786896032743
    

    Using TMM::cmp_group and TMM::cmp_unit avoids collisions between TMMs, even if two call clock at the same microsecond (I borrowed this idea from twitter, it's similar to their Snowflake CRDT).

    Be aware this can create a lot of inter-TMM traffic on high-traffic systems - not normally an issue, but each subtable lives on a single TMM and so if you have a large system with a busy virtual server this can put a lot of load on one TMM. I recommend testing either way.