Writing a PAC File

This section describes how to write a new PAC file from scratch. Zscaler highly recommends that you do the following:

  1. Copy and paste the default PAC file and customize it as necessary.
  2. Build your PAC file one element at a time.
  3. Save the file and test it after each addition.

Before you begin, ensure that you review Best Practices for Writing PAC Files.

FindProxyForURL Function

A PAC file must start with the opening function FindProxyForURL(url, host) on the first line. This function identifies which URLs, host names, or IP addresses are redirected to a proxy. The URL variable must contain the full URL of a destination (e.g., http://www.zscaler.com), and the host variable must contain a domain name or its IP address (e.g., zscaler.com or 72.249.144.174). Generally speaking, the host variable is preferable to the URL variable. If no URL or host is specified, all web requests will use the return argument.

Return Statements

Enter a return statement in curly brackets after each argument. The return statement directs requests to a proxy or to the specified destination. For example:

functionFindProxyForURL(url, host)
{ return "PROXY ${GATEWAY}:9400"; return "DIRECT"; }

A return statement accepts one of two values:

  • DIRECT tells the browser to bypass the proxy and go directly to the destination server.
  • PROXY tells the browser to send the request to a proxy.

The DIRECT or PROXY values must be in uppercase characters and enclosed in quotation marks. Add a semicolon immediately after the closing quotation marks. For example:

return "DIRECT";
return "PROXY ${GATEWAY}:9400";

Each PROXY statement must specify the fully qualified host name or the IP address of the proxy and the port. IP addresses are generally discouraged because they can change at any time. Zscaler recommends that you use the variables ${GATEWAY} and ${SECONDARY_GATEWAY} instead. If your organization uses a subcloud, use the variables ${GATEWAY.<Subcloud>.<Zscaler cloud>.net} and ${SECONDARY.GATEWAY.<Subcloud>.<Zscaler cloud>.net}.

The Zscaler service uses its geolocation technology to automatically find the ZEN closest to you and with the quickest response time. Naming a primary and secondary gateway provides failover in case one of the ZENs is unavailable for any reason.

ZENs accept web requests on ports 80, 443, 9400, 9480 and 9443.

  • Port 80 is the standard port used by almost all web servers.
  • Port 443 is the standard port used for encrypted (HTTPS) traffic. Port 9400 can be used instead if another host between the end user and ZEN attempts to redirect the user’s traffic before it can reach the ZEN.

Ensure to allow port 9400 for outbound traffic destined to ZEN from the user end before adding the proxy entry to the PAC file. If port 9400 is blocked on the user end for outbound traffic, do not add the proxy entry for the port to the PAC file. This ensures that there is no latency due to the browser's multiple attempts to connect to the blocked port until it fails-over to a reachable port. 

  • Port 9443 can be used for road warriors who need the service to proxy and inspect HTTPS transactions.
  • Port 9480 can be used to exempt authentication for traffic that is destined to ZENs from a known location.   

Write the opening function FindProxyForURL(url, host) { } followed by a return statement on the next line. For example:

functionFindProxyForURL(url, host)
{ return "PROXY ${GATEWAY}:9400"; return "DIRECT"; }

Save the text file and test it. Verify that the PAC file sends your browser to the Zscaler service with no other arguments, ‘if’ statements, or other elements. Confirm by navigating to ip.zscaler.com, which indicates if you reached the web server directly or through a ZEN.

Adding Arguments

You can add various arguments that identify which internal or external hosts must be proxied. Add one nested argument at a time, and then test and confirm that the PAC file works properly after each addition.

Following is a sample PAC file that contains arguments, each of which is preceded by a comment.

functionFindProxyForURL(url, host)
{
//
//Exclude FTP from proxy
//
if (url.substring(0, 4) == "ftp:")
{
return "DIRECT";
}
//
//Bypass proxy for internal hosts
//
if (isInNet(host, "0.0.0.0", "255.0.0.0")||
isInNet(host, "10.0.0.0", "255.0.0.0") ||
isInNet(host, "127.0.0.0", "255.0.0.0") ||
isInNet(host, "169.254.0.0", "255.255.0.0") ||
isInNet(host, "172.16.0.0", "255.240.0.0") ||
isInNet(host, "192.0.2.0", "255.255.255.0")||
isInNet(host, "64.206.157.136", "255.255.255.255"))
{
return "DIRECT";
}
//
//Bypass proxy for this server
//
if (dnsDomainIs(host, "mail.domain.com")
{
return "DIRECT";
}
return "PROXY ${GATEWAY}:9400; ${SECONDARY_GATEWAY}:9400; DIRECT";
}

For the arguments in the sample PAC file above, specify the following:

Zscaler-Specific Variables

Listed below are the different Zscaler-specific variables you can use in your PAC file argument:

The following lines exclude FTP traffic from being redirected to the proxy (since the service does not support native FTP):

   //
   //Exclude FTP from proxy
   //
if (url.substring(0, 4) == "ftp:") 
    {
     return "DIRECT";
    }

The argument begins with a simple if statement using the built-in function called url.substring. If arguments are always followed by opening and closing parentheses—they describe the conditions under which the argument must be evaluated and for which a result is required. In the example, the argument specifies that if the URL contains the string ftp: in the first four characters of the URL (from 0 to 4), return DIRECT—bypass the proxy named in the line below. Note that the if argument’s result must be enclosed within its own set of open and closed curly brackets.

Save this change to the PAC file and upload it to the service portal. Reload the PAC file in your browser and navigate to an FTP download site such as ftp://ftp.hp.com/. After loading this page, log in to the Analytics portal to determine if this transaction was logged. The transaction should not appear in the logs.

The following lines in the PAC file example exclude requests for internal hosts from being redirected to a proxy.

//
 //Do not send traffic to the following network to Zscaler
 //
if (isInNet(host), "192.168.0.0", "255.255.0.0")
{
 return "DIRECT";
}

This argument uses the JavaScript function IsInNet(), which is typically used to identify either of the following:

  • Client IP address (If the request comes from this IP address, use this proxy.) Be aware that this argument returns the first IP address on your device, based on its OS. The first IP address, shown when you use the ipconfig command, may be the IPv6 address of the device or the IP address of virtual adapters and this can cause conflicts.
  • Host server IP address (If the request is going to this address, use this proxy.) Be aware that this argument results in a DNS lookup. It can impact performance if the DNS server is not available. Instead, you can use the following to constrain the IsInNet() function based on the host domains being accessed:
If dnsDomainIs(host, "internal.net") {
If (isInNet(host, "10.0.0.0”, "255.0.0.0")
Return "DIRECT";
}

Save the PAC file and test again. Browse to an internal host and ensure that you can reach it. (If you were proxied through a ZEN, your request would be denied. The request first goes outside your network to the Zscaler proxy but is then blocked as it tries to access an internal host as it comes back in from outside the network.)

The following lines in the PAC file example exclude requests for multiple internal hosts from being redirected to a proxy.

  //
  //Bypass proxy for internal hosts
  //
 if(isInNet(host, "0.0.0.0", "255.0.0.0")||
 isInNet(host, "10.0.0.0", "255.0.0.0") ||
 isInNet(host, "127.0.0.0", "255.0.0.0") ||
 isInNet(host, "169.254.0.0", "255.255.0.0") ||
 isInNet(host, "172.16.0.0", "255.240.0.0") ||
 isInNet(host, "192.0.2.0", "255.255.255.0")||
 isInNet(host, "64.206.157.136", "255.255.255.255"))
 {
  return "DIRECT";
 }

 As shown in the example, before closing the IF argument with a final parenthesis, the Boolean “or” operator (||) is used to concatenate the internal networks. The last isInNet statement does not require the or (||) operator. Instead, it uses an additional parenthesis to close the opening parenthesis.

The last statement is a specific IP address, not a network. To bypass specific hosts, write the exact IP address with a subnet mask of 255.255.255.255.

Use of isInNet() is extremely effective when the host you are trying to reach is an actual IP address. If you try to reach a host by domain name, such as https://zscaler.com your browser must perform a DNS lookup for it. If the host name is not resolvable, the client needs to wait for DNS to time out before moving on. To avoid this and to avoid placing an undue strain on the name server, insert a variable that uses a regular expression immediately above the isInNet() argument to restrict the result just to IP addresses.

Therefore, instead of writing:

if (isInNet(host, "0.0.0.0", "255.0.0.0")||
isInNet(host, "10.0.0.0", "255.0.0.0") ||
isInNet(host, "127.0.0.0", "255.0.0.0") ||
...)
{
  return "DIRECT";
}
Write this:
reip = /^\d+\.\d+\.\d+\.\d+$/g;
if (reip.test(host))
{
 if (isInNet(host, "0.0.0.0", "255.0.0.0")||
 isInNet(host, "10.0.0.0", "255.0.0.0") ||
 isInNet(host, "127.0.0.0", "255.0.0.0") ||
    ...)
 {
  return "DIRECT";
 }      
}

Here, the variable reip = /^\d+\.\d+\.\d+\.\d+$/g; is used in the argument if(reip.test(host)) {, and the result is only used if the host is an IP address in one of the  networks specified in the nested if argument. Note that this can only be used for URLs that include IP addresses, such as http://192.0.2.3/example.

In some versions of IE, use of the preceding variable may not work. An alternative to this is to use the JavaScript shell expression match function:

if (shExpMatch(host, "/^\d+\.\d+\.\d+\.\d+$/g")) 
{
 if (isInNet(host, "10.0.0.0", "255.0.0.0") ||
 isInNet(host, "192.168.0.0", "255.255.0.0")) 
 {
  return "DIRECT";
 }      
}

Save this change, reload the PAC file in your browser, and then try browsing to an internal web server in the internal network. If you can reach the server, you have bypassed the Zscaler ZEN.

The following lines in the PAC file example exclude specific servers, such as mail.domain.com, from being redirected to a proxy. In the example, a separate if isInNet() argument lists internal host names.

//Bypass proxy for this server //
if (dnsDomainIs(host, "mail.domain.com")
{ return "DIRECT"; }

//To bypass all hosts in a domain, use
dnsDomainIs(host, "host.com")
 
You can also use the following to bypass specific internal hosts:
var bypassHosts = /(remote\.mydomain\.com|mail\.mydomain\.com)/; 
if (bypassHosts.test(host)) 
{
 
  return "DIRECT";
}

 The preceding argument includes a variable that contains two hosts: remote.mydomain.com and mail.mydomain.com. Using the JavaScript test(host) function, any host you enter here will return DIRECT, and will not require a DNS lookup. var is the JavaScript function to set a variable. bypassHosts is a JavaScript function. You must use this specific name/function. Forward slashes mark the beginning and ending boundary of the variable. Open and close parentheses in the variable match the parentheses in the argument (test(host)). The periods in the host names must be “escaped” with a back slash. The variable itself requires a semicolon to close the variable argument.

You can use the ${GATEWAY} and ${SECONDARY_GATEWAY} variables to determine the ZEN closest to the client. For example:

return "PROXY ${GATEWAY}:80; PROXY ${SECONDARY_GATEWAY}:80; DIRECT";

The Zscaler service uses its geo-location technology to find the closest ZEN with the quickest response time. These variables provide the optimal user experience.

If your organization uses a subcloud, use the variables ${GATEWAY.<Subcloud>.<Zscaler cloud>.net} and ${SECONDARY.GATEWAY.<Subcloud>.<Zscaler cloud>.net}. For example:

return "PROXY ${GATEWAY.safemarch.zscalerbeta.net}:80; PROXY ${SECONDARY.GATEWAY.safemarch.zscalerbeta.net}:80; DIRECT";


If your organization has a large number of users behind a single egress IP address, you can configure the PAC file to choose from multiple gateway IP addresses to distribute load. To do this, use the following variables:

Zscaler recommends this method if you want specific users to go to specific gateway IP addresses.

Add the suffixes _F0 through _F7 to the ${GATEWAY} variable for the PAC server to return the eight healthy gateway IP addresses that correspond to each variable. For example, ${GATEWAY_F0} corresponds to the first IP address on the list of healthy IP addreses, ${GATEWAY_F1} corresponds to the second IP address, etc.

If the data center does not have eight healthy gateway IP addresses, it still returns healthy IP addresses for all gateway variables. However, it might reuse IP addresses.

This variable is applicable only to Zscaler App clients.

Use the suffix _FX for the PAC server to dynamically issue gateway IP addresses based on a client fingerprint, meaning all users coming from a single egress IP address are given an IP address from the pool of healthy gateway IP addresses. The fingerprint is used to ensure that a single device continues its session to the same gateway IP address. For example:

return "PROXY ${GATEWAY_FX}:80; PROXY ${SECONDARY_GATEWAY_FX}:80; DIRECT";

You can also use the _FX suffix with the subcloud variables, e.g, ${GATEWAY_FX.safemarch.zscalerbeta.net}.

The _FX suffix provides load balancing for multiple VIPs depending on the HTTP headers (useragent, x-forwarded-for, and z-client). This variable is used only for Zscaler App clients because the z-client ID will be different for each user.

You can use the ${SRCIP} variable to determine the client's public IP address. For example:

var egressip = "${SRCIP}";
if (shExpMatch(egressip,"203.0.113.10")) {
		/* User is in the office */
		return "PROXY 10.84.0.188:80;DIRECT";
}

You can use the ${COUNTRY} variable to determine the client's country. For example:

var country = "${COUNTRY}";
if (shExpMatch(country,"China")) { 
        /* User is in China */ 
        return "PROXY ${COUNTRY_GATEWAY}:80;DIRECT"; 
}

The ${COUNTRY} variable supports the countries listed on MaxMind. Anonymous Proxy, Satellite Provider, and Other Country are MaxMind-specific codes. Your IP addresses might not fall into those categories.

You can use the ${COUNTRY_GATEWAY} and ${COUNTRY_SECONDARY_GATEWAY} variables to determine the closest ZEN in the client's country. For example:

return "PROXY ${COUNTRY_GATEWAY}:80; PROXY ${COUNTRY_SECONDARY_GATEWAY}:80;"

If your organization uses a subcloud, use the variables ${COUNTRY_GATEWAY.<Subcloud>.<Zscaler cloud>.net} and ${COUNTRY_SECONDARY.GATEWAY.<Subcloud>.<Zscaler cloud>.net.}. For example:

return "PROXY ${COUNTRY_GATEWAY.safemarch.zscalerbeta.net}:80; PROXY ${COUNTRY_SECONDARY.GATEWAY.safemarch.zscalerbeta.net}:80; DIRECT";

This prevents clients that are near multiple ZENs or in countries that are close to one another from having their traffic forwarded to the wrong ZENs.