Converting TCL-Based iRules to Distributed Cloud Service Policies and L7 Routes

I have been having lots of conversations with teams lately on the possibility of converting iRules to F5 Distributed Cloud (XC) Services.  And while it's not a direct-import solution, there are constructs that align with what iRules are doing for customers today.  For this article, I will dissect a customer's iRules and highlight how we would move that same functionality to F5 XC.

Note: For the F5 XC Services configs we will really only be using Standard HTTP(s) Load Balancers with L7 Routes, Standard Origin Pools, and Service Policies.

So let's take a look at an example iRule used in a production environment today.

This is our starting point. Let's break things apart.  First, there is no construct for RULE_INIT in XC Services, so let's throw that out.  We know that the customer wants to debug and we know that they are using static::class_name for their datagroup.  So we know they have a datagroup (and we know from working with the customer, the datagroup contains a list of domains), but otherwise nothing special is happening here.  From the datagroup, we can just convert each of the domains to a different L7 Route, unless there are similarities that we can use RegEx to match.

The main portions we will focus on are HTTP_REQUEST and HTTP_RESPONSE.  These are super simple to migrate in most cases.

Under the HTTP_REQUEST event, we see lines 9 through 10 are just setting some values, which we also do not need to do, but if you want to see how we automatically extract the values shown here, you can use this reference: https://docs.cloud.f5.com/docs/how-to/advanced-security/configure-http-header-processing

Now, lines 15 to 24, we have our first real action.  

 

15     if { $r_host eq "internal-site.customer.com" } {
16        if { [IP::addr $client_ip/32 equals 100.40.130.243/32] } {
17          SSL::disable  serverside
18          pool internal-site-pool
19          return
20        } else {
21          log local0. "Unexpected client IP. client_ip=$client_ip host=$r_host uri=$r_uri"
22          reject
23        }
24     }

 

In XC Services, we will make this a service policy attached to the Load Balancer.  So create a service policy allowing only on the prefix.  Matching on Host header to set the pool isn't really required outside basic configs either.  So we just create a load balancer for internal-site.customer.com with Origin set to internal-site-pool.

Lines 26 to 29 are fairly simple as well. We are just listening for a specific list of Host headers and setting the Origin. 

 

26     if { $r_host eq "special-site.customer.com" or $r_host eq "special-site.gov"} {
27        node 191.141.88.211 443
28        return
29     }

 

In this instance, we just create an HTTP LB and add both domains, and set our Origin as needed.  Note in this customer's case was to add a destination which resides on a public IP at another organization. This is baseline functionality with XC Origin Pools today, so nothing special needs to be done here.

Next, we have some "interesting" logic in lines 31 to 49.

 

31     if { $r_host eq "secure.customer.com" } {
32     	 set form_id [URI::query $r_uri "form_id"]
33     	 set A [class match -value $form_id equals code_table]
34     	 set B [URI::query $r_uri "referralcode"]
35     	 if { $A eq "" } {
36     	 	 set A "customer-gift"
37     	 	 set B "foobar1"
38     	 }
39     	 if { $B eq "" } {
40     	 	 if { $A ne "" } {
41     	 	 	 set B "foobar2"
42     	 	 } else {
43     	 	 	 set B "foobar1"
44     	 	 }
45     	 }
46     	 set target "http://donate.customer.com/$A?code=$B"
47     	 HTTP::respond 302 Location $target
48     	 return
49     }

 

We are listening for a specific host header, easy.  Then we are looking at the Query String values, also easy.  We are also iterating through a datagroup to compare the query value to the datagroup values. Not so easy, but since we know the values, we can create the logic require in an L7 Route.  However, if we look closely at this irule, line 35.  If A is null, then set A and B.  But then on line 39, we are checking if B is null, which will never happen because either the request comes in populated, or we set B on 37.  So this stanza does not really do anything for us. So let's move on to line 46+.  Now we can see that we are receiving traffic on one domain, parsing some values, and redirecting to a new domain and adding in the values.  Now if this irule was set up and work as originally intended were we had a broad range of values, it might be a little more complex with the logic, and our lack of ability to concantinate strings today, but with how its been working in production this is an easy conversion, and we can fix some of the logic.

