DNS in PAC files: why isInNet and dnsResolve slow every request

Published

The most common performance problem in PAC files is a line that looks like simple arithmetic, isInNet(host, "10.0.0.0", "255.0.0.0"). When host is a name, this is a DNS lookup, and it runs before every connection.

What happens

FindProxyForURL runs before every connection. The helper functions dnsResolve, isResolvable and isInNet with a host name each ask the resolver and wait for the answer (PAC-P001):

  • normally that is tens of milliseconds added to every first byte;
  • on a degraded resolver it is seconds;
  • on a broken one, the request hangs.

isInNet(host, …) hides the lookup. In Chromium’s helper library it calls dnsResolve whenever its first argument is not already an IP address (source: code, verified 2026-10-04; PAC-P002).

The engines wait in different ways (source: code, verified 2026-10-04, unless marked):

EngineDNS in the PAC
Chromiumasynchronous; the script may run again once the answer arrives (expert, unverified)
Firefoxsynchronous, blocks the PAC thread
pacparsersynchronous, blocking getaddrinfo

Why it also fails open

When resolution fails, dnsResolve returns an empty value and isInNet returns false. The rule simply does not match, and the request takes whatever comes next, normally the default route (PAC-X010). On a hostile network the resolver answer is controlled by the attacker, so the routing decision is too. If a name has several addresses, only one of them is compared.

The fix: decide on the string first

Most PAC files can decide everything from the host name. Reorder so that cheap string tests come first (PAC-P005), and use address tests only for IP literals:

function FindProxyForURL(url, host) {
  host = host.toLowerCase();
  // 1. names: string tests, no DNS
  if (isPlainHostName(host) || dnsDomainIs(host, ".corp.example") || host == "corp.example") {
    return "DIRECT";
  }
  // 2. IP literals only: isInNet on a literal does not resolve anything
  if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) {
    if (isInNet(host, "10.0.0.0", "255.0.0.0") ||
        isInNet(host, "172.16.0.0", "255.240.0.0") ||
        isInNet(host, "192.168.0.0", "255.255.0.0")) {
      return "DIRECT";
    }
  }
  // default route
  return "PROXY proxy1.corp.example:8080; PROXY proxy2.corp.example:8080";
}

If you really must route by the resolved address, resolve once, keep the result in a variable, and do it after all string rules (PAC-P004):

var ip = dnsResolve(host);
if (ip && isInNet(ip, "10.0.0.0", "255.0.0.0")) {
  return "DIRECT";
}
  • isInNet("10.0.0.0", host, "255.0.0.0"): arguments in the wrong order, always false (PAC-C016).
  • An invalid network or mask such as 10.0.0 or 255:255.255.0: always false (PAC-E010).
  • IPv6 networks in isInNet never match. isInNetEx exists only in some engines (PAC-K014).
  • isResolvable(host) as an “is it internal” test: it answers a different question (PAC-P003).

Check your file

The online PAC file tester flags every DNS call on the request path and its position in the file. Set DNS answers in the tester to see how each rule behaves when a name resolves, and when it does not.

Frequently asked questions

Does isInNet do a DNS lookup?

Yes, when the first argument is a host name. isInNet resolves it first and then compares the address with the network and mask. Given an IP address it does no lookup.

Why is my PAC file slow?

Usually because it calls dnsResolve, isResolvable or isInNet on a host name for every request. Each call waits for the DNS resolver before the browser can open the connection. Decide on the host name string first and run DNS rules only for the few requests that need them, or not at all.

What happens when DNS fails inside a PAC file?

dnsResolve returns an empty value and isInNet returns false, so the request skips the rule and takes the next one, usually the default route. Routing then depends on the resolver, which fails open.