Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

morti

morti

A clean OpenConnect wrapper for Fortinet VPN on macOS.
morti handles login, TOTP generation, reconnects, monitoring, and disconnect flow without the usual manual steps.

macOS Shell License


Why morti

  • Connect to a Fortinet VPN through OpenConnect
  • Generate TOTP codes automatically from your secret
  • Read passwords and TOTP secrets from Apple Keychain or 1Password
  • Retry failed connections and monitor the tunnel in the background
  • Restore your DNS settings on disconnect, so VPN DNS servers never stay behind
  • Show clear status, logs, and notifications
  • Keep the workflow down to explicit commands: connect, disconnect, status

Quick Start

Install:

curl -fsSL https://raw.githubusercontent.com/arastu/morti/main/install.sh | bash

Then:

nano ~/.morti.conf
source ~/.zshrc
morti connect

Commands

Command Description
morti connect Connect and start the background monitor
morti disconnect Disconnect and stop the monitor
morti reconnect Disconnect and connect again cleanly
morti status Show VPN and monitor state
morti debug Run OpenConnect in the foreground with verbose output
morti logs Tail ~/.morti.log
morti dns-restore Put your DNS settings back after a leftover VPN DNS
morti setup Create a fresh ~/.morti.conf
morti help Show the built-in help card

Install

One-line install

curl -fsSL https://raw.githubusercontent.com/arastu/morti/main/install.sh | bash

From a clone

git clone https://github.com/arastu/morti.git
cd morti
bash install.sh

Manual install

brew install openconnect terminal-notifier oath-toolkit
cp morti.sh ~/.morti.sh
cp morti.conf.example ~/.morti.conf
chmod 600 ~/.morti.conf
echo 'source ~/.morti.sh' >> ~/.zshrc
source ~/.zshrc

Configuration

Your config file lives at ~/.morti.conf.

# Required
MORTI_SERVER="vpn.yourcompany.com"
MORTI_USERNAME="your.username"
MORTI_PASSWORD="your-password"
# MORTI_PASSWORD_CMD="security find-generic-password -w -a your.username -s morti-vpn-password"
# MORTI_PASSWORD_CMD="op item get 'Fortinet VPN' --fields label=password"

# TOTP: choose one
MORTI_TOTP_SECRET="BASE32SECRETHERE"
# MORTI_TOTP_SECRET_CMD="security find-generic-password -w -a your.username -s morti-vpn-totp-secret"
# MORTI_TOTP_SECRET_CMD="op item get 'Fortinet VPN' --fields label=totp_secret"
# MORTI_TOTP_CMD="totp-cli p account"
# MORTI_TOTP_CMD="op item get 'Fortinet VPN' --otp"

More options are documented in morti.conf.example, including:

  • MORTI_SERVERCERT
  • MORTI_REALM
  • MORTI_PING_HOST
  • MORTI_EXTRA_ARGS

Apple Keychain

You can keep secrets out of ~/.morti.conf and fetch them at runtime:

security add-generic-password -U -a your.username -s morti-vpn-password -w 'your-password'
security add-generic-password -U -a your.username -s morti-vpn-totp-secret -w 'BASE32SECRETHERE'

Then in ~/.morti.conf:

MORTI_PASSWORD_CMD="security find-generic-password -w -a your.username -s morti-vpn-password"
MORTI_TOTP_SECRET_CMD="security find-generic-password -w -a your.username -s morti-vpn-totp-secret"

1Password

If you use the op CLI:

MORTI_PASSWORD_CMD="op item get 'Fortinet VPN' --fields label=password"
MORTI_TOTP_CMD="op item get 'Fortinet VPN' --otp"

Passwordless sudo

OpenConnect needs root privileges to create and manage the tunnel. To avoid repeated password prompts:

sudo visudo -f /etc/sudoers.d/openconnect

Add:

yourusername ALL=(ALL) NOPASSWD: /opt/homebrew/bin/openconnect
yourusername ALL=(ALL) NOPASSWD: /bin/kill

How It Works

morti connect
  -> load ~/.morti.conf
  -> generate a fresh OTP
  -> clean up any morti-managed session
  -> save your current DNS settings
  -> start openconnect
  -> verify tunnel health
  -> start background monitor

morti disconnect
  -> stop the monitor
  -> ask openconnect to exit cleanly (SIGINT, up to MORTI_STOP_TIMEOUT)
  -> restore your DNS settings if the VPN's are still there

If the tunnel drops, the monitor retries the connection automatically.


DNS Handling

