- Add definitions.md: normative terminology disambiguation (Interface, Service, Transport, Token, Identity, Domain, Scope, CredentialProvider, etc.) - Add credentials.md: CredentialProvider trait and CredentialSet enum for outbound auth, mirroring IdentityProvider pattern for inbound auth - Rewrite interface.md: StreamInterface/MessageInterface split (ADR-035), InterfaceRequest/InterfaceResponse, HttpInterface/DnsInterface stubs, ListenerConfig with Stream/Http/Dns variants, credential presentation table - Update auth.md: API keys in DynamicConfig (ADR-037), credential presentation per (Transport, Interface) pair, ApiKeyEntry struct in AuthPolicy - Update configuration.md: API keys, ListenerConfig with Http/Dns variants, expanded TOML config examples - Update call-protocol.md: resolve OQ-IF-01 (InterfaceEvent carries EventEnvelope + Identity), add MessageInterface awareness to protocol adapter layer - Update overview.md: three-layer model now includes StreamInterface/ MessageInterface, CredentialProvider/CredentialSet exports, definitions.md reference, ADRs 035-037 - Update open-questions.md: resolve OQ-IF-01, OQ-IF-02, add OQ-P2-01 through OQ-P2-04, add OQ-CP-01 through OQ-CP-04, add OQ-DEF-01, OQ-DEF-03, OQ-DEF-08 - Update README.md: add definitions.md, credentials.md, ADRs 035-037, phase2 research docs, current state description Key architectural decisions: - ADR-035: StreamInterface/MessageInterface split (two Layer 2 traits) - ADR-036: CredentialProvider as core type (outbound auth, alknet_core::credentials) - ADR-037: API keys as DynamicConfig auth (hash-verified bearer tokens)
18 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 2026-06-07 |
Alknet Overview
Purpose
Alknet is a self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol. It enables:
- Private tunneling of services (Postgres, Redis, internal APIs) over SSH
- Censorship circumvention — SSH over TLS on port 443 looks like HTTPS to DPI
- NAT traversal — iroh transport allows peer-to-peer connections without public IPs or port forwarding
- Service mesh connectivity — a lightweight transport layer for the pubsub/operations event system
The core insight: SSH tunnels work because SSH is fundamental infrastructure. Blocking it breaks the internet. Alknet makes SSH tunneling accessible through a simple CLI with pluggable transports.
Crate Structure
Alknet is decomposed into six crates with a strict acyclic dependency graph (ADR-027):
| Crate | Purpose | Exists Now? |
|---|---|---|
| alknet-core | Transport, SSH, call protocol, config, auth types, OperationSpec, Interface trait |
Yes |
| alknet-napi | Node.js native addon via napi-rs | Yes |
| alknet-secret | BIP39, SLIP-0010 HD key derivation, AES-256-GCM, SecretProtocol irpc service |
Phase 2+ |
| alknet-storage | SQLite-backed metagraph, identity tables, ACL graph, honker, StorageProtocol |
Phase 2+ |
| alknet-flowgraph | FlowGraph<N,E> over petgraph, operation graph, call graph |
Phase 2+ |
| alknet (CLI) | Binary that assembles everything with feature flags | Yes |
The four library crates (core, secret, storage, flowgraph) are independent of each other. Dependencies flow upward only: the CLI binary sits at the top and wires concrete implementations together. alknet-storage implements alknet-core's IdentityProvider trait without a crate dependency — the CLI binary provides the bridge.
irpc is behind a feature flag in alknet-core. Nodes that only do SSH tunneling don't need the service layer overhead.
Three-Layer Model
Alknet uses a three-layer model (ADR-026, ADR-035):
| Layer | Responsibility | Examples |
|---|---|---|
| Layer 1: Transport | Produces byte streams (AsyncRead + AsyncWrite + Unpin + Send) |
TCP, TLS, iroh, WebTransport (future) |
| Layer 2: Interface | Two categories: StreamInterface (consumes transport stream, produces session) and MessageInterface (handles discrete requests, manages own transport) | Stream: SSH, raw framing. Message: HTTP, DNS |
| Layer 3: Protocol | Carries semantics — operation registry, service calls, events | Call protocol, OperationEnv, operation dispatch |
SSH is an interface, not a transport. DNS is a message interface, not a transport. The three-layer model enables HTTP interfaces (stealth mode byte-peek), DNS control channels, and local service mesh (raw framing) without wrapping SSH inside those transports.
A stream-based connection is always a (Transport, StreamInterface) pair. Message-based interfaces manage their own transport. The protocol layer is agnostic to both.
Service Layer
The irpc service layer decomposes alknet's core responsibilities into independently testable, deployable, and replaceable components (ADR-033, services.md):
- Auth (
AuthProtocol) — verify identities, check credentials - Secret (
SecretProtocol) — derive keys, encrypt/decrypt - Config (
ConfigProtocol) — dynamic config reload - Storage (
StorageProtocol) — graph CRUD, metagraph operations
OperationEnv is the universal composition mechanism. A handler receives context.env.invoke("secrets", "derive", input) and doesn't know whether the dispatch is local (direct function call), in-cluster (irpc service), or cross-node (call protocol EventEnvelope). Three dispatch paths, one handler-facing API.
Phase boundary: Phase 1 ships ConfigIdentityProvider (ArcSwap-backed) and ConfigServiceImpl (ArcSwap-backed) as the only auth and config implementations. The irpc service protocols (AuthProtocol, SecretProtocol, etc.) and the production deployment topology (multi-node with StorageIdentityProvider) are contracted in the specs but will be implemented in Phase 2+. Application services (DockerService, NodeService, agent services) are downstream concerns that build on top of the call protocol and OperationEnv.
Identity
Identity struct and IdentityProvider trait are core types in alknet-core (ADR-029, identity.md):
pub struct Identity {
pub id: String, // Fingerprint (config auth) or account UUID (database auth)
pub scopes: Vec<String>, // Authorization scope strings
pub resources: HashMap<String, Vec<String>>, // Resource-level authorization
}
IdentityProvider decouples alknet-core from identity storage. Phase 1 ships ConfigIdentityProvider (reads from ArcSwap<DynamicConfig.auth>). StorageIdentityProvider (Phase 2+, backed by SQLite) replaces it for production deployments. Both produce the same Identity result.
Exports
Binary: alknet
A single binary with subcommands:
alknet serve — Start the server (accepts SSH connections)
alknet connect — Start the client (opens SSH session, exposes SOCKS5/port-forwards)
Library: alknet-core
The alknet-core crate exports the pluggable components for embedding or programmatic use:
Transporttrait — produces a duplex stream for SSH to run overTcpTransport— direct TCP connectionTlsTransport— TCP + tokio-rustls TLSIrohTransport— iroh QUIC P2P connectionInterfacetrait →StreamInterfacetrait andMessageInterfacetrait (ADR-035)InterfaceSessiontrait —recv()/send()producing/consumingInterfaceEventframesInterfaceRequest/InterfaceResponse— normalized request/response for message interfacesSocks5Server— local SOCKS5 proxy that forwards through SSH channelsPortForwarder— manages local/remote port forwardsServerHandler→SshInterface— russh server handler with configurable auth and channel policiesIdentity/IdentityProvider— core identity types (ADR-029)CredentialProvider/CredentialSet— outbound credential types (ADR-036)OperationSpec— operation registration for call protocol (ADR-025)OperationEnv/OperationContext— universal composition and operation contextConnectOptions/ServeOptions— programmatic configuration structsStaticConfig/DynamicConfig— static/immutable vs, hot-reloadable config (ADR-030)ConfigReloadHandle— programmatic reload of dynamic configForwardingPolicy— rule-based allow/deny for channel targets (ADR-031)ListenerConfig— stream and message listener configuration
Dependencies
| Dependency | Purpose | Crate | Feature-gated |
|---|---|---|---|
russh |
SSH client & server | core | No (core) |
tokio |
Async runtime | core | No (core) |
tokio-rustls |
TLS wrapping | core | Yes (tls) |
rustls |
TLS implementation | core | Yes (tls) |
rustls-acme |
ACME/Let's Encrypt auto-cert | core | Yes (acme) |
iroh |
P2P QUIC transport | core | Yes (iroh) |
irpc |
Streaming RPC service layer | core | Yes (irpc) |
arc-swap |
Lock-free dynamic config | core | No (core) |
serde |
Serialization | core | No (core) |
clap |
CLI argument parsing | CLI | No (CLI) |
toml |
TOML config file | CLI | No (CLI) |
tracing |
Structured logging | core | No (core) |
anyhow / thiserror |
Error handling | core | No (core) |
bip39 |
Mnemonic generation | secret | No (secret) |
ed25519-bip32 |
HD key derivation | secret | No (secret) |
aes-gcm |
AES-256-GCM encryption | secret | No (secret) |
rusqlite |
SQLite (via honker) | storage | No (storage) |
honker |
Event-sourced storage | storage | No (storage) |
petgraph |
Graph data structure | storage, flowgraph | No |
jsonschema |
JSON Schema validation | storage, flowgraph | No |
Note:
tun-rsis no longer a dependency. TUN support is deferred in favor of the externaltun2proxytool (ADR-014).
Architecture Constraints
-
SSH runs over transport, not alongside — The transport layer produces a single
AsyncRead+AsyncWrite+Unpin+Sendstream. SSH runs over that stream viarussh::client::connect_stream()/russh::server::run_stream(). The SSH layer never knows what transport it's on. (ADR-001, ADR-004) -
Three-layer model: Transport, Interface, Protocol — SSH is a StreamInterface (Layer 2), not a transport (Layer 1). HTTP and DNS are MessageInterfaces (Layer 2). A connection is always a (Transport, StreamInterface) pair for stream-based interfaces, or a standalone MessageInterface for message-based ones. The call protocol (Layer 3) is agnostic to both. This enables HTTP interfaces, DNS control channels, and local service mesh without wrapping SSH. (ADR-026, ADR-035)
-
SOCKS5 is the primary client interface — Port forwarding is built on top of SOCKS5-like channel management. For VPN-like "route all traffic" behavior, users run
tun2proxyalongside alknet's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014) -
No logging of tunnel destinations — The server logs auth attempts and connections (for fail2ban) but does not log
channel_open_direct_tcpipdestinations, DNS lookups, or bytes transferred. (ADR-006, ADR-013) -
Programmatic-first API — Configuration via CLI flags, library API structs (
ConnectOptions,ServeOptions), and environment variables. No~/.ssh/configparsing. Optional--configTOML file for reproducible deployments. (ADR-011, ADR-030) -
Feature flags control transport inclusion —
tls,iroh,acme,irpcare feature-gated so the base install is lean. Users opt in to heavier dependencies. -
Authentication is key-based and unified — Ed25519 public key (default) and OpenSSH certificate authority. Same key material for SSH and token auth. Identity resolves through
IdentityProvidertrait, decoupling core from identity storage. (ADR-012, ADR-023, ADR-029) -
NAPI exposes both connect() and serve() — The napi-rs wrapper provides client and server functionality, using napi-rs as the FFI bridge. The NAPI layer is transport-agnostic and not tied to pubsub. (ADR-015, ADR-016)
-
Static/dynamic config split — Transport-level settings (listen address, TLS certs) are immutable after startup. Auth, forwarding policy, and rate limits are hot-reloadable via
ArcSwap<DynamicConfig>. (ADR-030) -
Forwarding policy enforced before proxy spawn — Each
channel_open_direct_tcpipis checked againstForwardingPolicybefore a TCP connection is made. Default-allow preserves current behavior. (ADR-031) -
OperationEnv as universal composition mechanism — Handlers call
context.env.invoke(namespace, op, input)regardless of dispatch path (local, irpc service, remote call protocol). (ADR-033) -
Event boundary discipline — Domain events (Honker streams) stay within the owning service. irpc calls are synchronous and in-cluster. Call protocol
EventEnvelopeis the only thing that crosses node boundaries. (ADR-032) -
Error handling follows a consistent layered pattern — Transport and auth errors cause reconnection (client, with exponential backoff) or connection rejection (server). Channel-level errors (target unreachable, proxy failure) close the individual channel without killing the session. Library API errors propagate via
anyhow::Result/thiserrortypes. CLI reports errors to stderr with appropriate exit codes. NAPI errors are marshalled as JavaScript exceptions.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 001 | Pluggable transport | Transport trait produces AsyncRead+AsyncWrite+Unpin+Send, SSH consumes it |
| 002 | TUN shim separate | Superseded — TUN is deferred, use tun2proxy (ADR-014) |
| 003 | iroh stream join | tokio::io::join(recv, send) combines QUIC halves |
| 004 | SSH over transport | SSH never accesses TCP/iroh/TLS directly |
| 005 | SOCKS5 first | SOCKS5 is the primary interface; TUN is external (tun2proxy) |
| 006 | No logging of tunnel destinations | Server logs auth and connections, not destinations |
| 007 | NAPI single stream | NAPI exposes duplex streams, not SSH multiplexing |
| 008 | ACME/Let's Encrypt | Auto-provision TLS certs, domain and IP paths |
| 009 | Default iroh relay | n0 relay by default, --iroh-relay override |
| 010 | Transport chaining | --proxy works with all transports natively |
| 011 | Programmatic-first | No SSH config files; options are structs, env vars, CLI flags (amended by ADR-030 for optional TOML) |
| 012 | Key + cert-authority | Ed25519 keys + OpenSSH CA; no password auth |
| 013 | Fail2ban-friendly | Structured auth logs + built-in rate limiting |
| 014 | Defer TUN | Use tun2proxy for VPN-like behavior; no alknet-tun binary |
| 015 | napi-rs | Standard Node.js native addon tooling |
| 016 | connect + serve | NAPI exposes both client and server from the start |
| 017 | Stealth mode | Protocol multiplexing on port 443 |
| 018 | Control channel | Reserved alknet-control destination for pubsub |
| 019 | Proxy dual semantics | --proxy routes transport on client, data on server |
| 023 | Unified auth | Same key material for SSH and token auth |
| 024 | Bidirectional call protocol | Both sides can initiate calls |
| 025 | Handler/spec separation | Downstream registers operations without modifying core |
| 026 | Three-layer model | SSH is Layer 2, not Layer 1 |
| 027 | Crate decomposition | Six crates, acyclic deps, feature-gated irpc |
| 028 | Auth as irpc service | IdentityProvider is the contract, irpc is one backend |
| 029 | Identity as core type | Identity and IdentityProvider in alknet-core |
| 030 | Static/dynamic config | ArcSwap for hot-reloadable auth and forwarding |
| 031 | Forwarding policy | Per-identity, per-destination, per-transport rules |
| 032 | Event boundary | Domain events never cross service boundaries |
| 033 | OperationEnv | Universal composition, three dispatch paths |
| 034 | Head/worker | Replaces hub/spoke terminology |
| 035 | StreamInterface/MessageInterface | Two Layer 2 trait categories for stream vs message |
| 036 | CredentialProvider as core type | Outbound credentials in alknet_core::credentials |
| 037 | API keys in DynamicConfig | Hash-verified bearer tokens for service accounts |
Open Questions
See open-questions.md for all open and resolved questions. Key open questions: OQ-15 (QUIC coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01 (Interface session / EventEnvelope relationship).
References
- transport.md — Transport abstraction (Layer 1)
- interface.md — StreamInterface and MessageInterface (Layer 2)
- call-protocol.md — Call protocol (Layer 3)
- auth.md — Unified authentication, API keys, credential presentation
- identity.md — Identity and IdentityProvider
- credentials.md — CredentialProvider and CredentialSet (outbound auth)
- definitions.md — Terminology disambiguation
- configuration.md — StaticConfig, DynamicConfig, ForwardingPolicy
- services.md — irpc service layer, OperationEnv
- server.md — Server acceptance, channel handling
- client.md — Client connection, SOCKS5, port forwarding
- napi-and-pubsub.md — NAPI wrapper and pubsub adapter
- storage.md — alknet-storage: metagraph, identity, ACL
- flowgraph.md — alknet-flowgraph: call graph, operation graph
- secret-service.md — alknet-secret: BIP39, SLIP-0010, AES-GCM
- Feasibility Assessment
- russh API — SSH client/server library
- Dispatch — Reference implementation of russh port forwarding
- iroh — P2P QUIC connections
- tun2proxy — Recommended external TUN-to-SOCKS5 tool
- irpc — iroh streaming RPC
- Production certbot setup — Let's Encrypt on our infrastructure
- Production fail2ban setup — fail2ban with nftables on our infrastructure