A couple days ago an iRule was published that mitigates Microsoft’s HTTP.sys vulnerability described in CVE-2015-1635 and MS15-034. It’s a short rule, but it features the dreaded regex. Every time I get the chance at user groups, conferences, webinars, I preach the evils of regex on the data plane. One of the many reasons it is not the best choice is that just calling the regex engine is the equivalent of an 8x operation, let alone actually performing the matching. So I decided to look into some optimizations.

Original Rule

The rule as published is pretty simple. And actually, the regex is clean and the string it’s matching against is not long, so whereas the instantiation penalty is still high, the matching penalty should not be, so this isn’t a terrible use of regex. I’ve cleaned up the rule to only test the regex itself, setting the malicious header to a variable in CLIENT_ACCEPTED so I can just slam the box with an ab request (ab -n 5000 http://testvip/) from the BIG-IP cli.

when CLIENT_ACCEPTED {
  set x "GET / HTTP/1.1\r\nHost: stuff\r\nRange: bytes=0-18446744073709551615\r\n\r\n"
}
when HTTP_REQUEST {
  if { $x matches_regex {bytes\s*=.*[0-9]{10}} } {
  HTTP::respond 200 ok
  }
}

With this modified version, over 5000 requests my average CPU cycles landed at 45.4K on a BIG-IP VE on TMOS 11.6 running in VMware Fusion 5. Not bad at all, regex or not.

My Optimized Rule

My approach was to use scan to pull out the appropriate fields, then string commands to match the bytes and the digits in the number 10 or greater. For a scan deep dive, see my scan revisited article in the iRules 101 series.

when CLIENT_ACCEPTED {
  set x "GET / HTTP/1.1\r\nHost: stuff\r\nRange: bytes=0-18446744073709551615\r\n\r\n"
}
when HTTP_REQUEST {
  scan $x {%[^:]:%[^:]:%[^=]=%[^-]-%[0-9]} 0 0 a 0 b
  if { ($a eq " bytes") && ([string length $b] > 9) } {
    HTTP::respond 200 ok
  }
}

This is definitely faster at 42.6K average CPU cycles, but not nearly as much of an improvement as I had anticipated. Why? Well, scan is doing a lot, and then I’m using the ends_with operator, which is equivalent to a string match, and then also performing a string length operation and finally a comparison operation. So whereas it is a little faster, the number of operations performed was more than it needs to be, mostly because I wasn’t clever enough to make a better string match.

Another Way Better Optimized Rule

Which leads us to the current course leader…who I’ll leave unnamed for now, but you all know him (or her) very well in these parts. Back to a better string match. I love that 6+ years in, I learn new ways to manipulate the string commands in Tcl. I suppose if I really RTFM’d the Tcl docs a little better I’d know this, but I had no idea you could match quite the way this rule does.

when CLIENT_ACCEPTED {
  set x "GET / HTTP/1.1\r\nHost: stuff\r\nRange: bytes=0-18446744073709551615\r\n\r\n"
}
when HTTP_REQUEST {
  if { [string match -nocase {*bytes*=*[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]*} $x] } {
    HTTP::respond 200 ok
  }
}

So there you go. One simple string command. And it comes in at 31.3K average CPU cycles. That’s a significant reduction.

(Almost Always) A Better Way

Sometimes regex is the only way. But it’s a rare day when that is the case. There is almost always a better way than regex when it comes to iRules. And since speed is the name of the game on the data plane, I highly encourage all you iRulers out there to not be like me and get yourself deep into the weeds of the Tcl string commands and arm yourself with some seriously powerful weapons. You’ve been charged—Go forth!