We create an HTTP LB, with a redirect route that matches our known values.

And then we set this as a redirect route, passing our updated querystring parameters, matching the 302 in the irule.

Now we get to line 51, which may seem more complex, but it's really just iterating through the originally defined datagroup (list of domains) and redirecting to a predefined phishing page for that domain.  There are a couple of ways we could do this, one of which is to enable IP intelligence on the XC Load Balancer and let it handle blocking and redirecting phishing domains, but if the customer wants to maintain the list, we can also create routes for each domain as needed.

 

51     set data [class search -all -value $static::class_name equals $r_host]
52     foreach line $data {            
53         if {[catch {foreach {uri target http_code} [split $line $static::sepa] {break}} errmsg]} {
54             log local0. "Error parsing line - $errmsg : $line"
55         } else {
56             if {$static::DEBUG == 1 } {log local0. "Host: $r_host URI: $uri Target: $target Code: $http_code"}
57                 if { [string match $uri $r_uri] } {
58                     if {[catch {eval "set target_expaned $target"} errmsg]} {
59                         log local0. "Error expanding target - $errmsg : $target"
60                         set target_expaned $target
61                     }
62                     if {$static::DEBUG == 1 } {log local0. "Redirecting $r_host $r_uri to: $target_expaned with code: $http_code"}
63                     if {[catch {HTTP::respond $http_code Location $target_expaned} errmsg]} {
64                         log local0. "Error executing the redirect - $errmsg : $target_expaned"
65                     } else {
66                         set done 1
67                     }
68                     break
69                 }
70         }
71     }
72
73     if { $done == 0 } {
74       HTTP::respond 302 Location $default_redirect_url
75     }

 

Another layer of protection here is that if the domain is not explicitly advertised via an XC load balancer, it's not going to respond to it, meaning if the domain doesn't exist in the Load Balancer, it will not recieve a response, so manual protections like this irule implement are no longer needed.

Finally we get to line 78, HTTP_RESPONSE.  And here we are just adding some headers.  This can be done in a few locations.  If we are using routes and want the Response headers added to specific domains, we can add/modify/remove from the route construct, but as we see in the irule, the customer wants these values applied to all response headers.  So first on the list is HSTS, this is enabled via a checkbox in every HTTP Load balancer, so nothing fancy there.  Then we have X-FRAME-OPTIONS, which can either be injected manually or done via CORS configs, manually it would just be under More Options on the Load Balancer, and add response header.

 

when HTTP_RESPONSE {
    HTTP::header insert Strict-Transport-Security "max-age= 31536000"
    HTTP::header insert "X-FRAME-OPTIONS" "SAMEORIGIN"
}

 

That pretty much wraps up this small(ish) irule conversion.  I have a few other examples built in terraform over on github: xc-app-services-tf 

Updated Oct 06, 2023
Version 4.0
  • Great stuff, MichaelatF5!! Do you have some general guidance on the types of iRules that will lend themselves to easy conversion and those that won't?

  • JRahm So far, anything that is a rewrite of a header of any kind or an L7 path/route logic control is a solid candidate.  Anything that we can extract or inject into a header is perfect.

    We (in XC) cannot do streaming rewites today, so anything with a COLLECT will not be a good candidate, or anything that relies on APM events.  HOWEVER, streaming rewrites are a great candidate for inline NGINX in vk8s.

  • Nice work MichaelatF5 - you could write another article showing how this example iRule was a good candiate for refactor to LTM Policies. There seems to be more correlation between LTM Policies and Service Policies in F5 Distributed Cloud.

  • Thanks for this post!  I'm in the process of trying to reconcile the features between BigIP and XC and this will help.

    Here is another common scenario, pool selection based on a Header.  Is there a way with XC routes to select between alternative pools (XC Origins) based on a Header in the request?

    Thanks for the tips!

    Cheers,

    Mike

     

  • Nova selecting "origin" based on header is extremely easy.  This is done using (L7) Routes, same as mentioned for header matching above, and then just select the origin (pool) that you want to send traffic to.