Help us shape the future of Go! Take the 2026 Go Developer Survey

claimward-vpn-client

module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: BSD-3-Clause

README

claimward-vpn-client

The shared Go client library for the Claimward VPN: sign-in, the enrollment wire protocol, the userspace WireGuard tunnel, and the live route-push client.

This module ships no binary. It is imported by:

  • claimward-vpn-server — for the shared wire types (pkg/protocol) and gRPC route stubs (pkg/routespb);
  • the platform apps — claimward-vpn-app-osx (cmd/claimward-app + the privileged cmd/claimward-helper), and the -linux / -windows apps — which own the actual runnable binaries.

Looking for something to run? Build one of the app repos above; this repo is the library they share.

Packages

Package Purpose
pkg/protocol Wire contract (/enroll, /heartbeat, /deregister, /tenants) — the single source of truth shared with the server
pkg/auth Interactive sign-in behind a pluggable Provider: GitHub device-authorization flow (default), OIDC Authorization Code + PKCE, or go-authn device flow, whose provider also registers the device's WireGuard key (KeyRegistrar). Returns the bearer Token sent to the server
pkg/oidc The OIDC Authorization Code + PKCE flow (issuer discovery, loopback redirect capture) used by the oidc auth provider
pkg/browser Opens a URL in the user's default browser using absolute opener paths, so it works from GUI apps launched by LaunchServices
pkg/client High-level client: Enroll/Heartbeat/Deregister/Tenants against the server, plus TunnelConfig to turn an EnrollResponse into a wgtun.Config
pkg/wgkey WireGuard key generation / parsing
pkg/wgtun Userspace WireGuard tunnel via wireguard-go (+ interface, route and DNS setup: darwin and Linux with their tools, Windows on a Wintun adapter through winipcfg); needs elevated privileges
pkg/routeclient Watches the server's RouteService (gRPC) and reports live route updates without re-enrolling
pkg/routespb Generated gRPC/protobuf stubs for the RouteService (shared with the server)
pkg/appcore The app's business logic, shared by the macOS, Linux and Windows apps: sign-in, the tenant chosen for the session, connect/disconnect through the helper, status for the UI
pkg/helper The privileged helper (root / SYSTEM), shared by the three apps: enrolls, owns the tunnel, keeps the server's lease, watches route pushes. Each app's helper binary is a main that loads its configuration and listens
pkg/hproto, pkg/helperclient The helper's socket protocol (connect, tenants, renew, down, status) and the app's client for it
pkg/tokenstore 0600 on-disk session store (auth tokens, device key, chosen tenant), used by pkg/appcore in all three apps

Architecture (end to end)

app     --auth Provider-->  IdP          (GitHub device flow / OIDC PKCE / go-authn device flow → bearer token)
app     --connect (bearer, device key, tenant) over the helper socket-->  helper (root / SYSTEM)
helper  --POST /enroll (Bearer token, wg pubkey, tenant)-->  server  (only a server named in helper.json)
server  --wgctrl-->  wg0 kernel iface     (adds peer, allocates IP)
helper  <--assigned IP, server pubkey, endpoint, the tenant's routes + DNS--  server
helper  --wireguard-go-->  utunN / utun / Wintun "Claimward"   (tunnel up)
helper  --gRPC RouteService.Watch (TLS)-->  server (optional: live route updates)

Library usage

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/claimward/claimward-vpn-client/pkg/auth"
	"github.com/claimward/claimward-vpn-client/pkg/client"
	"github.com/claimward/claimward-vpn-client/pkg/protocol"
	"github.com/claimward/claimward-vpn-client/pkg/routeclient"
	"github.com/claimward/claimward-vpn-client/pkg/wgkey"
)

