đ Proxy Auto-Configuration (PAC)
â 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, orSOCKS5directives,PROXYandDIRECTare 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
PROXYandDIRECTdirectives.
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:
- Manual URL: You enter the address (e.g.,
http://config.rama.internal/proxy.pac) into the System Proxy settings. - 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 downloadhttp://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
FindProxyForURLExand the other IPv6-aware helpers. Microsoft defines all six helpers; Chromium defines five (omittinggetClientVersion), and Firefox defines none, so a script cannot assume the full set exists everywhere.