A CLI for managing VPN connections with automatic TOTP (Time-based One-Time Password) authentication using high performance code and security best practices.
- Native F5 VPN client: a pure-Rust, in-process F5 BIG-IP SSL VPN
implementation (PPP-over-HTTPS). No
openconnect, nosudo-spawned child. - Rootless: runs as your user (keyring intact); the only privilege needed is
CAP_NET_ADMINfor the TUN device + route setup, granted via a file capability (setcap cap_net_admin+ep). TUN/address/route configuration is done in-process via netlink. - Secure Credential Management: Stores PIN and TOTP secret securely in GNOME Keyring
- Automatic OTP Generation: Generates TOTP tokens automatically during connection
- Automatic Reconnection: Detects network interruptions and reconnects with exponential backoff (supervised in-process)
- Guaranteed host restore:
akon vpn offreconciles every networking change (tun, routes, rp_filter, DNS) from a persisted plan β even after a crash. - Health Monitoring: Periodic health checks detect silent VPN failures
- Fast & Lightweight: written in Rust, dependency-light (no external VPN binary)
-
Operating System: Linux (tested on Ubuntu/Debian, RHEL/Fedora). The VPN data plane is Linux-only (TUN + netlink).
-
CAP_NET_ADMIN: needed to create the TUN device and configure routes. Granted once as a file capability on the binary (no sudo at runtime):sudo setcap cap_net_admin+ep "$(command -v akon)" # Requires libcap's setcap: # Ubuntu/Debian: sudo apt install libcap2-bin # RHEL/Fedora: sudo dnf install libcap
Note: file capabilities do not elevate inside a user namespace (rootless-container dev environments) β those still need
sudo/--cap-add NET_ADMIN. Normal bare-metal hosts get true rootless operation. -
polkit rule (no DNS prompts): akon applies the tunnel's DNS via systemd-resolved, which would otherwise prompt for authentication on every connect. akon ships a scoped polkit rule (
/usr/share/polkit-1/rules.d/49-akon-resolved-dns.rules) so DNS applies without any password prompt. It is installed automatically by the deb/rpm packages and bymake install. From source withoutmake install:sudo install -m 644 packaging/polkit/49-akon-resolved-dns.rules \ /usr/share/polkit-1/rules.d/49-akon-resolved-dns.rules -
GNOME Keyring: For secure credential storage
sudo apt install gnome-keyring libsecret-1-dev
-
No
openconnect: akon is a self-contained native client and does not use or require theopenconnectbinary.
Download pre-built packages for your distribution from the GitHub Releases page.
# Install the package
sudo dpkg -i akon_latest_amd64.deb
# If there are dependency issues, run:
sudo apt-get install -f# Install the package
sudo dnf install ./akon-latest-1.x86_64.rpm# Clone the repository
git clone https://github.com/vcwild/akon.git
cd akon
# Build and install (grants the CAP_NET_ADMIN file capability)
make install
# Verify installation
akon --helpWhat make install does:
- Builds the release binary
- Installs to
/usr/local/bin/akon - Grants
cap_net_admin+epon the binary (so akon runs rootless, as your user) - Removes any legacy passwordless-sudo config from older akon versions
Store your VPN credentials securely:
akon setupYou'll be prompted for:
- Server: VPN server hostname (e.g.,
vpn.example.com) - Username: Your VPN username
- PIN: Your numeric PIN
- TOTP Secret: Your TOTP secret key (Base32 encoded)
These credentials are stored in:
- Config file:
~/.config/akon/config.toml(server, username, protocol) - Keyring: GNOME Keyring (PIN and TOTP secret - encrypted)
akon vpn onWhat happens (all in-process β akon is the VPN client):
- Loads config from
~/.config/akon/config.toml - Retrieves PIN and TOTP secret from keyring
- Generates current TOTP token
- Connects natively over TLS (auth β config β PPP), configures the TUN device and routes via netlink
- Carries the data plane and supervises health/reconnection in-process
- Reports IP address when connected (stays running until Ctrl-C or
akon vpn off)
akon vpn statusOutputs:
- Connected (exit code 0): Shows IP, device, duration, PID
- Not connected (exit code 1): No active connection
- Stale state (exit code 2): Process died, cleanup needed
akon vpn offDisconnect flow:
- Signals the running akon VPN process to stop (it drops the TUN and reverts in-process)
- Replays the persisted host-teardown plan to reconcile the host (removes the
tun, the VPN-server pin route, restores
rp_filter, reverts DNS) β idempotent and works even if the process was already killed - Cleans up state file
Generate OTP token for manual use:
akon get-passwordOutputs PIN+TOTP combined password (does not initiate connection).
~/.config/akon/config.toml
[vpn]
server = "vpn.example.com"
username = "your.username"
protocol = "f5" # F5 SSL VPN protocol
# Optional settings
timeout = 60
no_dtls = false
lazy_mode = true # Connect VPN when running 'akon' without argumentsWhen lazy_mode = true is set in your configuration, running akon without any arguments will automatically connect to the VPN:
# With lazy mode enabled, these are equivalent:
akon
akon vpn on
# With lazy mode disabled, akon without args shows help
akon # Shows usage informationThis feature is perfect for quick VPN connections - just type akon and go!
akon is a native, in-process F5 BIG-IP SSL VPN client β there is no
openconnect and no native_backend flag (the native path is always used for
protocol = "f5"). It performs the full handshake over TLS
(auth β XML config β /myvpn tunnel upgrade β PPP LCP/IPCP), configures the TUN
device and routes in-process via netlink, applies DNS on systemd-resolved
systems (Fedora/Ubuntu, with resolvconf/resolv.conf fallbacks), and
supervises health/reconnection in-process (honoring the [reconnection]
settings). It runs as your user with a cap_net_admin+ep file capability β no
sudo. It is Linux-only.
Migrating from an older akon? Drop any
native_backend = ...line from your config (it is ignored now), ensure the binary has the capability (sudo setcap cap_net_admin+ep "$(command -v akon)", or just re-runmake install), and stop installingopenconnect.
A deliberate, opt-in sign-off test (tests/production_signoff_test.rs) connects
the native backend to your own configured F5 server using your local
config and keyring credentials, reaches Connected, and disconnects
immediately. No server, username, or network is hardcoded in akon β it reads
everything from ~/.config/akon/config.toml and the keyring at run time. It
creates no TUN device and changes no routes/DNS, so it does not disrupt your
connectivity. It is disabled by default and requires an explicit double opt-in:
AKON_SIGNOFF_PRODUCTION=1 \
AKON_SIGNOFF_ACK=I_UNDERSTAND_THIS_HITS_PRODUCTION \
cargo test --test production_signoff_test -- --nocaptureThe control-plane sign-off above has been validated against a real production F5 appliance (authenticated with PIN+OTP, completed the full handshake + PPP to network-up, assigned a tunnel IP, disconnected cleanly).
A second, deeper gate (tests/production_dataplane_signoff_test.rs) opens a
real TUN device, connects to your appliance, then routes one target you
specify (AKON_SOAK_PROBE_TARGET, a host reachable only via the VPN) through the
tunnel as a /32 route and verifies it becomes reachable β proving user traffic
traverses the native data plane. It never installs a default route (so it
cannot hijack your connectivity), removes the route and tears down the TUN on
every exit (including failures), and is bounded. It needs root (CAP_NET_ADMIN)
and is triple-gated:
Use the helper, which builds as your user, generates the PIN+OTP as your user
(akon get-password), then runs the test binary, passing the password via
AKON_SOAK_PASSWORD (never printed):
AKON_SOAK_PROBE_TARGET=intranet.example.com ./test-support/run-dataplane-signoff.shRootless runtime is fully implemented: with
setcap cap_net_admin+epon the binary, akon configures the TUN and routes in-process via netlink as your user β nosudo. The containerized proof istest-support/run-rootless-validation.sh(runs the data plane as a non-root user inside a container). The soak still uses elevation only where your environment requires it for/dev/net/tun.
The probe target may be VPN-only: if its name doesn't resolve before the
tunnel is up, the soak routes the negotiated VPN DNS server through the tunnel
and resolves the name through the tunnel (which itself proves the data plane
carries traffic). You can also pass an IP literal
(AKON_SOAK_PROBE_TARGET=10.10.x.y:443) to skip DNS entirely. The whole soak is
bounded by a hard 30s deadline and tears down the TUN + all routes on every exit.
The probe target accepts a bare host, host:port, or a full URL (port defaults
to 443). Equivalent manual form (build first, then sudo the binary):
BIN=$(cargo test --test production_dataplane_signoff_test --no-run \
--message-format=json | sed -n 's/.*"executable":"\([^"]*production_dataplane_signoff_test[^"]*\)".*/\1/p' | tail -1)
sudo -E AKON_F5_DEBUG=1 \
AKON_SIGNOFF_PRODUCTION=1 \
AKON_SIGNOFF_ACK=I_UNDERSTAND_THIS_HITS_PRODUCTION \
AKON_SOAK_PROBE_TARGET=intranet.example.com \
"$BIN" --nocaptureThe route/teardown mechanics are rehearsed locally on a real TUN by the gated
test native_f5_real_tun_tests (sudo -E AKON_RUN_TUN_TESTS=1 cargo test -p akon-core --features test-actors --test native_f5_real_tun_tests), so the
production run only adds the live appliance.
akon automatically detects network interruptions and reconnects your VPN with intelligent retry logic.
Add a [reconnection] section to your config to enable automatic reconnection:
[vpn]
server = "vpn.example.com"
username = "your.username"
protocol = "f5"
[reconnection]
# Required: HTTP/HTTPS endpoint to check connectivity
health_check_endpoint = "https://your-internal-server.example.com/"
# Optional: Customize retry behavior (defaults shown)
max_attempts = 3 # Maximum reconnection attempts (default)
base_interval_secs = 5 # Initial retry delay
backoff_multiplier = 2 # Exponential backoff multiplier
max_interval_secs = 60 # Maximum delay between attempts
consecutive_failures_threshold = 1 # Health check failures before reconnection (default)
health_check_interval_secs = 10 # How often to check health (default)The name "akon" is a playful triple entendre:
- Memorable Command: A short, 4-letter command that's easy to type and remember
- Project Evolution: An acronym to auto-openconnect, the predecessor project
- Cultural Reference: A nod to the famous singer Akon, because connecting to VPN should be as smooth as his music
akon is a native, in-process F5 VPN client (no external process):
- Connects to the F5 appliance over TLS and runs the full protocol in-process
(HTTP auth β XML config β
/myvpntunnel upgrade β PPP LCP/IPCP), all behind aTransportseam. - Carries the data plane itself: a bidirectional pump moving IP packets between a real Linux TUN device and the F5/PPP framing.
- Configures the interface, addresses, and routes in-process via netlink
(rootless under a
cap_net_admin+epfile capability); applies DNS via the system resolver. - Records every host mutation in a persisted teardown plan so
akon vpn offalways restores the host. - Built test-first against an in-memory test-actors framework (the same
VpnBackendboundary is exercised by aSimulatedBackendoracle), with byte-exact protocol vectors and netns/container/production sign-off tests.
This design removes the external openconnect dependency, the sudo-spawned
child, and the FFI of earlier versions.
flowchart TB
User([π€ User]) -->|$ akon vpn on| CLI[CLI Entry Point]
CLI --> Config[Load Config<br/>~/.config/akon/config.toml]
Config --> Keyring[π Retrieve Credentials<br/>GNOME Keyring]
Keyring -->|PIN + TOTP Secret| TOTP[Generate TOTP Token<br/>Time-based OTP]
TOTP -->|PIN+OTP| Connector[Native F5 Backend<br/>in-process client]
Connector -->|TLS: auth β config β PPP| OC[π TUN device<br/>netlink routes]
OC -->|LifecycleEvents| Parser[Data-plane pump<br/>TUN β F5/PPP framing]
Parser -->|Connection Events| Monitor[Connection Monitor<br/>State Machine]
Monitor -->|Connected Event| State[Update State<br/>/tmp/akon_vpn_state.json]
State --> Success[β VPN Connected<br/>IP Assigned]
Success -.->|periodic checks| Health[π₯ Health Check<br/>HTTP Probe]
Health -->|HTTP GET| Endpoint[Internal Endpoint<br/>Connectivity Test]
Endpoint -->|Success| Continue[Continue Monitoring]
Endpoint -->|Failure| Threshold{Consecutive<br/>Failures β₯ threshold?}
Threshold -->|No| Continue
Threshold -->|Yes| Reconnect[π Reconnection<br/>Exponential Backoff]
Monitor -.->|NetworkManager D-Bus| NM[πΆ Network Events<br/>WiFi/Ethernet Changes]
NM -.->|suspend/resume<br/>WiFi change| Reconnect
Reconnect -->|backoff: 5sβ10sβ20sβ40sβ60s, in-process| Connector
style User fill:#34495e,stroke:#2c3e50,stroke-width:3px,color:#fff
style CLI fill:#3498db,stroke:#2980b9,stroke-width:3px,color:#fff
style Config fill:#95a5a6,stroke:#7f8c8d,stroke-width:2px,color:#fff
style Keyring fill:#f39c12,stroke:#e67e22,stroke-width:3px,color:#fff
style TOTP fill:#16a085,stroke:#138d75,stroke-width:2px,color:#fff
style Connector fill:#2980b9,stroke:#1f618d,stroke-width:3px,color:#fff
style OC fill:#27ae60,stroke:#229954,stroke-width:4px,color:#fff
style Parser fill:#8e44ad,stroke:#7d3c98,stroke-width:2px,color:#fff
style Monitor fill:#2c3e50,stroke:#1c2833,stroke-width:3px,color:#fff
style State fill:#34495e,stroke:#2c3e50,stroke-width:2px,color:#fff
style Success fill:#27ae60,stroke:#229954,stroke-width:4px,color:#fff
style Health fill:#9b59b6,stroke:#8e44ad,stroke-width:3px,color:#fff
style Endpoint fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
style Continue fill:#16a085,stroke:#138d75,stroke-width:2px,color:#fff
style Threshold fill:#e67e22,stroke:#d35400,stroke-width:3px,color:#fff
style Reconnect fill:#e74c3c,stroke:#c0392b,stroke-width:4px,color:#fff
style NM fill:#9b59b6,stroke:#8e44ad,stroke-width:2px,color:#fff
linkStyle 0 stroke:#3498db,stroke-width:3px
linkStyle 1 stroke:#95a5a6,stroke-width:2px
linkStyle 2 stroke:#f39c12,stroke-width:3px
linkStyle 3 stroke:#16a085,stroke-width:2px
linkStyle 4 stroke:#2980b9,stroke-width:3px
linkStyle 5 stroke:#27ae60,stroke-width:4px
linkStyle 6 stroke:#8e44ad,stroke-width:2px
linkStyle 7 stroke:#2c3e50,stroke-width:3px
linkStyle 8 stroke:#34495e,stroke-width:2px
linkStyle 9 stroke:#27ae60,stroke-width:3px
linkStyle 10 stroke:#9b59b6,stroke-width:2px,stroke-dasharray: 5 5
linkStyle 11 stroke:#3498db,stroke-width:2px
linkStyle 12 stroke:#16a085,stroke-width:2px
linkStyle 13 stroke:#e67e22,stroke-width:2px
linkStyle 14 stroke:#16a085,stroke-width:2px
linkStyle 15 stroke:#e74c3c,stroke-width:3px
linkStyle 16 stroke:#9b59b6,stroke-width:2px,stroke-dasharray: 5 5
linkStyle 17 stroke:#9b59b6,stroke-width:2px,stroke-dasharray: 5 5
linkStyle 18 stroke:#e74c3c,stroke-width:3px
Key Components:
- CLI Layer: Command handlers for
setup,vpn on/off/status,get-password - Config Management: TOML configuration with secure credential storage
- Authentication: TOTP generation, keyring integration, password assembly
- Native F5 backend: pure-Rust F5 client β framing, PPP, auth, config, HTTP, TLS transport, and orchestration (
backend.rs) - netlink & TUN: in-process link/address/route setup and the real TUN device
- Host teardown: persisted plan + idempotent reconciler used by
vpn off - Health Monitoring: Periodic endpoint checks for silent failures
- Reconnection: Exponential backoff retry logic (supervised in-process)
- State Management: Persistent connection state tracking
Automatically detects systemd and logs to journal:
# View logs
journalctl -f -u akon
# View with priority filter
journalctl -f -u akon -p infoakon/
βββ akon-core/ # Core library
β βββ src/
β β βββ auth/ # OTP, keyring, password generation
β β βββ config/ # TOML configuration
β β βββ vpn/ # VPN connection management
β β β βββ backend.rs # VpnBackend boundary + lifecycle events
β β β βββ transport.rs # Transport / TunDevice / DnsApplier seams
β β β βββ f5/ # Native F5 backend
β β β β βββ backend.rs # Orchestration (impl VpnBackend)
β β β β βββ framing.rs ppp.rs auth.rs config.rs http.rs # protocol layers
β β β β βββ tls_transport.rs netlink.rs tun.rs dns.rs # real I/O adapters
β β β β βββ teardown.rs # host-teardown plan + reconciler
β β β βββ testkit/ # in-memory actors + SimulatedBackend (test-only)
β β βββ error.rs # Error types
β βββ tests/ # Unit tests
βββ src/ # CLI application
β βββ cli/ # Command implementations
β β βββ setup.rs # Setup command
β β βββ vpn.rs # VPN commands (on/off/status)
β βββ main.rs # Entry point
βββ tests/ # Integration tests# Run all tests
cargo test
# Run with coverage (requires cargo-tarpaulin)
cargo install cargo-tarpaulin
cargo tarpaulin --out HtmlView coverage report
open tarpaulin-report.htmlTesting and mock keyring
For tests that need a keyring implementation (CI or local), akon-core provides a lightweight "mock keyring" implementation which stores credentials in-memory. This is useful for unit and integration tests that must not interact with the system keyring.
The mock keyring and its test-only dependency (lazy_static) are behind a feature flag
so they are opt-in for consumers of akon-core:
- Feature name:
mock-keyring - Optional dependency:
lazy_static(enabled only whenmock-keyringis enabled)
Run tests that require the mock keyring with
# Run a single integration test using the mock keyring
cargo test -p akon-core --test integration_keyring_tests --features mock-keyring -- --nocaptureNotes:
lazy_staticis declared as an optional dependency enabled bymock-keyringand also present as adev-dependencyso developers can run tests locally without enabling the feature.- This means the
lazy_staticcrate is not linked into production binaries unless a consumer enablesmock-keyringexplicitly. - The mock keyring mirrors production retrieval behavior for PINs (the runtime truncates retrieved PINs to 30 characters). Tests validate truncation and password assembly.
Contributions are welcome! Please:
- Follow existing code style
- Add tests for new features
- Update documentation
- Run
cargo clippybefore submitting - Ensure all tests pass:
cargo test
This project is licensed under the MIT license.
Note: This tool is designed for F5 SSL VPN protocol. Other protocols may work but are not officially supported.