func main() {
	ctx := context.Background()

	// 1. Interactive sign-in. Default provider is GitHub (device flow); set
	//    Provider "oidc" with OIDCIssuer/OIDCClientID for an OIDC issuer.
	provider, err := auth.New(auth.Config{
		Provider:       "github",
		GitHubClientID: "Iv1.0123456789abcdef",
	})
	if err != nil {
		log.Fatal(err)
	}
	tok, err := provider.Login(ctx, func(p auth.DevicePrompt) {
		fmt.Printf("visit %s and enter code %s\n", p.VerificationURI, p.UserCode)
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Generate a WireGuard keypair and enroll with the server.
	keys, err := wgkey.Generate()
	if err != nil {
		log.Fatal(err)
	}
	// With go-authn, the PUBLIC key is registered at the provider first, and
	// the token for the server is the one RegisterKey returns. Keep its
	// Refresh: the provider rotates refresh tokens.
	if reg, ok := provider.(auth.KeyRegistrar); ok {
		if tok, err = reg.RegisterKey(ctx, tok, keys.Public.String(), "laptop"); err != nil {
			log.Fatal(err)
		}
	}
	c := client.New("https://vpn.example.com")
	resp, err := c.Enroll(ctx, tok.Value, keys.Public,
		protocol.DeviceInfo{Name: "laptop", OS: "darwin", Platform: "cli"}, "")
	if err != nil {
		log.Fatal(err)
	}

	// 3. Turn the enrollment response into a tunnel config. Bring it up with
	//    pkg/wgtun (wgtun.Up), which needs elevated privileges.
	tun, err := client.TunnelConfig(resp, keys.Private)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("tunnel address:", tun.Address)

	// 4. Optionally watch the server for live route changes over gRPC.
	if resp.GRPCEndpoint != "" {
		go routeclient.Watch(ctx, resp.GRPCEndpoint, tok.Value, keys.Public.String(),
			func(u routeclient.Update) {
				fmt.Println("routes updated:", u.AllowedIPs)
			})
	}
}

With go-authn

Provider: "go-authn", with OIDCIssuer and OIDCClientID, signs in at a go-authn provider: an OpenID Connect provider in front of a SAML federation such as RENATER or eduGAIN, which also keeps whose each WireGuard key is. claimward-vpn-server then enrolls a key only if its owner registered it there (AUTH_PROVIDER=go-authn on the server). The flow is:

  1. Login: the device flow, scope openid wireguard. The token opens the provider's key registry and nothing else; a go-authn provider addresses such a token to itself alone;
  2. RegisterKey (the KeyRegistrar interface):
    • registers the device's public key (POST /wireguard/key), renewing it when it is already the person's;
    • then refreshes asking for openid alone (RFC 6749 §6), which gives the token for claimward-vpn-server;
    • when the sign-in token has lapsed, it first refreshes with no scope, which asks for everything granted;
    • it returns the rotated refresh token, which replaces the old one, since the old one is spent;
  3. Enroll with that token.

The private key never leaves the device. The provider's client needs wireguard_keys = true and a refresh_lifetime.

The privileged helper

The helper runs as root (or SYSTEM). Any process that can reach its socket can make it act, so what it does is bounded by its own configuration, never by the request:

  • It enrolls only with a server its configuration names. A helper that took the server from the request would let any local process point it at a server of its own, answering with routes for 0.0.0.0/0: every packet of the machine, sent where that process chose.
  • It takes no tunnel configuration from a request. The tunnel is what that server answered. The earlier up and update-routes actions, which took one, are gone.
  • Its socket is 0660, owned by root and one group (admin on macOS, claimward on Linux), in a directory only root can write. The earlier helper's socket was 0666.
  • Its configuration must be root's, and writable by root alone, or the helper does not start.

On Windows the same rules are ACLs, which the helper sets and reads back itself rather than trusting an installer to have done it:

  • the socket's directory, C:\ProgramData\Claimward, gets a protected DACL (nothing inherited from ProgramData, which lets every user create files there): SYSTEM and Administrators in full control, the socket's group allowed to list and traverse it and nothing more. It is created already carrying that DACL, and a directory that is a junction or a link is refused;
  • the socket gets its own DACL: SYSTEM and Administrators, and the group allowed to connect (read/write);
  • the group is the local group Claimward Users (the app's installer creates it and adds the person). If it does not exist the socket is opened to INTERACTIVE — everybody logged on at the machine, console or Remote Desktop — and the helper logs that it did;
  • helper.json must be owned by SYSTEM or Administrators, and writable by nobody else: the helper reads the file's owner and DACL and refuses any allow entry that grants write, append, delete, WRITE_DAC, WRITE_OWNER or generic write/all to another SID, and a null DACL.

The rules themselves (pkg/helper/acl.go) are pure functions, tested on every platform; the Windows CI job applies them to real files and reads them back.

{
  "servers": ["https://vpn.example.org"],
  "group": "claimward"
}

A route watch (pkg/routeclient) carries the bearer token, so it runs over TLS except to a loopback address. Against a server whose gRPC is not TLS, the handshake fails before the token is sent, and the tunnel stays up without live route updates.

The lease

The server removes a peer whose lease ends (LEASE_TTL, 24 h by default). While the tunnel is up the helper renews it (POST /api/v1/heartbeat) at half of what the lease has left, between 30 s and 10 minutes (the longest a device stays cut off after the server restarts), and acts on the answer:

The server answers The helper
a renewed lease renews again at half of it
404 not_enrolled: it forgot the device (restarted, or the lease ran out) enrolls again, with the same key and tenant, and brings the tunnel up from that answer; if that fails too, takes the tunnel down
403 (not_a_member, key_not_registered): access withdrawn takes the tunnel down
401, a 5xx, nothing retries within a minute, sooner than the lease ends

It renews with the last bearer it was given. That is enough for a GitHub token, but not for a go-authn access token, which expires in minutes and which only the app can refresh. So while it runs, pkg/appcore brings the helper a fresh bearer (the renew action) at 40% of what the lease has left, before the helper's own renewal is due. An app restarted under a running tunnel starts doing so at its first Status. renew names no server, key or tenant: it renews the enrollment the helper made itself.

Disconnecting (down) deregisters the peer, giving its address back, rather than leave it held until the lease ends. A helper that is stopping does not, so one restarted by its service manager finds the lease still there. The helper's status reports the lease (lease_expires_at) and why it last failed to keep the tunnel (last_error).

Tenants

A person may belong to several tenants and chooses one per session. appcore.Core:

  • Tenants asks the server, through the helper, which tenants the person may join;
  • SetTenant records the choice in the session; a new sign-in forgets it;
  • Connect enrolls into that tenant. A person in several who has not chosen gets ErrTenantRequired, with the tenants in Status().

App configuration

pkg/appcore reads config.json under os.UserConfigDir()/Claimward/ (~/Library/Application Support on macOS, ~/.config on Linux, %AppData% on Windows), then these environment overrides:

Key Environment Notes
server_url CLAIMWARD_SERVER required; must also be listed in the helper's servers
provider CLAIMWARD_AUTH_PROVIDER github (default), oidc or go-authn
github_client_id CLAIMWARD_GITHUB_CLIENT_ID required for github
oidc_issuer CLAIMWARD_OIDC_ISSUER required for oidc and go-authn
oidc_client_id CLAIMWARD_OIDC_CLIENT_ID required for oidc and go-authn
socket_path CLAIMWARD_HELPER_SOCKET default /var/run/claimward-helper.sock, C:\ProgramData\Claimward\helper.sock on Windows

The helper's own helper.json takes servers (required), group (default admin on macOS, claimward on Linux, Claimward Users on Windows) and socket.

Notes

  • The session store (pkg/tokenstore) is a 0600 JSON file under the user's config dir (os.UserConfigDir()), on every platform.
  • Creating the tun device and changing routes (pkg/wgtun) require elevated privileges; each app delegates this to its privileged helper (pkg/helper).
  • pkg/wgtun implements macOS, Linux and Windows. On Windows the device is a Wintun adapter named Claimward: wintun.dll (https://www.wintun.net, signed by WireGuard LLC) must sit beside the executable that calls wgtun.Up, which the Windows app's packaging takes care of. Address, routes, DNS and MTU are set through winipcfg (the IP Helper API, pure Go, CGO_ENABLED=0), as WireGuard for Windows does.
  • DNS push from the server is applied on Windows; on macOS and Linux it is carried in the protocol but not yet applied by wgtun — TODO.

License

BSD 3-Clause — see LICENSE.

Directories

Path Synopsis
internal
buildinfo
Package buildinfo carries version metadata injected at build time via -ldflags.
Package buildinfo carries version metadata injected at build time via -ldflags.
pkg
appcore
Package appcore is a Claimward app's business logic, shared by the macOS, Linux and Windows apps and independent of how each draws its window and tray: sign-in, the tenant chosen for the session, and driving the privileged helper (pkg/helper) to bring the tunnel up and down.
Package appcore is a Claimward app's business logic, shared by the macOS, Linux and Windows apps and independent of how each draws its window and tray: sign-in, the tenant chosen for the session, and driving the privileged helper (pkg/helper) to bring the tunnel up and down.
auth
Package auth abstracts the interactive sign-in flow behind a pluggable Provider, so the CLI and the platform apps work with several identity providers without caring which one is in use.
Package auth abstracts the interactive sign-in flow behind a pluggable Provider, so the CLI and the platform apps work with several identity providers without caring which one is in use.
browser
Package browser opens a URL in the user's default browser.
Package browser opens a URL in the user's default browser.
client
Package client is the high-level claimward client API shared by the CLI and the platform apps.
Package client is the high-level claimward client API shared by the CLI and the platform apps.
helper
Package helper is the privileged side of a Claimward app: the process that runs as root (or SYSTEM), owns the tunnel, and does the server comms -- shared by the macOS, Linux and Windows apps, each of which only supplies a main that listens.
Package helper is the privileged side of a Claimward app: the process that runs as root (or SYSTEM), owns the tunnel, and does the server comms -- shared by the macOS, Linux and Windows apps, each of which only supplies a main that listens.
helperclient
Package helperclient is the app's side of the privileged helper's socket.
Package helperclient is the app's side of the privileged helper's socket.
hproto
Package hproto is the line protocol between an unprivileged Claimward app and its privileged helper: one JSON request per connection, one JSON response back.
Package hproto is the line protocol between an unprivileged Claimward app and its privileged helper: one JSON request per connection, one JSON response back.
oidc
Package oidc implements the OpenID Connect Authorization Code + PKCE flow used by claimward clients to authenticate the user against any standards-compliant identity provider (discovered via the issuer's /.well-known/openid-configuration).
Package oidc implements the OpenID Connect Authorization Code + PKCE flow used by claimward clients to authenticate the user against any standards-compliant identity provider (discovered via the issuer's /.well-known/openid-configuration).
protocol
Package protocol defines the wire contract between the claimward VPN clients (the CLI in this repo and the platform apps such as claimward-vpn-app-osx) and claimward-vpn-server.
Package protocol defines the wire contract between the claimward VPN clients (the CLI in this repo and the platform apps such as claimward-vpn-app-osx) and claimward-vpn-server.
routeclient
Package routeclient watches the server's RouteService over gRPC and reports route updates, so a connected client can apply gateway route changes live without re-enrolling.
Package routeclient watches the server's RouteService over gRPC and reports route updates, so a connected client can apply gateway route changes live without re-enrolling.
tokenstore
Package tokenstore persists the user's session (OIDC tokens and the device's WireGuard private key) on disk with owner-only permissions.
Package tokenstore persists the user's session (OIDC tokens and the device's WireGuard private key) on disk with owner-only permissions.
wgkey
Package wgkey is a thin helper around WireGuard key generation and parsing, shared by the CLI, the platform apps and (for the server's public key) the enrollment flow.
Package wgkey is a thin helper around WireGuard key generation and parsing, shared by the CLI, the platform apps and (for the server's public key) the enrollment flow.
wgtun
Package wgtun brings up a userspace WireGuard tunnel using wireguard-go.
Package wgtun brings up a userspace WireGuard tunnel using wireguard-go.

Jump to

Keyboard shortcuts

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