openfortivpn-go

module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: GPL-3.0

README

openfortivpn-go

A modern, dependency-light Go rewrite of openfortivpn — a client for PPP+SSL VPN tunnel services used by Fortinet's FortiGate/FortiClient VPN gateways.

This project is not affiliated with Fortinet. It is a fork/reimplementation of the original C client, rebuilt from scratch in Go. It follows the same wire protocol and reuses the system's pppd the same way the original does, while adding a few quality-of-life features that the C client doesn't have (see Features below).

Credit where it's due: all protocol reverse-engineering and the original design come from adrienverge/openfortivpn and its contributors. This project would not exist without their work. See Acknowledgements.

Table of contents

Features

Authentication
  • Username/password — standard FortiGate logincheck flow, including realm support (--realm).
  • Two-factor authentication (2FA):
    • FortiToken Mobile push — detected and handled automatically; the client waits for you to approve the push notification on your phone (optionally delayed via otp-delay in the config file).
    • Manual OTP — if no push is available, you're prompted for a one-time code, or you can pass it directly with --otp.
  • Cookie-based reuse — reconnect instantly with a previously obtained SVPNCOOKIE value via --cookie, skipping the interactive login entirely (handy for scripting or fast reconnects).
  • SAML browser login — for gateways configured with SAML SSO, --saml-login <port> spins up a local callback server, prints a login URL to open in your browser, and completes authentication once the identity provider redirects back.
  • Secure password entry:
    • Hidden terminal input (no echo) by default.
    • Optional integration with any pinentry program (--pinentry <path>) — useful for GUI-based password entry or when scripting Assuan-protocol pinentry front-ends.
TLS
  • Certificate pinning — trust specific gateway certificates by SHA-256 fingerprint with --trusted-cert <digest> (repeatable), independent of the system trust store. If verification fails, the client prints the presented certificate's digest so you can copy it straight into --trusted-cert.
  • Custom CA — verify against a specific CA bundle with --ca-file.
  • Client certificate authentication — PEM certificate/key pairs (--user-cert / --user-key, optionally password-protected via --pem-passphrase), or PKCS#11 smartcard/HSM tokens using a pkcs11: URI as the --user-cert value.
  • TLS version control — set a minimum accepted TLS version with --min-tls (1.01.3; defaults to 1.2), or configure the cipher list with --cipher-list.
  • --insecure-ssl — disable all certificate verification (only for lab/debugging use).
  • HTTP(S) proxy support — connects through an upstream proxy automatically if HTTPS_PROXY/https_proxy/ALL_PROXY/all_proxy is set in the environment, using an HTTP CONNECT tunnel.
Routing
  • Full-tunnel by default — replaces the system default route with one via the VPN interface, while always keeping a direct host route to the gateway itself so the TLS session doesn't get routed through itself.
  • Automatic split-tunneling — if the FortiGate gateway supplies split-tunnel routes in its configuration, only those subnets are routed through the VPN; your existing default route is left untouched.
  • --half-internet-routes — an alternative full-tunnel strategy that adds two /1 routes (0.0.0.0/1 and 128.0.0.0/1) instead of replacing the default route directly (avoids clobbering a DHCP-managed default route on some systems).
  • --no-routes — disable all routing table changes and manage routes yourself.
DNS

DNS handling is implemented per-platform to match each OS's native mechanisms as closely as possible:

  • Linux — updates /etc/resolv.conf directly (with automatic backup/restore of the original file), or delegates to resolvconf if available (--pppd-use-peerdns / use-resolvconf in the config file).
  • Windows — configures DNS servers on the PPP interface via netsh interface ip set/add dns, restoring DHCP-sourced DNS on disconnect.
  • macOS — primarily relies on Apple's own signed pppd binary to publish DNS natively into SCDynamicStore (via usepeerdns + a registered serviceid), matching how the official FortiClient integrates with the system. If the gateway doesn't hand out DNS during IPCP negotiation, the client falls back to writing scoped resolver files under /etc/resolver/<domain> — and if no DNS search domain was provided by the gateway, it automatically discovers one via a reverse-DNS (PTR) lookup of the assigned DNS servers. The DNS cache is flushed (dscacheutil + mDNSResponder) after any change.
  • --no-dns — disable all DNS configuration changes.