On macOS the standard vpnc-script does not only set tunnel DNS — it writes the VPN's DNS servers into your active network service with networksetup -setdnsservers, which is a persistent System Settings change. OpenConnect only undoes it when it exits gracefully and re-runs the script with reason=disconnect. A force-kill, crash, sleep or reboot while connected leaves those DNS servers configured on Wi-Fi or Ethernet long after the tunnel is gone — and internal resolvers that are no longer reachable break name resolution.

morti closes that gap:

  • Saves your per-service DNS settings to ~/.morti.dns.backup before connecting
  • Gives OpenConnect up to MORTI_STOP_TIMEOUT seconds to run its own cleanup before kill -9
  • Restores the saved settings on disconnect, and again on the next connect, so leftovers from a crashed session heal themselves
  • Clears orphaned State:/Network/Service/utunN/* entries left in scutil
  • Flags leftovers in morti status, fixable any time with morti dns-restore

DNS you changed yourself is left alone. A service is only reset when it carries one of the addresses the VPN actually pushed — recorded in ~/.morti.dns.vpn while the tunnel is up. With no such record there is no way to tell a leak from a deliberate change, so morti does nothing.

The one exception is a service that is now empty but had DNS in the snapshot, which happens because the vpnc-script disconnect hook resets DNS to empty rather than to the previous value — silently dropping a manually configured resolver. morti only infers that immediately after tearing down a tunnel, or when you ask for it with morti dns-restore; at any other moment an empty service is just as likely to be your own switch back to DHCP.

Set MORTI_RESTORE_DNS=0 to turn the automatic restore off. morti dns-restore still works — an explicit request overrides the setting, since opting out of the automatic behaviour is exactly why you would run it by hand.


Environment Variables

Set these before source ~/.morti.sh if you want to override defaults:

Variable Default Purpose
MORTI_CONFIG_FILE ~/.morti.conf Config file path
MORTI_LOG_FILE ~/.morti.log Log file path
MORTI_MAX_RETRIES 5 Retry count
MORTI_RETRY_DELAY 3 Delay between retries
MORTI_MONITOR_INTERVAL 30 Health check interval
MORTI_OPENCONNECT_PATH /opt/homebrew/bin/openconnect OpenConnect binary path
MORTI_STOP_TIMEOUT 10 Seconds to wait for a clean OpenConnect exit before kill -9 (minimum 2; anything lower or non-numeric falls back to the default)
MORTI_RESTORE_DNS 1 Save and restore DNS settings around the tunnel (1/0, true/false, yes/no, on/off)
MORTI_PASSWORD_CMD unset Command used to fetch the VPN password
MORTI_TOTP_SECRET_CMD unset Command used to fetch the raw TOTP secret
MORTI_TOTP_CMD unset Command used to fetch the current OTP directly

Troubleshooting

Config file not found

Run:

morti setup

or:

cp morti.conf.example ~/.morti.conf

Failed to generate OTP

Make sure oath-toolkit is installed, or verify your MORTI_TOTP_CMD.

Connection keeps failing

Run:

morti debug

If needed, try MORTI_EXTRA_ARGS="--no-dtls" or pin the server certificate with MORTI_SERVERCERT.

Tunnel is up but traffic is broken

Set MORTI_PING_HOST to an internal reachable IP so the monitor can detect a dead tunnel.

VPN reconnects by itself after morti disconnect

This was caused by orphaned monitors left behind when morti connect was run more than once (fixed — monitors now stop themselves when they lose ownership of ~/.morti.monitor.pid). If you still see it on an older version, find and kill the stray monitor shell, then update:

morti status
rm -f ~/.morti.monitor.pid   # any leftover monitor exits on its next check

DNS servers from the VPN are still set after disconnecting

Check and fix:

morti status
morti dns-restore

This happens when OpenConnect is killed without getting to run its cleanup hook (a crash, a force-kill, sleep, or a reboot while connected). morti restores your DNS automatically on morti disconnect and on the next morti connect; morti dns-restore does it on demand. The baseline it restores lives in ~/.morti.dns.backup.

morti disconnect does not stop the monitor

Check:

morti status
rm -f ~/.morti.monitor.pid

Tests

The DNS and shutdown logic has tests that stub networksetup, scutil and ps, so they need no VPN and never touch your real settings:

zsh tests/dns_test.zsh
zsh tests/stop_test.zsh

Uninstall

curl -fsSL https://raw.githubusercontent.com/arastu/morti/main/uninstall.sh | bash

Or:

bash uninstall.sh

License

MIT

About

Forticlient VPN manager for macOS, auto-reconnects, generates TOTP, runs in the background, supports Apple Passwords and 1Password.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages