A LaunchAgent for Automatic Proxy Configuration

In the past, I’d shudder just hearing the words, “You’ll need to be on the VPN for that.” But thanks to a couple of blog posts by Joe Chrysler, I no longer bat an eye at the thought of needing to connect to a VPN throughout my workday.

In How to Keep Home Stuff off Your Work VPN while Social Distancing Joe talks about installing the VPN software in a Virtual Machine and proxying traffic to the VM using an SSH tunnel. And even better, in A Selective VPN for Mobile App Development, he shows how to use openconnect with ocproxy to create a VPN connection that doesn’t take over your whole machine. And, it can be run from the command line.

However, the biggest game-changer from those posts is the work he did to figure out the Automatic Proxy Configuration on macOS. With that in place, nearly all the software you need to use with the VPN will automatically send only the required traffic over the VPN connection, without any app-specific configuration needed.

One Problem with Automatic Proxy Configuration

I’ve been using an Automatic Proxy Configuration setup like that for nearly a year now, ever since I read Joe’s post. The only issue I’ve had with it is when the HTTP server is serving up the PAC file. (Joe talks about running an instance of miniserve, so that’s what I’ve been using too.) If that server isn’t running when an application first starts, it appears to ignore the proxy configuration.

For example, say I reboot my laptop and immediately start Chrome or Firefox before I run the PAC miniserve server. That means I’ll have to later quit Chrome or Firefox, start the miniserve instance, and then run the browser again for it to use the proxy configuration. That’s not the end of the world, but I tend to have a lot of web pages open, many of them somewhat stateful. It bugs me when I’m forced to close everything down when I’m in the middle of trying to get something done.

The answer I found is to start the PAC HTTP server automatically on startup. That way it’s always listening and ready to serve up the PAC file for any application, whenever it’s needed.

Working Directory

One of the areas I ran into trouble with was the working directory of the script. There are several ways to deal with this. However, the route I took was to modify the script that runs miniserve so that, no matter where it’s run from, it changes to the desired working directory before starting the server:


# start-pac-server

#!/usr/bin/env bash

# Serves a Proxy Auto-Config file so that you can configure
# macOS, iOS, and Android devices to send project traffic
# through the VPN without forcing every device to have its
# own independent VPN connection.

WEB_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd $WEB_ROOT

PROJECT_PAC_SERVER_PORT=8090

miniserve config \
  --port "$PROJECT_PAC_SERVER_PORT"

NOTE: I’ve also modified Joe’s script to serve up a config directory located in the same directory as the script. Joe designed his original script to be project-specific. Meanwhile, I’m using it more generically, with a single PAC configuration that supports all of my proxying needs.

Launch Agent

When you want a service to start automatically in macOS, you can either configure a LaunchAgent or a LaunchDaemon. According to this StackExchange answer, a LaunchAgent starts up when a user logs into a graphical session. That seemed to suit my needs perfectly, so that’s the route I took.

There are two steps to setting up a LaunchAgent. First, you need to create a plist file that defines the agent, and then load that plist using launchctl.

Here’s the ~/Library/LaunchAgents/com.atomicobject.proxypac.plist file I created:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<plist version="1.0">
  <dict>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:</string>
    </dict>
    <key>ProcessType</key>
    <string>Background</string>
    <key>Label</key>
    <string>com.atomicobject.proxypac</string>
    <key>Program</key>
    <string>/Users/baconpat/pac-proxy/start-pac-server.sh</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>LaunchOnlyOnce</key>
    <false/>
    <key>StandardOutPath</key>
    <string>/tmp/pacproxy.stdout</string>
    <key>StandardErrorPath</key>
    <string>/tmp/pacproxy.stderr</string>
    <key>UserName</key>
    <string>baconpat</string>
    <key>GroupName</key>
    <string>staff</string>
    <key>InitGroups</key>
    <true/>
  </dict>
</plist>

There are a few things to note here:

  • In order for the homebrew installed miniserve executable to be found, the homebrew bin directory needs to be added to the PATH.
  • I needed to specify the full path to the start-pac-server.sh script (which also needs to be executable).
  • To help with debugging, I configured it to send stdout/stderr to files in /tmp.
  • The combination of KeepAlive and LaunchOnlyOnce means the process can be killed or crash, and Launchctl will automatically start it up again.
  • I don’t know if the label needs to match the name of the plist file. But, I made them match, and it works, so I’m not messing with it.

And finally, to tell Launchctl about the new Launch Agent, run the following command:


launchctl load -w com.atomicobject.proxypac.plist

To make sure the process is running you can check it with this:


ps aux | grep -v grep | grep miniserve

A LaunchAgent for Automatic Proxy Configuration

Now, when you reboot your computer and log back in, the PAC server will start immediately. That means any applications that request the Automatic Proxy Configuration from the OS will be told where to proxy requests for various domains.