What Turnout does to your Mac

Turnout installs a service that runs as root, so here is an exact account of what it does and how to check it: what the service is, what it changes, what it keeps and what it connects to, each with a way to see it on your own Mac.

Two parts: an app and a background service

The app, Turnout.app, runs as you, with no special privileges. It shows your tunnels, edits configs and asks the background service to connect and disconnect them. It never touches your network traffic.

The background service is a LaunchDaemon, cc.ctsite.turnout.daemon, that runs as root. Its program, turnout-daemon, sits inside the app in /Applications/Turnout.app/Contents/MacOS/. Turnout registers it through macOS's Login Items (SMAppService), so macOS asks you to allow it in System Settings › General › Login Items & Extensions, and you can switch it off there.

The service creates each tunnel's utun interface and runs WireGuard in userspace with GotaTun, an implementation by Mullvad under the MPL-2.0 license, built into the service. Only the tunnel's packets pass through it; the rest of your traffic is routed by macOS as usual. Tunnels stay up when the app is closed, because they live in the service.

What it changes, and with which tools

Addresses, MTU, routes and DNS are changed with the programs wg-quick uses on macOS. Each is run directly with its arguments, never through a shell.

/sbin/ifconfig
The tunnel interface's addresses and MTU, and bringing it up.
/sbin/route
Routes into the tunnel, the route that keeps the server's address on your physical network, and routes that skip the tunnel.
/usr/sbin/networksetup
DNS servers and search domains on your network services.

So the usual commands show exactly what it did:

netstat -rn
networksetup -listallnetworkservices
networksetup -getdnsservers Wi-Fi   # or another service from the list

When a tunnel disconnects, the service removes the routes it added and puts back the DNS settings it replaced.

Your configs

Tunnels are saved in /Library/Application Support/cc.ctsite.turnout/tunnels, a folder that belongs to root, one file per tunnel with mode 0600, private keys included. Only the service writes there; the app asks it to save.

sudo ls -l "/Library/Application Support/cc.ctsite.turnout/tunnels"

Hooks

PreUp, PostUp, PreDown and PostDown run as root, through /bin/bash -c, with %i replaced by the tunnel's utun interface and %I by the tunnel's name. That is as much power as sudo, so:

  • Hooks only ever come from a config file on disk, never from anything the app sends to the service.
  • Saving a config whose hooks are new or changed needs an administrator's approval. The service checks that approval itself, with macOS Authorization Services; it does not take the app's word for it.
  • Edits that leave the hooks alone need no new approval.

Read a config's hooks before you approve them, as you would a script you run with sudo.

Who the service answers

The app talks to the service over a Unix socket, /var/run/cc.ctsite.turnout.daemon.sock. For every program that connects, the service checks its code signature and answers only a program signed by the same developer team with Turnout's bundle identifier, cc.ctsite.turnout. Other programs are refused.

After a crash

When the service stops, macOS removes the tunnel interface, and the routes on it go with it. Routes through your physical gateway and DNS settings do not, and DNS set this way even survives a restart. So the service writes every change to a journal before it makes it, and at its next start undoes whatever a previous run left behind. Until that is done it connects no tunnel, and Turnout shows what is still waiting, with Restore Now.

Turnout › Repair Network Settings… does the same on request. If the service is not running, it asks for an administrator password and replays the journal itself.

Network connections

For updates, the app itself connects to two places: GitHub, to see whether there is a new version, and Google Analytics, to count the installation. For the license it connects to Polar, once you have bought one, and to your iCloud account; The license below says when, and with what.

  • Only the copy in /Applications checks on its own, at most once an hour, when you open the menu or a window. Turnout › Check for Updates… checks whenever you choose it.
  • The check asks for a small file in this project's latest release on GitHub. It carries no account and nothing about you or your tunnels. GitHub sees the request and your IP address.
  • With each check, the app sends one event to Google Analytics (www.google-analytics.com, through its Measurement Protocol): a random installation ID that Turnout makes up on first use and keeps in its preferences, never derived from the hardware; the Turnout version and build; the macOS version; the processor architecture; the system language; and two fields Google Analytics needs to count it, a session number taken from when Turnout was started and a fixed engagement time. Nothing about tunnels, servers, keys or networks, and no account or user ID. Google sees the request and your IP address, under the Google Privacy Policy.
  • Switching off Check for updates automatically in General (Turnout › Settings…) stops both the automatic check and the report. Check for Updates… still checks when you choose it, and sends no report.
  • If you install an update, the installer comes from the same release, and Turnout checks its checksum and its Developer ID signature before handing it to the macOS installer.

