VPN Gate client for MacOS / Linux
There is not official GUI client for connecting with MacOS or Linux
- VPN connection lifecycle (connect, disconnect, retry)
- Saved server profiles
- Status bar management
- Improve UI
- Release and troubleshooting documentation
- Clone repo
- Navigate to repo
- Install packages
npm i - Run program
npm run start
The connection panel is authoritative: use its state and failure message rather than assuming a click succeeded. Server-row Connect buttons are enabled only while disconnected or failed. The available lifecycle actions are:
| State | Visible label | Meaning | Available action |
|---|---|---|---|
disconnected |
Disconnected | No connection attempt is active. | Connect from a server row. |
authorizing |
Authorizing | The operating system authorization flow is active. | Disconnect to cancel. |
connecting |
Connecting | OpenVPN started and is negotiating the tunnel. | Disconnect. |
connected |
Connected | The authenticated management channel reported the tunnel up. | Disconnect. |
disconnecting |
Disconnecting | Portlet is stopping the current attempt. | None; wait for Disconnected. |
reconnecting |
Reconnecting | An unexpected loss has a retry pending. | Disconnect to cancel recovery. |
failed |
Failed | The displayed failure stopped the attempt or exhausted retries. | Reconnect the same server, or Connect to another server. |
The automatic retry setting defaults to 1; the supported range is 0 through
5, and the saved setting persists across launches. Failures before a managed
OpenVPN attempt starts—including authorization, invalid configuration,
and executable discovery or version validation—become failed and do not
consume automatic retries. A launcher failure can occur before launch completes
or before the first connected signal; it also becomes failed without an
automatic retry. A management-channel loss after a managed attempt starts is
retryable, whether the UI has reached connecting or connected. To avoid
promising recovery during startup, this guide treats an unexpected OpenVPN
process exit as retryable only after the first connected signal. Authorization
cancellation and denial are not retried. A missing or unsupported OpenVPN
executable is not retried.
Disconnect during authorizing, connecting, connected, or reconnecting cancels the active attempt and any pending automatic retry. Reconnect retries the same server after a failure and resets the automatic-retry count. The displayed attempt number counts connection/authorization attempts. It increments before authorization and launch, so it does not prove that an OpenVPN process started; the first automatic retry normally shows attempt 2. Events are tied to their attempt: stale attempt events are ignored and a late initial status snapshot cannot replace a newer status event.
Portlet does not publish disconnected, start a retry, or start a replacement
attempt until the previous privileged OpenVPN process has confirmed exit. If
termination cannot be confirmed within the bounded helper deadline, the state
becomes failed and the application remains open during an attempted quit so it
cannot silently leave a privileged tunnel behind. At startup, Portlet removes
only current-user stale session directories whose private layout, ownership,
file types, and modes validate exactly; an unvalidated matching path fails
startup without being deleted.
Portlet displays the safe failure message in the connection panel. Fix the cause before selecting Reconnect; repeated Connect requests while a connection is active do not start another process.
| Failure code or symptom | Actual behavior | Operator/user response |
|---|---|---|
Startup/early failure: authorization-cancelled, authorization-denied, invalid-config, openvpn-not-found, or openvpn-unsupported |
Failure occurs before a managed OpenVPN attempt starts; state becomes failed and the failure does not consume automatic retries. |
Resolve the displayed cause, then use Reconnect or select another server. For missing system OpenVPN, install it at a supported path; for authorization failure, accept the next operating-system prompt. |
launch-failed |
Launcher failure can occur before launch completes or before the first connected signal; state becomes failed with no automatic retry. |
Verify the signed macOS bridge/helper or Linux pkexec, helper, policy, and graphical authentication agent, then use Reconnect. |
management-failed after a managed attempt starts |
State becomes reconnecting while retries remain. |
Wait for recovery or Disconnect to cancel it. |
Unexpected OpenVPN process exit after the first connected signal |
State becomes reconnecting while retries remain. |
Wait for recovery or Disconnect to cancel it. For a deterministic release test, wait for connected before forcing each process loss. |
retry-exhausted |
State becomes failed. |
Correct the underlying tunnel problem, then use Reconnect. |
The operating system owns every privileged prompt. There is no Portlet password
field: never type a system password into Portlet. If no system prompt appears,
do not work around authorization with sudo; verify the installed privileged
infrastructure described below.
Run this matrix on every installer and OpenVPN variant intended for release. Record the observed states rather than marking a scenario passed from its final screen alone.
| Scenario | Test operation | Expected visible state sequence | Expected result and user action |
|---|---|---|---|
| Successful connect | Select a reachable server and approve the system prompt. | authorizing → connecting → connected |
Tunnel traffic is usable; Disconnect remains available. |
| User cancels authorization | Select Connect, then cancel the operating-system prompt. | authorizing → failed |
Cancellation is displayed and is not retried; use Reconnect to try again. |
| System OpenVPN absent | On a disposable test image with no OpenVPN installed at any supported path, select Connect. | authorizing → failed |
openvpn-not-found is displayed; Install OpenVPN, then use Reconnect. |
| Intentional disconnect | Select Disconnect from an active connection. | connected → disconnecting → disconnected |
No automatic retry starts; use Connect for a new session. |
| Forced unexpected OpenVPN exit | Terminate the active OpenVPN process outside Portlet. | connected → reconnecting |
A configured retry is scheduled; Disconnect to cancel recovery. |
| One successful automatic reconnect | Restore the test network before the first retry completes. | reconnecting → authorizing → connecting → connected |
The attempt number advances and the tunnel works; Disconnect remains available. |
| Retry exhaustion | Set automatic retries to 1. After each attempt reaches connected, terminate its OpenVPN process: once to start the retry and again after the retry connects. |
connected → reconnecting → authorizing → connecting → connected → failed |
The second post-connected loss displays retry-exhausted; repair the cause and use Reconnect. |
| Restart after an unclean exit | Force-quit Portlet while connected, then reopen it. | loading → disconnected |
Inspect for an orphaned OpenVPN process and stale private session directories before using Connect; any leftover blocks release. |
| Bundled/system build startup | Launch each installed release variant, then test Connect. | loading → disconnected |
Startup is idle because executable discovery is lazy; Connect must exercise the correct bundled or system path. |
| Stale event isolation after reconnect | After a successful retry, induce or replay a late exit from the old attempt. | connected |
The newer status remains connected; Disconnect remains available. |
For each fresh-install run, confirm that the first Connect produces the system
authorization prompt, Portlet contains no password field, Disconnect cancels a
pending retry, and stale events never replace the newer connected attempt. Run
forced-exit and unclean-restart cases only on a disposable test machine. After
the unclean restart scenario, inspect both the process list and Portlet's
vpn-sessions directory; the current automated suite does not substitute for
this machine-level observation. Capture evidence using
the release verification record, without
capturing credentials or generated OpenVPN configuration.
npm run package creates an unsigned, developer-only application directory.
It deliberately omits the signed macOS bridge and helper or the installed Linux
helper and Polkit policy, so its output is not VPN-capable. Use it for local UI
and lifecycle development, never as a release artifact.
VPN-capable release packaging supports a signed macOS ZIP with system OpenVPN,
plus Debian (.deb) and RPM (.rpm) packages with system or bundled OpenVPN.
The named release commands set PORTLET_RELEASE_BUILD=1; Forge rejects any
other make attempt before it creates a misleading developer artifact.
Privileged launchers are built as native x64 and arm64 binaries. On both
macOS and Linux, the requested release architecture must match the build host;
cross-architecture builds are rejected before packaging. macOS
universal release packages are not supported because the current helper build
does not produce or verify a universal native launcher. Build each supported
architecture on a matching CI runner instead.
Run npm run make:system to build release artifacts that discover OpenVPN on
the host. OpenVPN must already be installed, executable, root-owned, not
group- or world-writable, and report a supported version. On macOS it must also
be a regular file rather than a Homebrew-style symlink; this is checked before
the authorization prompt. The system build searches these locations in order:
- macOS:
/opt/homebrew/sbin/openvpn,/usr/local/sbin/openvpn,/usr/sbin/openvpn, and/usr/bin/openvpn - Linux:
/usr/sbin/openvpn,/usr/bin/openvpn, and/usr/local/sbin/openvpn
Run npm run make:bundled on Linux only after release automation supplies a
vetted, supported executable at this exact path:
resources/openvpn/linux/openvpn
The file must be executable. Release-supplied binaries are ignored by Git and copied into the packaged application only for bundled builds. The Linux package installer establishes the privileged launcher ownership described below.
macOS bundled OpenVPN release packaging is intentionally rejected. A ZIP cannot
make resources/openvpn/darwin/openvpn (or the corresponding path below
/Applications/portlet.app) root-owned, so it cannot satisfy the privileged
helper's trust boundary. Use npm run make:system with a root-owned regular
OpenVPN executable until an installer-managed bundled path is implemented.
The operating system owns the authorization prompt; Portlet never asks for, receives, or stores the user's system password.
On macOS, release packaging requires a valid Developer ID application identity
in PORTLET_APP_SIGNING_IDENTITY and its ten-character team identifier in
PORTLET_APP_TEAM_ID. Forge signs the application, embeds the
SMPrivilegedExecutables requirement, then stages the signed helper at
Contents/Library/LaunchServices/com.benbousquet.portlet.vpn-helper and the
signed authorization bridge at
Contents/Resources/native/portlet-vpn-bridge. The bridge asks SMJobBless to
install the helper when the VPN starts. Release smoke tests install the signed
application at /Applications/portlet.app. The bridge and helper accept only
fixed system OpenVPN paths, and the helper independently requires a root-owned
regular executable that is not group- or world-writable. An unsigned
development ZIP cannot satisfy these requirements.
On Linux, release packaging compiles the narrow native helper before packaging.
The Debian and RPM lifecycle scripts require pkexec, install
/usr/libexec/portlet-vpn-helper as root:root and mode 0755, and install the
policy at /usr/share/polkit-1/actions/com.benbousquet.portlet.vpn.policy as
root:root and mode 0644. Installation fails if either packaged asset is
missing or its final metadata cannot be verified. A graphical Polkit authentication agent
is also required so the desktop can show the system authorization prompt.
Do not set PORTLET_RELEASE_BUILD=1 by hand to bypass prerequisites. Use
npm run make:system or npm run make:bundled; missing compilers, signing
credentials, native assets, or package tools are release failures.
Flatpak packages are not VPN-capable builds because their sandbox cannot install or execute Portlet's required host-privileged OpenVPN launch path.