vpn

package module
v1.2.3 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: LGPL-2.1, LGPL-2.1-or-later Imports: 33 Imported by: 0

README

go-openlawsvpn

Pure-Go OpenVPN3 client protocol implementation — AWS Client VPN + SAML/CRV1 flow.

Zero C dependencies. CGO_ENABLED=0 builds a fully static binary. gomobile bind produces an .aar for Android without NDK or CMake. See CHANGELOG.md for project-wide release notes.

Status

Working end-to-end on Linux (CLI + daemon + GTK4 GUI) and Android (via the gomobile .aar). Find the current release with git tag --list 'v*' --sort=-v:refname | head -1. The .aar build pipeline is in .github/workflows/aar.yml; RPMs are built by COPR vorona/openlawsvpn; Arch Linux packages on AUR as openlawsvpn.

AWS support boundary: AWS documents SAML-based Client VPN connections as supported only with the AWS-provided client. This repository implements the compatible CRV1 wire flow as an unsupported third-party client. Use the AWS-provided client when AWS-supported operation is required.

Components
Component Description
cmd/daemon openlawsvpn-daemon — D-Bus session service; manages the VPN tunnel with CAP_NET_ADMIN (no root)
cmd/cli openlawsvpn-cli — CLI client with SAML flow, reconnect loop, and relay agent mode (-relay)
cmd/relay-server Local relay server for dev/testing without hitting production
gui-gtk/ GTK4 + libadwaita desktop GUI; communicates with the daemon over D-Bus; includes Relay screen

Build

# Daemon
CGO_ENABLED=0 go build -o openlawsvpn-daemon ./cmd/daemon
# Grant CAP_NET_ADMIN so the daemon can open TUN devices without root:
sudo setcap cap_net_admin+eip ./openlawsvpn-daemon
./openlawsvpn-daemon &

# GTK4 GUI (requires gtk4-devel, libadwaita-devel, dbus-devel)
cd gui-gtk && cargo build --release
./target/release/openlawsvpn-gui

# Linux CLI (direct, no daemon)
CGO_ENABLED=0 go build -o openlawsvpn-cli ./cmd/cli
sudo ./openlawsvpn-cli -config your.ovpn

# Add "verb 4" to the profile to log the verified server certificate.

# Public relay demo.
sudo ./openlawsvpn-cli -relay default -daemon \
  -logfile /tmp/vpn.log -pidfile /tmp/vpn.pid

# Private relay token (CI/CD headless auth); token file must be mode 0600.
sudo ./openlawsvpn-cli -relay-token-file /run/user/$UID/openlawsvpn-relay-token \
  -daemon -logfile /tmp/vpn.log -pidfile /tmp/vpn.pid

# Android .aar (requires gomobile + Android NDK)
gomobile bind -o go-openlawsvpn.aar -target android -androidapi 31 \
    github.com/openlawsvpn/go-openlawsvpn

With verb 4, the client logs the verified server certificate during every TLS handshake in an OpenSSL-like format: subject, issuer, serial number, validity period, DNS names, and SHA-256 fingerprint. This is the certificate used to authenticate the VPN server; it is not the user's SAML or client certificate.

RPM packages (Fedora / RHEL)
make srpm    # builds openlawsvpn-*.src.rpm
make rpm     # builds binary RPMs via mock

Produces three sub-packages: openlawsvpn-daemon, openlawsvpn-gui, and openlawsvpn (meta).

Arch Linux (AUR)
# Using an AUR helper:
paru -S openlawsvpn

# Or manually:
git clone https://aur.archlinux.org/openlawsvpn.git
cd openlawsvpn && makepkg -si

Installs openlawsvpn-daemon, openlawsvpn-cli, and openlawsvpn-gui.

Daemon D-Bus interface

The daemon exposes com.openlawsvpn.Daemon on the session bus:

Method / Signal Signature Description
Connect(path) (s) Start VPN using the given .ovpn config
Disconnect() () Tear down the active tunnel
Status() → (s,s,s,s) state, server_ip, assigned_ip, profile_path
StateChanged (s,s,s) state, server_ip, assigned_ip
LogLine (s) Log message
StatsUpdate (t,t,t) bytes_sent, bytes_recv, uptime_secs
SAMLRequired (s) SAML browser URL
DNS / polkit

The daemon sets per-interface DNS via systemd-resolved. The polkit rule in packaging/10-openlawsvpn-dns.rules grants the daemon permission to call org.freedesktop.resolve1 methods without a password prompt.

Test

# Unit tests (no network):
go test -race ./...

# Integration tests (runs local mock server — no Docker required):
go test -v -tags=integration -timeout 120s .

CI / CD

Workflow Trigger What it does
CI (ci.yml) push / PR to main checks Rust dependency licenses and verifies release versions, Go builds, race tests, and vet
Build AAR (aar.yml) push tag v* or manual builds go-openlawsvpn.aar via gomobile bind, publishes GitHub Release, opens a version-bump PR on openlawsvpn-android-go
Release (release.yml) push tag v* or manual builds static cli + daemon binaries for amd64 / arm64 / ppc64le, attaches them to the GitHub Release
VPN Integration (vpn-integration.yml) manual integration run against a live endpoint

RPM packages are not built in this repo's CI — they are built by COPR (vorona/openlawsvpn) from the spec in packaging/openlawsvpn.spec.

Publishing a new release

Use the version script; do not edit only the RPM spec, Cargo manifest, or PKGBUILD. The Cargo manifest controls the version displayed by the GUI.

scripts/bump-version.sh X.Y.Z
# Move CHANGELOG.md's Unreleased entries to X.Y.Z and add a new Unreleased section.
# Add a concise matching entry to packaging/openlawsvpn.spec's %changelog.
make check-version
git diff --check
# Commit the complete version bump before tagging it.
git tag vX.Y.Z
git push origin vX.Y.Z

Before pushing the tag, confirm that the bump includes packaging/openlawsvpn.spec, packaging/PKGBUILD, and gui-gtk/Cargo.toml. CI rejects a commit when these versions differ, and release workflows reject a tag that does not match them.

The aar.yml workflow builds the AAR, attaches it (with SHA-256) to the GitHub Release, then triggers bump-aar.yml on openlawsvpn-android-go — which opens a PR bumping the pinned AAR version automatically.

AUR release: push a pkg/x.y.z-N tag to trigger PKGBUILD updates:

git tag pkg/X.Y.Z-1
git push origin pkg/X.Y.Z-1
make aur-release VERSION=X.Y.Z-1   # updates PKGBUILD, hash, .SRCINFO in ../aur-openlawsvpn
# then: cd ../aur-openlawsvpn && git add -A && git commit -m "..." && git push

Cross-repo auth: writes use the openlawsvpn-ci GitHub App via actions/create-github-app-token — secrets CI_APP_ID / CI_APP_PRIVATE_KEY. There is no ANDROID_GO_PAT PAT.

Known limitations

OpenVPN-PRF key derivation (plain OpenVPN 2.x)

AWS Client VPN always negotiates TLS-EKM (key-derivation tls-ekm in PUSH_REPLY), so key derivation is fully correct for that use case.

Plain OpenVPN 2.x servers that do not push key-derivation tls-ekm use the OpenVPN-PRF method, which requires the TLS ServerRandom (the 32-byte random from the server's ServerHello). Go's crypto/tls does not expose this value in ConnectionState, so the fallback path substitutes zeros. The TLS handshake and authentication succeed, but the derived data-channel keys will be wrong — packets fail to decrypt and no traffic flows.

No fix is possible without forking crypto/tls. Track the upstream Go proposal if plain OpenVPN 2.x support without EKM becomes a requirement.

SAML assertion is single-use

AWS Client VPN SAML assertions are cryptographically bound to the original AuthnRequest ID. The server marks the assertion consumed on first use. Retrying Phase 2 with the same token returns AUTH_FAILED,Invalid username or password even within the token's TTL.

For reconnects: if the server's CRV1 session is still alive the client can reconnect with the cached token. If the session has expired (AUTH_FAILED), the user must complete the browser SAML flow again.

License

LGPL-2.1-or-later with usage exception. See LICENSE and LICENSE_USAGE_EXCEPTION for details.

Documentation

Overview

Package vpn is the top-level go-openlawsvpn package.

It provides a high-level OpenVPN3 client that handles the full connection lifecycle including SAML/CRV1 authentication for AWS Client VPN and standard certificate-based auth for other OpenVPN3 servers.

Typical usage:

p, err := profile.ParseFile(f)
c := vpn.New(p)
c.SAMLTokenFn = func(ctx context.Context, ch vpn.SAMLChallenge) (string, error) {
    // open ch.URL in a browser, return the SAMLResponse token
}
err = c.Connect(ctx)
// tunnel is now up; data flows through the TUN device
c.Disconnect()
c.WaitForDisconnect()

Index

Constants

View Source
const AWSSAMLUnsupportedNotice = "This client is not AWS-supported for SAML Client VPN authentication. For AWS-supported operation, use the AWS VPN Client."

AWSSAMLUnsupportedNotice describes the AWS support boundary for this independent CRV1 implementation.

Variables

View Source
var ErrReauthRequired = fmt.Errorf("vpn: SAML re-authentication required: token rejected by server")

ErrReauthRequired is returned by Reconnect when the cached SAML token has been rejected by the server (AUTH_FAILED). The SAML assertion is cryptographically bound to its original AuthnRequest and cannot be reused with a new Phase 1 session. The caller must run the full browser flow again.

Functions

This section is empty.

Types

type Client

type Client struct {

	// SAMLTokenFn is called during Connect when the server issues a SAML/CRV1
	// challenge. The callback must open challenge.URL in a browser, wait for
	// the SAMLResponse via the ACS server on 127.0.0.1:35001, and return the
	// base64-encoded token. Required for AWS Client VPN profiles; ignored for
	// certificate-based auth.
	SAMLTokenFn func(ctx context.Context, challenge SAMLChallenge) (string, error)

	// ProtectFn, if set, is called with the raw file descriptor of every
	// transport socket before it is used.  On Android this must call
	// VpnService.protect(fd) so the socket bypasses the VPN tunnel and
	// reaches the real network.  Nil on Linux desktop (no-op).
	ProtectFn func(fd int) error

	// TUNSetup, if set, is called instead of tun.Open() to obtain the TUN
	// device.  The callback receives the ifconfig JSON so the Android layer can
	// configure VpnService.Builder before calling establish().
	// Nil on Linux desktop — tun.Open() is used directly.
	TUNSetup func(ifconfigJSON string, mtu int) (*tun.Device, error)

	// EventFn, if set, is called for every notable lifecycle event: state
	// transitions, log lines, and periodic stats. Called from internal
	// goroutines — must not block. Set before calling Connect.
	EventFn EventFn

	// reconnect
	// MaxReconnects is the maximum number of reconnect attempts before giving
	// up. A value of 0 means unlimited retries. Default is 0 (unlimited).
	MaxReconnects int
	// contains filtered or unexported fields
}

Client is a go-openlawsvpn VPN client.

A Client is not safe for concurrent use by multiple goroutines except where noted (Stats and Wait may be called concurrently with the data channel).

func New

func New(p *profile.Profile) *Client

New creates a new Client from the given profile. The Client is idle until Connect is called.

func (*Client) Connect

func (c *Client) Connect(ctx context.Context) error

Connect dials, authenticates, and brings up the VPN tunnel.

The auth flow is auto-detected from the profile:

  • AWS Client VPN (cvpn-endpoint-*.amazonaws.com): SAML/CRV1 two-phase flow. SAMLTokenFn must be set; it is called with the challenge URL and must return the base64-encoded SAMLResponse.
  • Certificate auth (profile has <cert>+<key>): mutual-TLS, no SAML.
  • Fallback: plain connection (PUSH_REPLY expected without challenge).

Connect is not safe for concurrent use.

func (*Client) ConnectPhase2

func (c *Client) ConnectPhase2(ctx context.Context, samlToken string) error

ConnectPhase2 completes the VPN connection using a SAML token delivered via the relay server. Call SetRelayPhase2 first to pre-seed Phase 1 state.

samlToken is the base64-encoded SAMLResponse received from the relay.

func (*Client) ConnectPhase2Reuse

func (c *Client) ConnectPhase2Reuse(ctx context.Context, samlToken string) error

ConnectPhase2Reuse completes the VPN connection with an already-obtained SAML token. Only for testing token-reuse behaviour — do not use in production code. Call ResetForTest first to restore the required state.

func (*Client) Disconnect

func (c *Client) Disconnect() error

Disconnect initiates a graceful teardown of the VPN tunnel. It signals the background goroutines to stop and begins cleaning up. Call Wait to block until teardown completes.

func (*Client) Done

func (c *Client) Done() <-chan struct{}

Done returns a channel that is closed when the client disconnects (for any reason — graceful, keepalive timeout, server-initiated, etc.). The disconnect reason is available via Wait after the channel closes.

func (*Client) LocalIP

func (c *Client) LocalIP() string

LocalIP returns the tunnel-side IP address assigned by the server after a successful Phase 2, or "" if the tunnel is not up.

func (*Client) Phase1ForTest

func (c *Client) Phase1ForTest(ctx context.Context) (*SAMLChallenge, error)

Phase1ForTest runs connectPhase1 and returns the SAML challenge (or nil). Only for integration tests — do not use in production code.

func (*Client) Phase1IP

func (c *Client) Phase1IP() string

Phase1IP returns the sticky backend IP captured during Phase 1. This is the IP the agent must use for Phase 2 to ensure server affinity. Returns "" before Phase 1 completes.

func (*Client) Reconnect

func (c *Client) Reconnect(ctx context.Context) error

Reconnect tears down the current connection and re-establishes the tunnel.

While the cached SAML token is still valid (NotOnOrAfter not yet passed), Reconnect skips Phase 1 entirely and retries Phase 2 directly with the cached server IP, StateID, and token — no browser required.

If Phase 2 returns AUTH_FAILED (the server's CRV1 session expired during the outage), Reconnect returns ErrReauthRequired immediately. The SAML assertion is bound to the original AuthnRequest ID and cannot be reused with a new Phase 1 session. The caller must run the full browser flow again.

It applies exponential backoff between attempts: 1 s, 2 s, 4 s, … capped at 30 s. MaxReconnects limits total attempts (0 = unlimited, the default).

Reconnect is not safe for concurrent use.

func (*Client) ResetForTest

func (c *Client) ResetForTest(challenge *SAMLChallenge)

ResetForTest resets the client to stateConnecting with the given SAML challenge, so ConnectPhase2Reuse can be called again with the same token. Only for manual token-reuse testing — do not use in production code.

func (*Client) SetRelayPhase2

func (c *Client) SetRelayPhase2(remoteIP, stateID string)

SetRelayPhase2 pre-seeds the Phase 1 state obtained by the mobile/desktop app so that ConnectPhase2 can skip Phase 1 and connect directly to the sticky backend IP with the SAML credentials delivered via the relay.

Must be called before ConnectPhase2 and only when using relay mode. remoteIP is the backend server IP from the CRV1 challenge; stateID is the opaque CRV1 state token.

func (*Client) Stats

func (c *Client) Stats() Stats

Stats returns a snapshot of current traffic counters and uptime. Safe to call concurrently while the tunnel is up.

func (*Client) WaitForDisconnect

func (c *Client) WaitForDisconnect() error

WaitForDisconnect blocks until the client is fully disconnected and returns the disconnect reason (nil for a clean Disconnect call). It is safe to call WaitForDisconnect concurrently from multiple goroutines.

type ClientState

type ClientState int

ClientState is the connection lifecycle state reported via events.

const (
	// StateIdle means no active connection.
	StateIdle ClientState = iota
	// StateConnecting covers Phase 1 and Phase 2 establishment.
	StateConnecting
	// StateWaitingSAML means Phase 1 completed and the GUI must open the SAML URL.
	StateWaitingSAML
	// StateConnected means the TUN interface is up and data flows.
	StateConnected
	// StateDisconnecting means teardown is in progress.
	StateDisconnecting
	// StateError means connection failed; Message carries the reason.
	StateError
)

func (ClientState) String

func (s ClientState) String() string

String returns a lowercase D-Bus-friendly representation of the state.

type Event

type Event struct {
	// Type identifies the event category.
	Type EventType

	// State is set when Type == EventStateChanged.
	State ClientState

	// Message carries a log line (EventLog), SAML URL (StateWaitingSAML),
	// error description (StateError), or assigned tunnel IP (StateConnected).
	Message string

	// ServerIP is the VPN server IP (set when State == StateConnected).
	ServerIP string

	// Stats is set when Type == EventStatsUpdate.
	Stats Stats

	// At is the wall-clock time of the event.
	At time.Time
}

Event is emitted by the Client for every notable lifecycle transition or log line.

type EventFn

type EventFn func(Event)

EventFn is a callback invoked for every Event emitted by the Client. It is called from internal goroutines; implementations must not block. Set Client.EventFn before calling Connect.

type EventType

type EventType int

EventType identifies the kind of event emitted by the Client.

const (
	// EventLog is an informational log line.
	EventLog EventType = iota
	// EventStateChanged signals a connection state transition.
	EventStateChanged
	// EventStatsUpdate carries a traffic statistics snapshot.
	EventStatsUpdate
)

type SAMLChallenge

type SAMLChallenge struct {
	// URL is the identity-provider URL the user must visit.
	URL string
	// StateID is the opaque session token that must be returned in Phase 2.
	StateID string
}

SAMLChallenge holds the parsed fields from a CRV1 SAML challenge. The caller must open URL in a browser; the IdP will POST the SAMLResponse to 127.0.0.1:35001, which the caller can capture via auth/saml.NewACSServer.

type Stats

type Stats struct {
	// BytesSent is the total number of plaintext bytes sent through the tunnel.
	BytesSent uint64
	// BytesRecv is the total number of plaintext bytes received through the tunnel.
	BytesRecv uint64
	// Uptime is the duration since the tunnel was established (zero if not up yet).
	Uptime time.Duration
}

Stats is a snapshot of per-session traffic counters.

Directories

Path Synopsis
auth
saml
Package saml — session expiry monitor.
Package saml — session expiry monitor.
cmd
cli command
Command openlawsvpn-cli is a minimal CLI for the go-openlawsvpn VPN client.
Command openlawsvpn-cli is a minimal CLI for the go-openlawsvpn VPN client.
daemon command
Command openlawsvpn-daemon is a D-Bus system service that manages VPN connections on behalf of the openlawsvpn GTK GUI.
Command openlawsvpn-daemon is a D-Bus system service that manages VPN connections on behalf of the openlawsvpn GTK GUI.
relay-server command
Command relay-server is a zero-dependency, in-memory relay server for local testing.
Command relay-server is a zero-dependency, in-memory relay server for local testing.
Package dns handles DNS configuration pushed by an OpenVPN server.
Package dns handles DNS configuration pushed by an OpenVPN server.
internal
compress
Package compress implements the OpenVPN3 data-channel compression framing stubs for the 'compress lz4' and 'comp-lzo' options pushed by the server.
Package compress implements the OpenVPN3 data-channel compression framing stubs for the 'compress lz4' and 'comp-lzo' options pushed by the server.
crypto
CBC cipher implementation for the OpenVPN3 data channel.
CBC cipher implementation for the OpenVPN3 data channel.
ctls
Package ctls implements TLS-over-control-channel for OpenVPN3.
Package ctls implements TLS-over-control-channel for OpenVPN3.
datachannel
Package datachannel implements the OpenVPN3 data channel: P_DATA_V2 packet framing, the encrypt/decrypt pipeline, and replay-protection sliding window.
Package datachannel implements the OpenVPN3 data channel: P_DATA_V2 packet framing, the encrypt/decrypt pipeline, and replay-protection sliding window.
framing
Package framing implements the OpenVPN3 wire format: packet opcodes, the 2-byte length-prefixed TCP framing, and raw UDP framing.
Package framing implements the OpenVPN3 wire format: packet opcodes, the 2-byte length-prefixed TCP framing, and raw UDP framing.
mssfix
Package mssfix clamps the TCP MSS option in SYN and SYN-ACK packets.
Package mssfix clamps the TCP MSS option in SYN and SYN-ACK packets.
prf
Package prf implements the OpenVPN3 key-derivation pseudo-random function.
Package prf implements the OpenVPN3 key-derivation pseudo-random function.
reliable
Package reliable implements the OpenVPN3 reliable control-channel transport: sequence numbers, ACK handling, retransmit queue, and a sliding receive window.
Package reliable implements the OpenVPN3 reliable control-channel transport: sequence numbers, ACK handling, retransmit queue, and a sliding receive window.
mock
mockserver command
Command mockserver is the go-openlawsvpn mock OpenVPN server.
Command mockserver is the go-openlawsvpn mock OpenVPN server.
Package profile parses OpenVPN .ovpn configuration files.
Package profile parses OpenVPN .ovpn configuration files.
Package relay implements the CLI agent side of the openlawsvpn SAML relay protocol.
Package relay implements the CLI agent side of the openlawsvpn SAML relay protocol.
Netlink route management for the routing package.
Netlink route management for the routing package.
Package testenv manages the mock openvpn3-core server for integration tests.
Package testenv manages the mock openvpn3-core server for integration tests.
Package tun provides a TUN network interface for Linux.
Package tun provides a TUN network interface for Linux.

Jump to

Keyboard shortcuts

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