Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🔀 Proxy Auto-Configuration (PAC)

A Proxy Auto-Config (PAC) file is a JavaScript function that determines whether web browser requests (HTTP, HTTPS, and FTP) go directly to the destination or are forwarded to a web proxy server.

— Wikipedia

The “Smart” Routing Script

While static system proxies are simple (everything goes to one IP), they lack nuance. In a real-world network, you don’t want to send traffic to your internal printer or a local dev server through a remote Rama proxy. You need a way to say: “Proxy the internet, but stay direct for the office.”

This is exactly what a PAC file does. It is a single JavaScript file containing a function called FindProxyForURL(url, host). Every time your browser wants to load a resource, it runs this function to get instructions.

The client passes two arguments to this function:

  • url: The full destination URL (e.g., https://example.com:8443/foo/bar?baz=1).
  • host: The host component extracted from the URL (e.g., example.com).

The function returns a string instructing the client on how to proceed:

  • DIRECT: Connect to the destination server directly, bypassing the proxy.
  • PROXY host:port: Connect via the specified HTTP proxy.

Note

While some clients support SOCKS, HTTPS, or SOCKS5 directives, PROXY and DIRECT are the most universally compatible across all platforms and browsers.

Unless you really need to and know for sure it is supported it is best to only use the PROXY and DIRECT directives.

You can return multiple options separated by a semicolon (;). The client will attempt them in order.

Script Example

The power of PAC lies in its simplicity. It provides a set of helper functions that allow you to make decisions based on the destination.

function FindProxyForURL(url, host) {
    // 1. Stay direct for local hostnames
    if (isPlainHostName(host) || dnsDomainIs(host, ".local")) {
        return "DIRECT";
    }

    // 2. Stay direct for the internal corporate network
    if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0")) {
        return "DIRECT";
    }

    // 3. Send everything else to our Rama proxy
    // If the proxy is down, the browser will try to go DIRECT as a backup
    return "PROXY proxy.rama.internal:8080; DIRECT";
}

Key PAC Functions:

  • isPlainHostName(host): True if there are no dots in the hostname (e.g., http://intranet).
  • dnsDomainIs(host, ".com"): Allows for domain-specific routing.
  • isInNet(ip, pattern, mask): Allows for IP-range based routing.
  • shExpMatch(str, shellExpression): Pattern matching for URLs using shell-style wildcards.

Distribution: How Browsers Find the File

A PAC file isn’t much use if the client doesn’t know where it is. There are two primary ways to distribute a PAC script:

  1. Manual URL: You enter the address (e.g., http://config.rama.internal/proxy.pac) into the System Proxy settings.
  2. WPAD (Web Proxy Auto-Discovery): This is the “zero-config” method. The browser uses DNS or DHCP to look for a server named wpad. It then tries to download http://wpad/wpad.dat. While convenient, WPAD has significant security risks (like DNS poisoning), which is why many modern environments prefer manual URLs or MDM-pushed configs.

Learn more about WPAD at https://en.wikipedia.org/wiki/Web_Proxy_Auto-Discovery_Protocol.

System proxy settings is the more common way to use PAC.

The Limitations of PAC

As flexible as PAC files are, they share the same weakness as all System Proxies: Application Compliance.

A PAC file is a suggestion, not a law. Furthermore, because PAC relies on JavaScript, many non-browser applications (like low-level CLI tools or embedded devices) don’t have a JavaScript engine to run the script. For those “blind” applications, the PAC settings are essentially invisible.

Rama Support

Rama both evaluates PAC scripts (route your own client’s traffic the way a PAC file says) and generates them (hand a PAC file to clients you do not control). It lives in rama::js::pac, behind the pac feature.

Evaluating means a request’s URL goes through the script’s FindProxyForURL and the resulting proxy list is attached to that request, for rama’s connector stack to dial through — trying each proxy in the order the script listed and falling back to the next when one is unreachable. DIRECT becomes an explicit “no proxy” route rather than an absent one. Scripts are compiled once and evaluated per request on a dedicated worker thread, bounded in wall-clock time, so a hostile or runaway script cannot hold up the client. Where the script comes from is left open: bundled with your configuration, fetched over http, read from a file, cached with a TTL, or anything else you can express as a rama service.

The full set of PAC host functions is available, including Microsoft’s six IPv6-aware extensions. Chromium defines five of them (not getClientVersion), while Firefox defines none, so they can be left undefined for a deployment that wants only the classic surface. Rama also follows WinHTTP by preferring FindProxyForURLEx when it is available. Name resolution goes through rama’s own DNS stack rather than a second one. Two choices here are worth being aware of because they are about what a script gets to see: https URLs are stripped down to their origin before the script sees them (a proxy decision needs the origin, not which page someone visited), and myIpAddress tells a script something about your network topology. Both are configurable, and default to what browsers do.

shExpMatch reads its pattern the way browsers do — as a regex under the covers, so a pattern like vpn[0-9].corp.example means there what it means here. That inheritance includes the sharp edges: an unparenthesised | anchors only one of its branches, and a bracket expression written literally (an IPv6 address, say) is a character class. A deployment that would rather have a pattern mean exactly what it spells can opt into literal matching.

A script also cannot decide how much work its request is worth. Rama’s execution time limit only reaches JavaScript, not the native work a helper function does, so name resolution and glob matching carry per-request budgets of their own; exhausting one fails that request rather than quietly answering “no match”, since the latter is how a rule gets bypassed. The number of proxies a verdict may name is bounded the same way.

Note that a PAC script only ever names where to connect. It does not decide how rama talks to that proxy: socks5 DNS behaviour, credentials, TLS, and connection reuse all remain the connector’s business.

WPAD auto-discovery (DHCP option 252 / DNS wpad) is not implemented — point rama at a script URI explicitly. SOCKS/SOCKS4 directives are skipped, as rama has no SOCKS4 support.

See the rama::js::pac module docs for the API, and http_pac_client for a client that routes through a generated script.

Evaluate and Generate PAC Files

The rama pac command evaluates existing scripts and generates simple, ordered domain policies. A positional PAC source is read from an existing file, or treated as inline JavaScript otherwise. Use -, --stdin, --file, or --source when the source should be explicit.

Evaluate one or more URIs immediately. As with rama send, http is assumed when a scheme is omitted; results report the completed, canonical URI:

rama pac eval proxy.pac \
  www.example.com \
  https://service.corp.example/

With a named source, newline-delimited URIs can come from stdin:

printf '%s\n' https://a.example/ https://b.example/ | rama pac eval proxy.pac

Without URI arguments in a terminal, eval opens a line-oriented REPL with cursor editing and in-session history navigation. Its :help command lists source reload, realm reset, and URL-sanitization controls. Batch output supports text, json, and jsonl; --offline disables DNS and local-interface disclosure, while --fresh uses a new JavaScript realm for every URI instead of preserving script-global state.

Generate a PAC file from first-match-wins rules:

rama pac generate \
  --route 'exact:health.corp.example=DIRECT' \
  --route '*.corp.example=PROXY proxy.example:8080; DIRECT' \
  --default 'DIRECT' \
  --output proxy.pac

Ordinary domains match themselves and their subdomains. Prefix a route’s domain list with exact: when only those hosts should match. Repeat --route in precedence order; comma-separate domains that share the same directive list. Existing output files are not replaced unless --force is passed.

More Resources

  • MDN: Proxy Auto-Configuration: Excellent documentation on built-in helper functions (like shExpMatch). Note: These functions can be slow; Safechain often uses optimized custom logic instead.
  • Cloudflare: PAC Best Practices: Modern performance tips.
  • Microsoft: WinHTTP IPv6 Extensions: Information on FindProxyForURLEx and the other IPv6-aware helpers. Microsoft defines all six helpers; Chromium defines five (omitting getClientVersion), and Firefox defines none, so a script cannot assume the full set exists everywhere.