Intro
A publicity-adverse colleague of mine wrote this amazing program. I wanted to publish it not so much for what it specifically does, but as well for the programming techniques it uses. I personally find i relatively hard to look up concepts when using TCL for an F5 iRule.
Program Introduction
Test
# RULE_INIT is executed once every time the iRule is saved or on reboot. So it is ideal for persistent data that is shared accross all sessions.
# In our case it is used to define a template with some variables that are later substituted
when RULE_INIT {
# "static" variables in iRules are global and read only. Unlike regular TCL global variables they are CMP-friendly, that means they don't break the F5 clustered multi-processing mechanism. They exist in memory once per CMP instance. Unlike regular variables that exist once per session / iRule execution. Read more about it here: https://devcentral.f5.com/s/articles/getting-started-with-irules-variables-20403
#
# One thing to be careful about is not to define the same static variable twice in multiple iRules. As they are global, the last iRule saved overwrites any previous values.
# Originally the idea was to load an iFile here. That's also the main reason to even use RULE_INIT and static variables. The reasoning was (and I don't even know if this is true), that loading the iFile into memory once would have to be more efficient than to do it every time the iRule is executed. However, it is entirely possible that F5 already optimized iFiles in a way that loads them into memory automatically at opportune times, so this might be completely unnecessary.
# Either way, as you can tell, in the end I didn't even use iFiles. The reason for that is simply visibility. iFiles can't be easily viewed from the web UI, so it would be quite inconvenient to work with.
# The template idea and the RULE_INIT event stayed, even though it doesn't really serve a purpose, except maybe visually separating the templates from the rest of the code.
#
# As for the actual content of the variable: First thing to note is the use of {} to escape the entire string. Works perfectly, even though the string itself contains braces. TCL magic.
# The rest is just the actual PAC file, with strategically placed TCL variables in the form of $name (this becomes important later)
set static::pacfiletemplate {function FindProxyForURL(url, host)
{
var globalbypass = "$globalbypass";
var localbypass = "$localbypass";
var ceglobalbypass = "$ceglobalbypass";
var zpaglobalbypass = "$zpaglobalbypass";
var zscalerbypassexception = "$zscalerbypassexception";
var bypass = globalbypass.split(";").concat(localbypass.split(";"));
var cebypass = ceglobalbypass.split(";");
var zscalerbypass = zpaglobalbypass.split(";");
var zpaexception = zscalerbypassexception.split(";");
if(isPlainHostName(host)) {
return "DIRECT";
}
for (var i = 0; i < zpaexception.length; ++i){
if (shExpMatch(host, zpaexception[i])) {
return "PROXY $clientproxy";
}
}
for (var i = 0; i < zscalerbypass.length; ++i){
if (shExpMatch(host, zscalerbypass[i])) {
return "DIRECT";
}
}
for (var i = 0; i < bypass.length; ++i){
if (shExpMatch(host, bypass[i])) {
return "DIRECT";
}
}
for (var i = 0; i < cebypass.length; ++i) {
if (shExpMatch(host, cebypass[i])) {
return "PROXY $ceproxy";
}
}
return "PROXY $clientproxy";
}
}
set static::forwardingpactemplate {function FindProxyForURL(url, host)
{
var forwardinglist = "$forwardinglist";
var forwarding = forwardinglist.split(";");
for (var i = 0; i < forwarding.length; ++i){
if (shExpMatch(host, forwarding[i])) {
return "PROXY $clientproxy";
}
}
return "DIRECT";
}
}
}
# Now for the actual code (executed every time a user accesses the vserver)
when HTTP_REQUEST {
# The request URI can of course be used to differentiate between multiple PAC files or to restrict access.
# So can basically any other request attribute. Client IP, host, etc.
if {[HTTP::uri] eq "/proxy.pac"} {
# Here we set variables with the exact same name as used in the template above.
# In our case the values come from a data group, but of course they could also be defined
# directly in this iRule. Using data groups makes the code a bit more compact and it
# limits the amount of times anyone needs to edit the iRule (potentially making a mistake)
# for simple changes like adding a host to the bypass list
# These variables are all set unconditionally. Of course it is possible to set them based
# on for example client IP (to give different bypass lists or proxy entries to different groups of users)
set globalbypass [ class lookup globalbypass ProxyBypassLists ]
set localbypass [ class lookup localbypassEU ProxyBypassLists ]
set ceglobalbypass [ class lookup ceglobalbypass ProxyBypassLists ]
set zpaglobalbypass [ class lookup zpaglobalbypass ProxyBypassLists ]
set zscalerbypassexception [ class lookup zscalerbypassexception ProxyBypassLists ]
set ceproxy [ class lookup ceproxyEU ProxyHosts ]
# Here's a bit of conditionals, setting the proxy variable based on which virtual server the
# iRule is currently executed from (makes sense only if the same iRule is attached to multiple
# vservers of course)
if {[virtual name] eq "/Common/proxy_pac_http_90_vserver"} {
set clientproxy [ class lookup formauthproxyEU ProxyHosts ]
} elseif {[virtual name] eq "/Common/testproxy_pac_http_81_vserver"} {
set clientproxy [ class lookup testproxyEU ProxyHosts]
} elseif {[virtual name] eq "/Common/proxy_pac_http_O365_vserver"} {
set clientproxy [ class lookup ceproxyEU ProxyHosts]
} else {
set clientproxy [ class lookup clientproxyEU ProxyHosts ]
}
# Now this is the actual magic. As noted above we have now set TCL variables named for example
# $globalbypass and our template includes the string "$globalbypass"
# What we want to do next is substitute the variable name in the template with the variable values
# from the code.
# "subst" does exactly that. It performs one level of TCL execution. Think of "eval" in basically
# any language. It takes a string and executes it as code.
# Except for "subst" there are two in this context very useful parameters: -nocommands and -nobackslashes.
# Those prevent it from executing commands (like if there was a ping or rm or ssh or find or anything
# in the string being subst'd it wouldn't actually try to execute those commands) and from normalizing
# backslashes (we don't have any in our PAC file, but if we did, it would still work).
# So what is left that it DOES do? Substituting variables! Exactly what we want and nothing else.
# Now since the static variable is read only, we can't do this substitution on the template itself.
# And if we could it wouldn't be a good idea, because it is shared accross all sessions. So assuming
# there are multiple versions of the PAC file with different proxies or bypass lists, we would
# constantly overwrite them with each other.
# The solution is simply to save the output of the subst in a new local variable that exists in
# session context only.
# So from a memory point of view the static/global template doesn't really gain us anything.
# In the end we have the template in memory once per CMP and then a substituted copy of the template
# once per session. So as noted earlier, could've probably just removed the entire RULE_INIT block,
# set the template in session context (HTTP_REQUEST event) and get the same result,
# maybe even slightly more efficient.
set pacfile [subst -nocommands -nobackslashes $static::pacfiletemplate]
# All that's left to do is actually respond to the client. Simple stuff.
HTTP::respond 200 content $pacfile "Content-Type" "application/x-ns-proxy-autoconfig" "Cache-Control" "private,no-cache,no-store,max-age=0"
# In this example we have two different PAC files with different templates on different URLs
# Other iRules we use have more differentiation based on client IP. In theory we could have one big iRule
# with all the PAC files in the world and it would still scale very well (just a few more if/else or switch cases)
} elseif { [HTTP::uri] eq "/forwarding.pac" } {
set clientproxy [ class lookup clientproxyEU ProxyHosts]
set forwardinglist [ class lookup forwardinglist ProxyBypassLists ]
set forwardingpac [subst -nocommands -nobackslashes $static::forwardingpactemplate]
HTTP::respond 200 content $forwardingpac "Content-Type" "application/x-ns-proxy-autoconfig" "Cache-Control" "private,no-cache,no-store,max-age=0"
} else {
# If someone tries to access a different path, give them a 404 and the right URL
HTTP::respond 404 content "Please try http://webproxy.drjohns.com/proxy.pac" "Content-Type" "text/plain" "Cache-Control" "private,no-cache,no-store,max-age=0"
}
}
To be continued...