Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
# CI Pipeline for akon
# Validates code quality, runs tests, and verifies builds on every push and pull request
# Validates code quality, runs tests, and verifies builds on every push and PR.
#
# Test strategy (must match the runtime: a GitHub runner has NO root TUN access,
# NO netlink privileges, NO production F5 appliance, and NO GNOME Keyring daemon):
# - We run the PURE protocol layers + the OFFLINE native e2e/equivalence/
# dataplane suites (built with `--features test-actors`, driven by the
# in-memory test actors + a loopback TLS server — no root, no network egress).
# - The privileged/online suites SELF-SKIP here: the real-TUN, netns, podman,
# and production sign-off tests gate on env flags / capabilities / podman and
# return early when those are absent.
# - The real GNOME-Keyring tests self-skip when no secret-service daemon is
# present (the case on CI); keyring logic is covered deterministically by the
# dedicated `mock-keyring` job below.
name: CI

# Trigger on pull requests
on:
pull_request:

jobs:
# User Story 1: Code Quality Validation
# Validates code formatting and linting rules
# Code quality: formatting + clippy (lint the gated test code too).
lint:
name: Lint (rustfmt + clippy)
runs-on: ubuntu-latest
Expand All @@ -28,15 +38,14 @@ jobs:
- name: Check code formatting
run: cargo fmt --all --check

- name: Run clippy linter
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run clippy linter (incl. test-actors test code)
run: cargo clippy --workspace --all-targets --features test-actors -- -D warnings

# User Story 2: Automated Test Execution
# Runs all unit and integration tests across workspace
# Automated tests: pure layers + offline native suites. Privileged/online and
# real-keyring tests self-skip on the runner (see strategy note above).
test:
name: Test
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
rust: [stable]
Expand All @@ -54,11 +63,10 @@ jobs:
with:
toolchain: ${{ matrix.rust }}

- name: Run tests
run: cargo test --workspace --verbose
- name: Run tests (pure + offline native, test-actors enabled)
run: cargo test --workspace --features test-actors --verbose

# User Story 2b: Run feature-gated integration test using mock-keyring
# This job runs the integration test that depends on the `mock-keyring` feature.
# Keyring logic, deterministically, via the in-memory mock-keyring backend.
mock-keyring-test:
name: Test (mock-keyring integration)
runs-on: ubuntu-latest
Expand All @@ -78,11 +86,9 @@ jobs:
toolchain: ${{ matrix.rust }}

- name: Run mock-keyring integration test
# Run only the integration test that is gated by the `mock-keyring` feature
run: cargo test -p akon-core --test integration_keyring_tests --features mock-keyring -- --nocapture

# User Story 3: Build Verification
# Verifies successful compilation in release mode
# Build verification (release; test-only code is gated out).
build:
name: Build (release)
runs-on: ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ jobs:
curl \
rpm-build \
rpmdevtools
# Install runtime dependencies (openconnect + dbus)
# Install build dependencies (dbus + setcap)
dnf install -y \
openconnect \
libcap \
dbus-devel \
pkgconf-pkg-config

Expand Down Expand Up @@ -187,9 +187,9 @@ jobs:
curl \
rpm-build \
rpmdevtools
# Install runtime dependencies (openconnect + dbus)
# Install build dependencies (dbus + setcap)
dnf install -y \
openconnect \
libcap \
dbus-devel \
pkgconf-pkg-config

Expand Down
70 changes: 55 additions & 15 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
<!--
SYNC IMPACT REPORT
==================
Version: 0.0.0 → 1.0.0 (Initial Constitution)
Date: 2025-10-08
Version: 1.0.0 → 1.1.0 (Add Principle VI: Test Actors & Seam-Isolated Testing)
Date: 2026-06-21

CHANGES:
- Initial constitution creation for auto-openconnect (akon) project
- Established 5 core principles: Security-First, Modular Architecture, Test-Driven Development,
Observability & Logging, CLI-First Interface
- Added Security Requirements section
- Added Development Standards section
- Defined governance and amendment procedures
- Added Principle VI: Test Actors & Seam-Isolated Testing (NON-NEGOTIABLE),
codifying the methodology proven while building the test actors framework
(spec 005) and the native F5 backend (spec 006): isolate heavy/real-world
integrations (OS, network, TLS, processes) behind seams; emulate them with
in-memory actors as ground truth; validate behavior offline and
deterministically; then confirm with one bounded REAL end-to-end test on the
production path before acknowledging a replacement.
- Expanded Development Standards with a "Test Methodology" subsection
(seams, actors, backend-agnostic boundaries, no-hang discipline,
real end-to-end confirmation).
- Updated Governance code-review checklist to require seam/actor compliance and
hang-proof tests.

PRINCIPLES DEFINED:
1. Security-First Architecture
2. Modular Architecture
3. Test-Driven Development (NON-NEGOTIABLE)
4. Observability & Logging
5. CLI-First Interface
6. Test Actors & Seam-Isolated Testing (NON-NEGOTIABLE) ← NEW

TEMPLATES REQUIRING UPDATES:
✅ plan-template.md - Constitution Check section aligns with all 5 principles
✅ spec-template.md - Security and testing requirements align
✅ tasks-template.md - Task categorization supports security, testing, and modularity
✅ commands/*.md - Generic guidance preserved, no agent-specific references

FOLLOW-UP TODOS: None
✅ plan-template.md - Constitution Check updated to include Principle VI and bump version reference
✅ spec-template.md - Testing/seam requirements align (no structural change required)
✅ tasks-template.md - Task categorization supports seam/actor tests (no structural change required)

FOLLOW-UP TODOS:
- Pre-existing drift: constitution still references Python tooling (mypy/ruff/pytest,
*.py modules) although the codebase is Rust. Not addressed in this amendment;
track separately.
-->

# Auto-OpenConnect (Akon) Constitution
Expand Down Expand Up @@ -98,6 +107,23 @@ FOLLOW-UP TODOS: None

**Rationale**: CLI-first design enables automation (systemd timers, NetworkManager dispatchers) and scripting without GUI dependencies.

### VI. Test Actors & Seam-Isolated Testing (NON-NEGOTIABLE)

**Every behavior that depends on a heavy or real-world integration MUST be testable offline, deterministically, and without hanging — by isolating the integration behind a seam and emulating it with an in-memory test actor that serves as ground truth.**

This principle codifies the methodology that produced the test actors framework (`akon-core/src/vpn/testkit/`) and the native F5 backend. It applies to anything that would otherwise require real infrastructure to test: the operating system (process spawn/signal/discovery, TUN devices, routing), the network (TLS sockets, HTTP endpoints, DTLS, reachability), external binaries (`openconnect`, `pgrep`, `kill`), root privileges, or wall-clock time.

- **Seams over real I/O**: Heavy integrations MUST be accessed through an explicit interface (a Rust trait such as `Transport`, `TunDevice`, `SystemEffects`, or `VpnBackend`) — never via hard-coded direct calls scattered through logic. Each seam has a real production implementation and a test implementation.
- **Durable, behavior-shaped boundaries**: The primary abstraction MUST be expressed in terms the project will still own after a dependency is removed (e.g. connection lifecycle events), NOT in terms of the current implementation's artifacts (e.g. a child process's stdout). Implementation-specific seams (like `SystemEffects` for the openconnect path) are permitted but MUST be internal details of one implementation and deletable with it.
- **Actors as ground truth**: Test implementations of seams MUST be in-memory actors (a fake server, a peer, a registry, a controllable network) that emulate real behavior faithfully and reuse the real codecs/state machines wherever possible (e.g. the fake F5 server drives the genuine framing/PPP code). They MUST perform no real I/O, require no root, and never touch the host network.
- **Backend-agnostic scenario suites**: When replacing a component, the SAME scenario suite MUST validate the old and new implementations against the shared boundary, and equivalence MUST be demonstrated before the replacement may become the default.
- **No-hang discipline**: Tests MUST be deterministic and bounded. Every wait on I/O MUST have a timeout; every in-memory transport/channel MUST signal EOF/close (including on drop) so consumer loops terminate. A test that can hang is a defect, not an inconvenience — the fix is to bring the integration into the actors model, not to leave a blocking test.
- **Real end-to-end confirmation**: Emulation proves protocol/logic correctness; it does not by itself acknowledge a replacement. A replacement of a real integration MUST also be confirmed by at least one **real** end-to-end test that exercises the production seam implementation (e.g. a genuine TLS-over-TCP handshake against a local server), kept bounded so it cannot hang and self-contained so it needs no external infrastructure, root, or non-loopback network.
- **Feedback loop**: When something is too complex or too heavy to test, the required response is to extend the actors model with the missing capability (a new seam or actor), then test against it — iterating until the behavior is covered. Writing a slow, flaky, or hanging test instead is prohibited.
- **Zero release cost**: Test actors and in-memory implementations MUST be gated out of release builds (e.g. behind a `test-actors` feature / `cfg(test)`), so they add no runtime cost or attack surface to shipped binaries. The seam traits and real implementations remain in production.

**Rationale**: akon's core job — establishing VPN tunnels — is exactly the kind of behavior that is expensive, privileged, and disruptive to test against reality (it needs a server, root, and would drop the developer's own connectivity). Seam isolation plus in-memory actors make that behavior testable on every change, while a single bounded real end-to-end test guards against the divergence between emulation and production I/O (such as TLS read coalescing). This is what makes risky changes — above all, removing the `openconnect` dependency — safe to develop test-first and prove equivalent before shipping.

## Security Requirements

### Credential Isolation
Expand Down Expand Up @@ -130,6 +156,19 @@ FOLLOW-UP TODOS: None
- All PRs MUST pass: unit tests (pytest), type checking (mypy), linting (ruff), integration tests (keyring/file I/O).
- Security-critical modules MUST have dedicated test files: `test_auth.py`, `test_keyring_utils.py`, `test_password_generator.py`.

### Test Methodology (Principle VI in practice)

This section is the operational guide for satisfying Principle VI. It is the default way features touching the OS, network, processes, TLS, or privileged operations are built.

- **Identify the seam first.** Before implementing anything that does real I/O, define the trait that abstracts it (read/write byte stream, OS effects, TUN device, connection backend). Logic depends on the trait, not on concrete sockets/commands.
- **Pure layers stay pure.** Decompose protocols into pure, deterministic units (framing/codecs, state machines, parsers) that are testable with byte-exact vectors and need no I/O at all. Validate these against ground truth (e.g. the reference implementation's wire format) with explicit test vectors.
- **Provide two implementations per seam.** A real one (production) and an in-memory actor (test). The actor reuses the real pure layers so tests exercise genuine code, not a re-mock of it.
- **Drive tests with scenarios, not ad-hoc setup.** Compose real-world situations declaratively and assert on an ordered timeline of observable, backend-agnostic events. Reuse one scenario suite across implementations to prove equivalence.
- **Bound everything.** Wrap handshakes/loops in `tokio::time::timeout`; ensure in-memory transports/channels report EOF on close and on drop. No unbounded `recv`/`read` without a deadline.
- **Confirm on the real path.** Add at least one bounded, self-contained real end-to-end test (e.g. a local TLS server on loopback with a self-signed cert) for any replacement of a real integration. This is what catches emulation/production divergence (e.g. TLS coalescing post-`/myvpn` PPP bytes).
- **Iterate the framework, not the workaround.** If a behavior can't be tested cleanly, extend the actors framework with the missing seam/actor and circle back — never settle for a slow, flaky, or hanging test.
- **Gate test code out of releases.** Keep actors/in-memory impls behind a test feature/`cfg(test)`; ship only seams + real implementations.

### Documentation

- README MUST include: quick start, security best practices, troubleshooting, configuration examples.
Expand Down Expand Up @@ -159,7 +198,8 @@ All code reviews MUST verify:
- Test coverage for new code paths.
- Logging completeness for state changes.
- CLI interface consistency (exit codes, output format).
- **Seam & test-actor compliance (Principle VI)**: heavy/real integrations are behind a seam with an in-memory actor; behavior is tested offline and deterministically; replacements include a bounded real end-to-end test; no test can hang; test-only code is gated out of release builds.

Complexity that violates modularity principles MUST be justified in commit messages or rejected.

**Version**: 1.0.0 | **Ratified**: 2025-10-08 | **Last Amended**: 2025-10-08
**Version**: 1.1.0 | **Ratified**: 2025-10-08 | **Last Amended**: 2026-06-21
3 changes: 2 additions & 1 deletion .specify/templates/plan-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

Verify compliance with Auto-OpenConnect Constitution v1.0.0:
Verify compliance with Auto-OpenConnect Constitution v1.1.0:

- [ ] **Security-First**: Are credentials stored only in GNOME Keyring? No plaintext secrets in code/config/logs?
- [ ] **Modular Architecture**: Is functionality decomposed into independent modules with clear boundaries?
- [ ] **Test-Driven Development**: Are tests written before implementation? Security modules >90% coverage?
- [ ] **Observability**: Are all state changes logged to systemd journal? No secrets in logs?
- [ ] **CLI-First Interface**: Is functionality accessible via CLI with composable outputs?
- [ ] **Test Actors & Seam-Isolated Testing**: Are heavy/real integrations (OS, network, TLS, processes, root, time) behind a seam (trait) with an in-memory actor as ground truth? Is behavior tested offline, deterministically, and hang-proof (bounded waits, EOF-on-close/drop)? For any replacement of a real integration, is there a bounded real end-to-end test on the production seam? Is test-only code gated out of release builds?

**Security-Critical Changes** (require extra scrutiny):
- [ ] OAuth token handling
Expand Down
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Changelog

All notable changes to akon are documented here. This project adheres to
[Semantic Versioning](https://semver.org/).

## [2.0.0] — 2026-06-21

### ⚠️ Breaking changes

akon is now a **native, in-process F5 BIG-IP SSL VPN client** written in pure
Rust. The `openconnect` delegation has been **removed entirely**. See
`docs/adr/0002-remove-openconnect-native-f5-is-the-only-backend.md`.

- **`openconnect` is no longer used or required.** akon no longer spawns
`openconnect` (or any child process) for the VPN protocol, and the
`openconnect`/`procps` package dependencies are gone.
- **Runtime model changed: no `sudo`.** akon runs as your user (so the keyring
stays accessible). The only privilege needed is `CAP_NET_ADMIN` for the TUN
device and route setup, granted once as a **file capability**:

```bash
sudo setcap cap_net_admin+ep "$(command -v akon)"
```

Packaging post-install scripts and `make install` now do this automatically
(and remove the legacy `/etc/sudoers.d/akon` passwordless-sudo file). Requires
`libcap` (`setcap`): `apt install libcap2-bin` / `dnf install libcap`.
- **Config: the `native_backend` flag is removed.** The native backend is always
used for `protocol = "f5"`. A leftover `native_backend = …` line is harmlessly
ignored.
- **Protocol scope is F5.** Other openconnect protocol identifiers remain
parseable in config for forward-compatibility but are not implemented by the
native client.

### Added

- Native F5 client: F5 framing (encap + HDLC/FCS16), PPP (LCP/IPCP/IP6CP)
negotiation, HTTP auth + XML config, TLS transport, and orchestration behind a
backend-agnostic `VpnBackend` boundary — all validated test-first against an
in-memory test-actors framework and byte-exact wire vectors.
- **In-process netlink** configuration of the TUN device, addresses, MTU, and
routes (no `ip`/`sysctl` child processes), enabling true rootless operation.
- **Guaranteed host restore:** `akon vpn off` replays a persisted host-teardown
plan (tun, server-pin route, `rp_filter`, DNS) — idempotent, and works even if
the `vpn on` process was killed.
- In-process health-checked reconnection (honors the `[reconnection]` config).
- Containerized rootless validation and gated production sign-off tests.

### Removed

- `openconnect_backend`, `cli_connector`, `output_parser`, the openconnect
`process` module, `connection_event`, `system_effects`, and the spawned
reconnection daemon.
- Dependencies: `which`, `bindgen`, `daemonize` (and `regex` from akon-core).
- openconnect-specific error variants (`OpenConnectError`, `ProcessSpawnError`,
`TerminationError`, `ParseError`).

### Migration

1. Update akon (or `make install`).
2. Ensure the capability is set: `getcap "$(command -v akon)"` should show
`cap_net_admin=ep`; if not, run the `setcap` command above.
3. Remove any `native_backend = …` line from `~/.config/akon/config.toml`
(optional — it is ignored).
4. Run akon **without** `sudo`: `akon vpn on`. You may uninstall `openconnect`.
Loading
Loading