Reliability & operations
  • Persistent reconnection--persistent <seconds> automatically reconnects after a disconnect instead of exiting, with a configurable delay between attempts.
  • Clean shutdownCtrl+C (SIGINT), SIGTERM, or SIGHUP (e.g. closing the terminal window the process is attached to) tear down routes/DNS and terminate pppd gracefully before the process exits.
  • Orphaned pppd cleanup (Unix) — the PID of the launched pppd is tracked in /var/run/openfortivpn-go.pppd.pid. If a previous run was killed non-gracefully (kill -9, crash, abrupt system sleep) and left pppd running, the next run detects and terminates it before starting a new session — avoiding two PPP sessions competing for the same OS-level PPP link.
  • macOS: prevents idle sleep while connected — shells out to caffeinate -s -w <pid> for the lifetime of the tunnel, so the Mac won't go to sleep with the PPP link (and the legacy com.apple.nke.ppp kernel extension it depends on) active. The assertion is tied to the process PID, so it's automatically released even if the process dies unexpectedly.
  • Configuration file — every CLI flag has an equivalent config-file key, using the same INI-style format and default path (/etc/openfortivpn/config) as the original client. See Configuration file.
  • Flexible logging:
    • -v / -vv / -vvv — progressively more verbose console output.
    • -q / --quiet — errors only.
    • --log-file <path> — always writes complete DEBUG-level logs (full request/response bodies and XML, untruncated) to a file, independent of the console verbosity — useful for filing detailed bug reports without cluttering your terminal.
Cross-platform builds

Single static binaries for:

OS Architectures
Linux amd64, arm64, arm
macOS amd64 (Intel), arm64 (Apple Silicon)
Windows amd64, arm64

