Monday, February 20, 2012

F5 Pacfiles R Us Part Duex

 

In my previous article I explained how pac files can be consolidated by using an F5 Load Balancer eliminating a web server in the process.   The pac file for the most part was very static.  What if you want to make it dynamic – such that you have many different proxy systems and you want to serve a different version of the pacfile based on client’s source address.

In this article I am going to show you a way to make it dynamic in 2 steps

 

1. First you need to create a data group contain the IP address of the clients associated to the proxy systems that you want to use.

Name: regions_proxy

Type: Address

"10.10.10.0/255.255.255.0" := "proxy-a.domain.com:8080"

"10.10.0.0/255.255.0.0" := "proxy-b.domain.com:8080"

.

.

.

"10.0.0.0/255.0.0.0" := "proxy-c.domain.com:8080"

...Etc

 

 

2.    Then you need the iRule itself.  This time you are going to use Datagroups and variables

when RULE_INIT {
        # Set the contents of the PAC file to be delivered within static::pacfile. While
        # specific logic here is fine, the "localized" proxy
        # should be returned using the $selected_proxy variable... this
        # variable will be filled in when the file delivered
        # with the value learned from the DataGroup.

   set static::pacfile {
     function FindProxyForURL(url, host) {

         if (isPlainHostName(host))
         return "DIRECT";

         if (shExpMatch(url, "http://10.*")||
         shExpMatch(url, "https://10.*")||
         shExpMatch(url, "ftp://10.*")||
         shExpMatch(url, "http://localhost*")||
         shExpMatch(url, "https://localhost*")||
         shExpMatch(url, "http://127.0.0.1*")||
         shExpMatch(url, "https://127.0.0.1*")||
         shExpMatch(url, "http://172.*")||
         shExpMatch(url, "https://172.*")||
         shExpMatch(url, "ftp://172.*"))
         return "DIRECT";

      if (dnsDomainIs(host, ".extranet.com")||
         dnsDomainIs(host, ".extranet2.com"))
         return "Proxy $proxyselect";
        
         if (dnsDomainIs(host, ".intrant.com")||
         dnsDomainIs(host, ".intranet2.com"))
         return "DIRECT";
       
      return "PROXY $proxyselect";
      }
   }
}

when CLIENT_ACCEPTED {

        # Create a DataGroup class called "proxy_regions" and populate it with
        # the IP networks and their proxy value assignments:
        #
        # "10.0.0.0/8" := "proxya.domain.com:8080"

        if { [class match [IP::client_addr] eq regions_proxy] } {
                set proxyselect "[class match -value [IP::client_addr] eq regions_proxy]"
        } else {
                set proxyselect "DIRECT"
        }
}


when HTTP_REQUEST {
   # Returns pacfile via "proxy.pac" as part of the HTTP Request
   # with specific proxy Content-type
   switch [HTTP::uri] {
      "/proxy.pac" {
         HTTP::respond 200 content [subst $static::pacfile] "Content-Type" "application/x-ns-proxy-autoconfig" "pragma" "no-cache"
      }
   }
}

As you can see with datagroups and variables.  You can control how the pacfile is structured and customized without create different file versions.

F5 Pacfiles R Us

As some of my friends would agree I am an F5 evangelist.  The reason why I am such a fan is because it continues to eliminate or lesson some of the biggest head-aches that I experience at work. One of those pain points that it helps lesson is managing and using pac files.

Pac Files are used throughout various companies to direct traffic out proxy systems but is not wel understood by most and how it relates to F5 ADCs.  Hopefully this article can clear it up for you and give you a better how idea of how to use it with the ADC.

What are pac file?  A pac file is a text file containing JavaScript code.  It is commonly called proxy.pac, but it can be named differently. It’s mostly used for 2 primary reasons:

  • Companies have multiple proxies where traffic is directed through, either for redundancy or access to networks that are normally closed off.
  • Split domain, where a single domain (i.e., domain.com ) is hosted both internally and externally.

A very basic pac file contains a single function.  It’s function is to direct users through a proxy or proxies based on a URL or website request.   A more advanced pacfile script can direct users out through a proxy or proxies based on user’s IP address or subnet.

The pac file settings are usually the following link  http://pac.webserver.com/proxy.pac.  Pac files can also be hosted on different ports where the links for eample port 7070 (i.e., http://pac.webserver.com:7070/proxy.pac)

The pac file setting is configured in the browsers option or preference settings.   Most cases they are labeled clearly.

The pac file can be hosted on the user’s PC or it can be hosted on a web server (i.e. Apache or IIS). Most companies host in a web server, for centralized control.  It’s important to remember that, so far no browser supports more then one pacfile at the user browser level.  However, there are certain 3rd party extensions that can be used to get around this limitation.  However, it’s usually designed for a specific browser rather then a universal version.

 

So now you understand what a pac file is, what does a standard syntax look like?

 

  1: function FindProxyForURL(url, host) {
  2:     
  3:     
  4:     if (shExpMatch(url, "http://10.*")||
  5:     shExpMatch(url, "https://10.*")||
  6:     shExpMatch(url, "ftp://10.*")||
  7:     shExpMatch(url, "http://localhost*")||
  8:     shExpMatch(url, "https://localhost*")||
  9:     shExpMatch(url, "http://127.0.0.1*")||
 10:     shExpMatch(url, "https://127.0.0.1*")||
 11:     shExpMatch(url, "http://172.*")||
 12:     shExpMatch(url, "https://172.*")||
 13:     shExpMatch(url, "ftp://172.*"))
 14:     return "DIRECT";
 15:         
 16: 
 17:     if (dnsDomainIs(host, "insidesite.com")||
 18:     dnsDomainIs(host, "servername.google.com"))
 19:     return "DIRECT";
 20: 
 21:   return "PROXY proxy.internaldomain.com:8080";
 22:   }
 23: }

 

Simple?  It’s actually simple once you know what each lines are doing.   Let me explain: Lines 4 to 14 state that if a user entered a HTTP request that’s a private address or a localhost or loopback,  should go direct and not use a proxy.   Lines 17 to 19 state that if a user entered a HTTP request where the hostname matches either those 2 entries - should go direct and not use a proxy.  Line 21 is the catch all – if the user’s HTTP request does not match any of the IF conditional statements, the script forces a request to go to proxy.internaldomain.com on port 8080.   It’s important to note that the pacfile scripts are processed at the client and NOT on the server that is delivering the pacfile.

 

Now that you the basic idea of pac file syntax, the next step is to know how it works with respect to user, proxy and a web server holding a webpage.

Let’s say the Pac file is hosted on a proxy server also running a web server.  Let’s assume that the user already has the proxy pac file settings already configured in their browser.

 

clip_image002

Step 1: When the user starts the internet browser, browser retrieves the pac file .

 

 

clip_image003

Step 2: The pacfile is downloaded to the browser’s cache and process any URL requests (For example the user’s browser could have home page going to www.cnn.com) before it let’s the user enter information in the URL

 

Step 3: The user then enters in the URL http://www.google.com

 

clip_image005

Step 4:  The browser runs through the pacfile script  like the one above, attempting to match to a IF conditional statement.   In our example it won’t match any IF statement statement – thus by default is sent to “PROXY proxy.internaldomain.com:8080”

 

clip_image006

Step 5: Proxy makes a request to the internet web server and requests for www.google.com page on behalf of the user.

 

clip_image007

Step 6: The page is returned to the proxy.

 

clip_image008

 

Step 7: As a final step, the proxy sends the request web page to user.

 

Okay this is great.  What does this have to do with ADCs?  As it turns out there is one disadvantage for pacfiles – hosting a pacfile.  It requires a web server.  While the thought of hosting a web page might not be earth shattering it does become a problem when you run into the following scenario.

  • Your IT organization might mandate a reduction in the amount of web servers in the environment.
  • Your IT organization could contain a mixture of Squid, Bluecoat, and/or Ironport proxy systems.  Each proxy has their own distinctive method to deliver a pac file, some might provide you flexibility to control your pac file deliver and some might not.
  • Your IT organization wants to deliver different pac files based on users location or type of browser or IP addresses.

 

What if you can host the pac file in an iRule?    A centralized and consistent approach to deliver a pac file(s) to a user without requiring a web server OR relying on proxy vendors.

 

The iRule

IMPORTANT NOTE: From this point the iRule is written to conform with v10

So how do we get an iRule to deliver a pacfile?

 

At this point we know the following:

  • Pac file is a JavaScript code that the browsers understands
  • Pac file is delivered via HTTP REQUEST and RESPONSE

 

function FindProxyForURL(url, host) {
	if (isPlainHostName(host))
	return "DIRECT";

	if (shExpMatch(url, "http://10.*")||
	shExpMatch(url, "https://10.*")||
	shExpMatch(url, "ftp://10.*")||
	shExpMatch(url, "http://localhost*")||
	shExpMatch(url, "https://localhost*")||
	shExpMatch(url, "http://127.0.0.1*")||
	shExpMatch(url, "https://127.0.0.1*")||
	shExpMatch(url, "http://172.*")||
	shExpMatch(url, "https://172.*")||
	shExpMatch(url, "ftp://172.*"))
	return "DIRECT";		
	if (dnsDomainIs(host, "inside.com")||
	dnsDomainIs(host, "inside.net")||
	dnsDomainIs(host, "outside.net"))
	return "DIRECT";
 
     return "PROXY proxy.internaldomain.com:8080";
	}
}

 

What’s the first step?   Since the script needs to be human readable with indents and spaces we need to set the entire Pac file script into a global variable for example “pacfile”

 

  1: 	set static::pacfile {
  2: 	function FindProxyForURL(url, host) {
  3: 		
  4: 		if (isPlainHostName(host))
  5: 		return "DIRECT";
  6: 	  
  7: 		if (shExpMatch(url, "http://10.*")||
  8: 		shExpMatch(url, "https://10.*")||
  9: 		shExpMatch(url, "ftp://10.*")||
 10: 		shExpMatch(url, "http://localhost*")||
 11: 		shExpMatch(url, "https://localhost*")||
 12: 		shExpMatch(url, "http://127.0.0.1*")||
 13: 		shExpMatch(url, "https://127.0.0.1*")||
 14: 		shExpMatch(url, "http://172.*")||
 15: 		shExpMatch(url, "https://172.*")||
 16: 		shExpMatch(url, "ftp://172.*"))
 17: 		return "DIRECT";
 18: 				
 19: 
 20: 		if (dnsDomainIs(host, "inside.com")||
 21: 		dnsDomainIs(host, "inside.net")||
 22: 		dnsDomainIs(host, "outside.net"))
 23: 		return "DIRECT";
 24: 
 25: 	return "PROXY proxy.internaldomain.com:8080";
 26: 	}
 27: }
 28: 

 

The event to use is when RULE_INIT since we are setting the script into a global variable.  Your code looks like the following

  1: when RULE_INIT {
  2: 	set static::pacfile {
  3: 	function FindProxyForURL(url, host) {
  4: 		
  5: 		if (isPlainHostName(host))
  6: 		return "DIRECT";
  7: 	  
  8: 		if (shExpMatch(url, "http://10.*")||
  9: 		shExpMatch(url, "https://10.*")||
 10: 		shExpMatch(url, "ftp://10.*")||
 11: 		shExpMatch(url, "http://localhost*")||
 12: 		shExpMatch(url, "https://localhost*")||
 13: 		shExpMatch(url, "http://127.0.0.1*")||
 14: 		shExpMatch(url, "https://127.0.0.1*")||
 15: 		shExpMatch(url, "http://172.*")||
 16: 		shExpMatch(url, "https://172.*")||
 17: 		shExpMatch(url, "ftp://172.*"))
 18: 		return "DIRECT";
 19: 				
 20: 
 21: 		if (dnsDomainIs(host, "inside.com")||
 22: 		dnsDomainIs(host, "inside.net")||
 23: 		dnsDomainIs(host, "outside.net"))
 24: 		return "DIRECT";
 25: 
 26: 	return "PROXY proxy.internaldomain.com:8080";
 27: 	}
 28: 	}
 29: }

 

The next step is to use HTTP_REQUEST event as the trigger for the request  and use HTTP::response command to send the the contents of the pacfile back to the requestor.   We also need to include a MIME type so the browser knows that proxy pac file is being send.  This is a requirement.  We also need to make sure that requestor’s browser does not cache the file for any length of time.   This is because we want to make updates and get immediate changes to the requestor.

Based on this the code should look something like the following

 

  1: when HTTP_REQUEST {
  2: 	switch -glob [HTTP::uri] {
  3: 	  "/proxy.pac" {
  4: 		HTTP::respond 200 content $static::pacfile "Content-Type" "application/x-ns-proxy-autoconfig" "pragma" "no-cache"
  5: 		return
  6: 	               }
  7: 	}
  8: }
 

 

Putting it all together the entire code should look like the following

  1: when RULE_INIT {
  2:   set static::pacfile {
  3:   function FindProxyForURL(url, host) {
  4:     
  5:     if (isPlainHostName(host))
  6:     return "DIRECT";
  7:     
  8:     if (shExpMatch(url, "http://10.*")||
  9:     shExpMatch(url, "https://10.*")||
 10:     shExpMatch(url, "ftp://10.*")||
 11:     shExpMatch(url, "http://localhost*")||
 12:     shExpMatch(url, "https://localhost*")||
 13:     shExpMatch(url, "http://127.0.0.1*")||
 14:     shExpMatch(url, "https://127.0.0.1*")||
 15:     shExpMatch(url, "http://172.*")||
 16:     shExpMatch(url, "https://172.*")||
 17:     shExpMatch(url, "ftp://172.*"))
 18:     return "DIRECT";
 19:         
 20:     if (dnsDomainIs(host, "inside.com")||
 21:     dnsDomainIs(host, "inside.net")||
 22:     dnsDomainIs(host, "outside.net"))
 23:     return "DIRECT";
 24: 
 25:   return "PROXY proxy.internaldomain.com:8080";
 26:   }
 27:   }
 28: }
 29: 
 30: when HTTP_REQUEST {
 31:   switch -glob [HTTP::uri] {
 32:     "/proxy.pac" {
 33:       HTTP::respond 200 content $static::pacfile "Content-Type" "application/x-ns-proxy-autoconfig" "pragma" "no-cache"
 34:       return
 35:     }
 36:   }
 37: }

As you can see you now have the ability to deliver a pac file without using a web server