Mitigating Slow HTTP Post DDoS Attacks With iRules – Follow-up

Last month I posted a Tech Tip using iRules to mitigate the slow POST DDoS attack. The example that I posted was an early prototype that was passed around an internal mailing list. I listed a few “gotchas” in my original post, but it wasn’t long until the folks started chatting about it and had an improved implementation.

A couple of the limitations mentioned with the first solution were the 4MB TMM payload collection ceiling and not using the Content-Length header to determine payload collection size. Hoolio took the original iRule I posted and added in logic (forum post) to accomplish both of these. There are two static variables that are set upon rule initiation: the default content length (static::content_length) and the timeout for a slow post (static::timeout). The static::content_length variable should be set to be slightly larger than the largest POST payload you expect your application to accept up to just shy of 4MB. If you have large files that rely on POST requests for uploads, you’ll want to build in additional logic to guard against attacks and further verify legitimate requests. Secondly, the timeout is the maximum amount of time we want to allow a client to fulfill their POST request. If your application supports people with a variety of connection speeds and quality, 2 seconds might be a bit short and may end up disrupting legitimate users. Tune the timeout according to your user base.

Next, as a request arrives at the LTM and the iRule is triggered, we will identify the request type. If it is a POST request, we’ll begin processing the HTTP payload collection logic. In the event that the Content-Length is null or larger than our default value, we’ll set our payload collection length to the default content length value. If it is zero, we won’t collect anything. Finally, if it is a reasonable value within our established payload collection bounds, we’ll collect the length specified by the Content-Length header and set the timeout as a ratio of the Content-Length value and the maximum default collect length (2048 bytes in this case).

The next portion of the rule proceeds much like my first example. If the content_length variable has been set, we start the timer and start collecting. If we don’t collect all the payload data within the allotted time, then we respond with a HTTP status code 408 – request timeout (thanks hoolio for correcting me with a more appropriate response code) and close the connection. If the data is collected in a timely manner, we cancel the connection closure. Put that all together and here’s what you’ve got:

   1: when RULE_INIT {
   2:     # Default amount of request payload to collect (in bytes)
   3:     set static::collect_length 2048
   4:  
   5:     # Default timeout for POST requests to send $collect_length bytes (in seconds)
   6:     set static::timeout 2
   7: }
   8:  
   9: when HTTP_REQUEST {
  10:     # Only check POST requests
  11:     if { [HTTP::method] equals "POST"} {
  12:         # Create a local variable copy of the static timeout
  13:         set timeout $static::timeout
  14:  
  15:         # Check for a non-existent Content-Length header
  16:         if {[HTTP::header Content-Length] eq ""}{
  17:             # Use default collect length of 2k for POSTs without a Content-Length header
  18:             set collect_length $static::collect_length
  19:         } elseif {[HTTP::header Content-Length] == 0}{
  20:             # Don't try collect a payload if there isn't one
  21:             unset collect_length
  22:         } elseif {[HTTP::header Content-Length] > $static::collect_length}{
  23:             # Use default collect length
  24:             set collect_length $static::collect_length
  25:         } else {
  26:             # Collect the actual payload length
  27:             set collect_length [HTTP::header Content-Length]
  28:  
  29:             # Calculate a custom timeout based on the same ratio we use for the default collect length and default timeout
  30:             set timeout [expr {[HTTP::header Content-Length] / $static::collect_length * $static::timeout}]
  31:         }
  32:  
  33:         # If the POST Content-Length isn't 0, collect (a portion of) the payload
  34:         if {[info exists collect_length]}{
  35:             # If the entire request hasn't been received within X seconds, send a 408, and close the connection
  36:             set id [after $timeout {
  37:                 HTTP::respond 408 content "Your POST request is not being received quickly enough. Please retry."
  38:                 TCP::close
  39:             }]
  40:  
  41:             # Trigger collection of the request payload
  42:             HTTP::collect $collect_length
  43:         }
  44:     }
  45: }
  46:  
  47: when HTTP_REQUEST_DATA {
  48:     # Check if the 'after' ID exists
  49:     if {[info exists id]} {
  50:         # If all the POST data has been received, cancel the connection closure
  51:         after cancel $id
  52:     }
  53: }
Published Dec 03, 2010
Version 1.0