Requirements

  • A system pppd binary on Linux and macOS (openfortivpn-go shells out to it, matching the original client's architecture) — e.g. apt install ppp / dnf install ppp on Linux; macOS ships pppd out of the box.
  • Windows needs no separate driver install: if wintun.dll (the wintun driver — the same one WireGuard/Tailscale use) isn't already present next to the executable or in System32, it's downloaded automatically on first run for the correct architecture, with its checksum verified before use. Requires internet access on first run; place a trusted copy of wintun.dll next to the executable yourself to skip the download entirely.
  • Root/administrator privileges — required to run pppd (or create the TUN adapter on Windows), modify the routing table, and change DNS configuration. Run with sudo on Linux/macOS or an elevated shell on Windows.

Installation

Go install
go install github.com/elizandrodantas/openfortivpn-go/cmd/openfortivpn@latest
Prebuilt binaries

Download the binary for your OS/architecture from the Releases page. Each release's notes list every change since the previous tag, grouped by type (see Contributing for how those are generated).

Build from source

See Building from source.

Usage

sudo openfortivpn vpn.example.com:443 --username alice

If you omit --host/--port, you can instead pass host[:port] as a positional argument (as above). All flags below can also be set via the configuration file; CLI flags always take precedence.

Examples
# Interactive password + OTP prompt
sudo openfortivpn vpn.example.com --username alice

# Password and OTP supplied non-interactively
sudo openfortivpn vpn.example.com -u alice -p 'hunter2' --otp 123456

# Reuse a previously obtained session cookie (no interactive login)
sudo openfortivpn vpn.example.com --cookie 'AbCdEf0123...'

# SAML SSO login via local browser callback on port 8020
sudo openfortivpn vpn.example.com --saml-login 8020

# Pin the gateway certificate instead of trusting the system CA store
sudo openfortivpn vpn.example.com -u alice --trusted-cert 3f1e...c9

# Full debug log to file, quiet console
sudo openfortivpn vpn.example.com -u alice -q --log-file /tmp/openfortivpn.log

# Auto-reconnect every 10 seconds after a disconnect
sudo openfortivpn vpn.example.com -u alice --persistent 10
Flag reference
Flag Shorthand Description
--config <path> -c Config file path (default: /etc/openfortivpn/config)
--verbose -v Increase verbosity; repeat for more detail (-v, -vv, -vvv)
--quiet -q Suppress non-error output
--host <host> VPN gateway hostname
--port <port> VPN gateway port
--timeout <seconds> Connect timeout for TCP+TLS (default 20; 0 = no timeout)
--username <user> -u Username
--password <pass> -p Password (omit to be prompted interactively)
--otp <code> One-time password / 2FA code
--cookie <value> Reuse an existing SVPNCOOKIE session, bypassing login
--saml-login <port> Enable SAML browser-based login on the given local port
--realm <realm> Authentication realm
--sni <hostname> Override the TLS SNI hostname sent to the gateway
--ifname <name> Bind to a specific local network interface
--pinentry <path> Path to a pinentry binary for secure password entry
--ca-file <path> Custom CA certificate bundle
--user-cert <path> Client certificate (PEM path, or pkcs11:... URI)
--user-key <path> Client private key file
--pem-passphrase <pass> Passphrase for an encrypted private key
--insecure-ssl Disable TLS certificate verification (dangerous)
--min-tls <ver> Minimum TLS version: 1.0, 1.1, 1.2 (default), 1.3
--seclevel-1 Use OpenSSL SECLEVEL=1 semantics for weak Diffie-Hellman parameters
--cipher-list <list> TLS cipher list
--trusted-cert <sha256> Pin a trusted certificate by SHA-256 digest (repeatable)
--no-routes Do not modify the routing table
--no-dns Do not modify DNS configuration
--half-internet-routes Use 0.0.0.0/1 + 128.0.0.0/1 instead of replacing the default route
--pppd-use-peerdns Ask pppd to configure DNS itself
--pppd-log <path> pppd log file
--pppd-plugin <path> pppd plugin path
--pppd-ifname <name> pppd interface name
--pppd-call <path> pppd call file
--pppd-accept-remote Accept the remote IP address offered by pppd
--persistent <seconds> Reconnect automatically after this many seconds (0 = disabled)
--log-file <path> Write full DEBUG-level logs to this file regardless of -v
--version Print the client version
--help -h Show help

Configuration file

By default, openfortivpn reads /etc/openfortivpn/config (override with -c/--config). It's missing that file entirely is fine — you'll just need to pass everything via flags. The format is INI-style: key = value (or key=value), # starts a comment, blank lines are ignored, and unknown keys are a hard error.

# /etc/openfortivpn/config
host = vpn.example.com
port = 443
username = alice
password = hunter2
otp-prompt = Enter your FortiToken code:
otp-delay = 5
no-ftm-push = false

realm = corp
sni = vpn.example.com

set-routes = true
set-dns = true
half-internet-routes = false
persistent = 10

use-syslog = false
use-resolvconf = false

pppd-use-peerdns = false
pppd-log = /var/log/pppd.log
pppd-plugin =
pppd-ipparam =
pppd-ifname =
pppd-call =
pppd-accept-remote = false

ca-file =
user-cert =
user-key =
pem-passphrase =
insecure-ssl = false
min-tls = 1.2
seclevel-1 = false
cipher-list =
trusted-cert = 3f1e9c2a5b7d...c9

saml-login = 8020
user-agent =
hostcheck =
check-virtual-desktop =

Notes:

  • Boolean values accept 1/true/yes and 0/false/no (case-insensitive).
  • trusted-cert may be repeated (one line per pinned digest).
  • cookie (and cookie-on-stdin) are intentionally ignored in the config file — a session cookie must always be supplied via --cookie on the command line, so it's never persisted to disk.

Building from source

Requires Go (see go.mod for the minimum version) and make.

make build   # build for the current OS/architecture -> ./openfortivpn
make dist    # cross-compile for all supported OS/architecture pairs -> ./dist/
make test    # go test -race ./...
make lint    # go vet + staticcheck
make clean   # remove build artifacts
make help    # list all targets

make dist produces binaries for: linux/amd64, linux/arm64, linux/arm, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64.

The version string embedded in binaries (visible via --version) is derived from git describe --tags --always --dirty, or can be overridden explicitly: make build VERSION=v1.2.3.

Architecture

Package Responsibility
cmd/openfortivpn CLI entrypoint (Cobra): flag parsing, logging setup, signal handling
internal/config Config struct, INI file loader, validation
internal/auth Authentication strategies: password/OTP/FTM push, cookie, SAML
internal/tlsconn TLS connection setup, proxy support, certificate verification/pinning
internal/httptunnel Minimal hand-rolled HTTP client over the raw TLS socket, plus the local SAML callback server
internal/tunnel Top-level orchestration: connect → auth → XML config → pppd → relay → routes/DNS
internal/xmlparse Lenient parser for the gateway's fortisslvpn_xml response (assigned IP, DNS, split routes)
internal/ppp Drives the system pppd via a PTY (Unix); on Windows, drives a wintun adapter instead
internal/ppp/pppproto Minimal PPP LCP/IPCP negotiation (RFC 1661/1332), used by the Windows engine in place of pppd
internal/hdlc HDLC framing/deframing between the relay loop and the local PPP transport (pppd PTY on Unix, wintun engine pipe on Windows)
internal/io Packet framing on the wire and the bidirectional relay loop
internal/ipv4 Per-OS routing table and DNS management (Linux/macOS/Windows)
internal/userinput Interactive password/OTP prompts, with optional pinentry support
pkg/version Build-time version string, injected via -ldflags

Known limitations

  • Windows support is new and needs real-world testing. Since there's no pppd on Windows, openfortivpn-go implements its own minimal PPP client (LCP/IPCP negotiation, see internal/ppp/pppproto) on top of a wintun virtual adapter. It requires Administrator privileges; wintun.dll is fetched automatically on first run if not already present (see Requirements), which needs internet access at that point. If you hit issues, please open one with -vvv --log-file output attached.
  • macOS DNS integration writes scoped resolver entries under /etc/resolver/<domain> as a fallback; this is not a global default-resolver override the way the official FortiClient (a signed NetworkExtension) can achieve. In practice this only matters if the gateway doesn't hand out DNS via IPCP and no search domain is configured (auto-discovery via PTR lookup covers most of these cases).

Contributing

Contributions are welcome — see CONTRIBUTING.md for development setup, the commit message convention this project relies on for automated release notes, and pull request expectations.

License

Licensed under the GNU General Public License v3.0 — see LICENSE, matching the license of the original project this is forked from.

Acknowledgements

  • adrienverge/openfortivpn — the original C implementation this project is a Go rewrite of, including all of the FortiGate protocol reverse-engineering it's built on.
  • Everyone who has contributed to the original project over the years.

Directories

Path Synopsis
cmd
openfortivpn command
internal
auth
Package auth implements VPN authentication methods.
Package auth implements VPN authentication methods.
config
Package config defines the VPN configuration structure and loading logic.
Package config defines the VPN configuration structure and loading logic.
hdlc
Package hdlc implements HDLC-like framing for PPP packets per RFC 1662.
Package hdlc implements HDLC-like framing for PPP packets per RFC 1662.
httptunnel
Package httptunnel implements a minimal HTTP/1.1 client over a raw *tls.Conn.
Package httptunnel implements a minimal HTTP/1.1 client over a raw *tls.Conn.
io
Package io implements the I/O relay loop and PPP-over-TLS packet framing.
Package io implements the I/O relay loop and PPP-over-TLS packet framing.
ipv4
Package ipv4 manages VPN routing table changes and DNS configuration.
Package ipv4 manages VPN routing table changes and DNS configuration.
ppp
ppp/pppproto
Package pppproto implements the small subset of PPP (RFC 1661), LCP and IPCP (RFC 1332/1877) negotiation needed to establish a FortiGate SSL-VPN tunnel where no system pppd is available (Windows).
Package pppproto implements the small subset of PPP (RFC 1661), LCP and IPCP (RFC 1332/1877) negotiation needed to establish a FortiGate SSL-VPN tunnel where no system pppd is available (Windows).
ppp/wintundll
Package wintundll makes the Wintun driver DLL (https://www.wintun.net/, the same driver WireGuard/Tailscale use) available before any Wintun API call is made.
Package wintundll makes the Wintun driver DLL (https://www.wintun.net/, the same driver WireGuard/Tailscale use) available before any Wintun API call is made.
tlsconn
Package tlsconn handles TLS dialing, proxy CONNECT tunneling, and certificate verification with SHA-256 pinning.
Package tlsconn handles TLS dialing, proxy CONNECT tunneling, and certificate verification with SHA-256 pinning.
tunnel
Package tunnel orchestrates the full VPN connection lifecycle.
Package tunnel orchestrates the full VPN connection lifecycle.
userinput
Package userinput handles interactive password prompts and pinentry.
Package userinput handles interactive password prompts and pinentry.
xmlparse
Package xmlparse implements a lenient XML parser for FortiGate server responses, which may contain malformed XML that strict parsers reject.
Package xmlparse implements a lenient XML parser for FortiGate server responses, which may contain malformed XML that strict parsers reject.
pkg

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL