An iRules Challenge, Resolved
Last week I wrote about challenging the new FSEs that were here attending boot camp. I got to throw together some requirements for the engineers to cut their teeth on, so to speak, and learn about iRules and at least as importantly how to make use of the resources they have available to them. I wanted to present both the challenge as well as my take on the solution somewhat iRule::ology style, but broken down by requirement rather than line by line of code. Even though the iRule is created to solve a fictional list of requirements, there’s still some value in seeing how to go about each of these things, so let’s take a look at the challenge and then my take on the solution.
First, the requirements:
- Client HTTP request must be sent to a specified URI based on the CNAME requested.
- Over 1000 unique CNAMEs to be matched.
- If no match is found, respond to the user (from the iRule) with a simple HTML message stating as such.
- If a match is found, insert a cookie with the resulting URL of the CNAME match, ensuring the next time they request domain.com they are automatically sent to the appropriate <cname>.domain.com based on the cookie.
- Log the country of origin for each successful request along with the URI they were routed to and why.
- Logs must be shipped to an external syslog server (10.10.10.1).
- External syslog server is only routable via the Management interface, not via Self-IP.
And here is the iRule solution that I came up with. There are many different ways that you could skin this cat, obviously, but I was looking for the most effective/efficient solution possible while maintaining some form of readability.
1: when HTTP_REQUEST {2: set setcookie 03: if {[HTTP::cookie exists "ltm_cname"]}{4: set redirURI [HTTP::cookie value "ltm_cname"]5: } else {6: set redirURI [class search -value cnameclass equals [getfield [HTTP::host] "." 1]]7: if {$redirURI ne ""} {8: set setcookie 19: } else {10: HTTP::respond 200 content "<HTML>No match was found for [HTTP::host]. Please try another CNAME.</HTML>"11: return12: }
13: }
14: log local0. "Inbound Req from IP: [IP::client_addr], Country: [whereis [IP::client_addr]] sent from CNAME: [HTTP::host] to URI: $redirURI"15: HTTP::uri $redirURI16: }
17:
18: when HTTP_RESPONSE {19: if {$setcookie} {20: HTTP::cookie insert name "ltm_cname" value $redirURI21: }
22: }
Now then, let’s break down the iRule I came up with to solve these issues and count off the requirements in order, along with the code that solves each of them:
1. Client HTTP request must be sent to a specified URI based on the CNAME requested.
This portion of the requirements is a bit complex to show in just a few lines of code, as most of the rule revolves around accomplishing this. Basically what’s happening is we’re performing a class lookup on the cname of the requested URL. If we get a value back from that, we’ll use that variable later to perform an HTTP::uri rewrite of the URI itself. I chose not to redirect to save the connection cost of closing this connection and forcing the user to re-open a new one with the new URI. If we’re focusing precisely on the URI rewriting portion of the code itself, however, things become very simple as that only takes a single line for us to start with:
HTTP::uri $redirURI
This was just included to force people away from hard-coding the cases in logic within the iRule. I wanted to lead people down a path of using a data group or something similar. I chose to perform a class search to retrieve the desired value for efficiency and clarity. A class would be set up with the format:
class cnameclass {
"/uri” := “/new/uri”
}
I’m then able to scale this out to many thousand entries and retain efficiency and manageability. It also makes the code required for the lookup very straight forward. All we need to do is single out the cname portion of the URL, that is, the portion that comes before the domain name. So in a request like www.domain.com “www’ would be the CNAME portion of the request. We’re expecting people to be entering bob.domain.com, or mysite.domain.com, etc. To get just the CNAME of the URL I’m using the getfield command against the HTTP::host value, splitting on “.” and taking the first portion. Then run that through the class search command with the “-value” flag, and we’ll get the URI we want to redirect to:
set redirURI [class search -value cnameclass equals [getfield [HTTP::host] "." 1]]
3. If no match is found, respond to the user (from the iRule) with a simple HTML message stating as such.
The solution here should be obvious to any long-term iRulers. HTTP::respond can very easily send a response back to the client from within an iRule. The only thing left to do is to set up some logic so that only people that try to access a cname with no matching class entry get this response. The other important part of this snippet is the return command. I’m saving myself some logic in this code by forcing the iRule to exit any time this response is sent. By doing this, I don’t have to wrap a big if/else around the functional code with this as the else clause. This chunk looks like (and yes, I omitted a line that wasn’t relevant, that’s not a formatting bug):
if {$redirURI ne ""} { ... } else { HTTP::respond 200 content "<HTML>No match was found for [HTTP::host]. Please try another CNAME.</HTML>" return }
4.If a match is found, insert a cookie with the resulting URL of the CNAME match, ensuring the next time they request domain.com they are automatically sent to the appropriate <cname>.domain.com based on the cookie.
This one gets a little bit more tricky, and was where many of the new engineers got tripped up, but that was kind of the intent of the challenge, wasn’t it? The trick here is two fold. One is that you have to work backwards logically from where we are in the requirements to make sure that the very first thing you do in your iRule is to check for this cookie you’re about to set and act upon the results. The other is that to properly set a cookie for a client, you have to set it on the response, not the request. This is something I still see people struggling with, which is why I wanted to get this lesson out of the way at the beginning for these awesome new engineers. So, the code in question here is actually pretty simple. First we do a simple check for the cookie, setting the redirURI to the cookie value if it exists. Then we make sure to set up a variable telling us whether or not we need to insert a cookie in the response. Lastly, in the response, we check for said variable and set the cookie as necessary:
set setcookie 0 if {[HTTP::cookie exists "ltm_cname"]}{ set redirURI [HTTP::cookie value "ltm_cname"] } else { set redirURI [class search -value cnameclass equals [getfield [HTTP::host] "." 1]] if {$redirURI ne ""} { set setcookie 1and
when HTTP_RESPONSE { if {$setcookie} { HTTP::cookie insert name "ltm_cname" value $redirURI } }
This was surprisingly easy for all of the engineers, and I think every single one of them got this step correct. The whereis command must be pretty darn easy to use, which is a great thing. All this requires is a pretty simple log statement and some use of the whereis command, and you’re all set. The whereis command will return the country of origin for any IP address that you feed it (with more optional arguments for region and the like if you so desire). This was implemented somewhat recently (v10.1) and no one seemed to have any trouble digging up the proper usage.log local0. "Inbound Req from IP: [IP::client_addr], Country: [whereis [IP::client_addr]] sent from CNAME: [HTTP::host] to URI: $redirURI"
6. Logs must be shipped to an external syslog server (10.10.10.1) & 7. External syslog server is only routable via the Management interface, not via Self-IP.
I’m combining #6 and #7 because this was a bit of a trick question, as it were, since there isn’t anything that you can do via iRules to ensure that this happens or doesn’t happen. What I mean is, TMM can’t route to the management interface. As such, you can’t directly send any log traffic from an iRule to a syslog server hanging off the management port. To do this you’d have to update syslog-ng on the LTM and convince it to log remotely to the desired server. This was far less about iRules coding, obviously, and far more about learning the limitations of the products and how to research best practices and gotchas. These requirements came directly from the forums, so it’s definitely something people are doing out there. No code to show for these requirements.
That’s it for my explanation of the code and how it works. Keep an eye on my blog for more info on the winners and their solutions.