Your tunnels connect to the servers in your configs, and your hooks do what you wrote in them. Apart from that, neither the app nor the service connects anywhere: no crash reporting of its own, no advertising, no account.

The license

Turnout runs in full for a 7-day trial on each Mac, with 7 more days once if you ask; after that, connecting a tunnel needs a license. Only connecting: tunnels that are connected stay connected, and disconnecting, editing, importing, exporting, Repair Network Settings… and uninstalling always work. Nothing is ever disconnected for licensing reasons.

Licensing lives in the app. The background service knows nothing about it, and never connects to Polar or iCloud.

  • No license check runs during the trial. The trial's dates are kept in this Mac's keychain, in an item named cc.ctsite.turnout.license, which stays when Turnout is uninstalled, so installing again does not start a new trial.
  • Once you have bought, the app calls Polar's license service, api.polar.sh, when you activate this Mac (the key, the name you give the Mac, its model identifier, the macOS and Turnout versions, and a one-way hash of its hardware identifier); about once a week when you open the menu or the window, and whenever you choose Check Now (the key, this Mac's activation ID and the hash); and when you remove a Mac (the key and that Mac's activation ID). No account, and nothing about your tunnels.
  • If Polar cannot be reached, nothing changes: a licensed Mac keeps connecting, however long it stays offline, and the License pane only says when the license was last confirmed.
  • The key and the list of Macs using it are kept as records in Turnout's area of your iCloud account (CloudKit), so your other Macs signed in to the same account find the license and use it with one click. The app reads them when it starts and when you open its menu or window, and writes them only after an activation, a check or a removal. They are stored with Apple's standard iCloud protection, not end-to-end encrypted. A Mac that is not signed in to iCloud syncs nothing; there, you enter the key by hand.

Everything that is sent, and to whom, is listed in the privacy policy; the terms are on License terms.

Location and Wi-Fi rules

A Wi-Fi rule (#@ OnWiFi) needs the name of the network the Mac is on. macOS counts that name as location, so only an app with Location access can read it. Turnout asks for access when you set up a Wi-Fi rule, never at launch, and uses it only for the network's name, never for where you are. The names you list stay in the config file on your Mac. Without access, Wi-Fi rules do nothing.

Logs

/Library/Logs/cc.ctsite.turnout.daemon.log
What the background service did, and its errors.
/Library/Logs/cc.ctsite.turnout.update.log
Written when an update is installed.

Help › Show Logs in Finder shows both files. The app's own messages are in the system log:

/usr/bin/log show --predicate 'subsystem == "cc.ctsite.turnout"' --last 1h

The full path matters in zsh, where a plain log is a different, built-in command.

Look for yourself

launchctl print system/cc.ctsite.turnout.daemon   # the service, as launchd sees it
ls -l /Applications/Turnout.app/Contents/MacOS/    # Turnout and turnout-daemon
cat /Applications/Turnout.app/Contents/Resources/uninstall.sh
defaults read cc.ctsite.turnout analyticsClientID  # the random installation ID
netstat -rn
networksetup -getdnsservers Wi-Fi

The uninstaller is a shell script inside the app. Read it before you run it.

Uninstalling

There are three ways, and they all run that same script:

  • With Turnout's window open, choose Turnout › Uninstall Turnout…. In the confirmation, Also delete saved tunnels and their private keys deletes them too. On a licensed Mac, it first takes this Mac off your license and removes its record from your iCloud account, unless you choose otherwise in the confirmation.
  • Open the installer and, where it lists what to install, choose Remove Turnout, and Delete saved tunnels if you want that as well.
  • In Terminal, adding --purge to delete saved tunnels as well:
    sudo sh /Applications/Turnout.app/Contents/Resources/uninstall.sh

Each one disconnects every tunnel, which restores routes and DNS; takes the background service out of Login Items; undoes anything a crashed service left behind; and deletes the app, its logs, the installer's receipt, and every user's Turnout preferences, saved window state and caches. Saved tunnels and their private keys, in /Library/Application Support/cc.ctsite.turnout, stay unless you choose to delete them.

The license item in the keychain stays, so installing again does not start a new trial, and the key stays in your iCloud account for your other Macs. The installer and the Terminal script run as root, without your keychain or iCloud account, so they cannot take this Mac off your license: do that first in the License pane, or afterwards from another Mac or in Polar's customer portal.