Foundational architecture documents following the SDD process: ADRs: - 001: ALPN-based protocol dispatch (one endpoint, ALPN negotiation) - 002: ProtocolHandler trait (replaces StreamInterface/MessageInterface) - 003: Crate decomposition (one crate per handler, core provides shared infra) - 004: Auth as shared core (IdentityProvider, hybrid resolution model) - 005: irpc as call protocol foundation - 006: ALPN string convention and connection model (alknet/ prefix, one ALPN per connection) Docs: - overview.md: crate graph, shared types, ALPN registry, failure modes - README.md: index with doc table, ADR table, lifecycle definitions - open-questions.md: 10 OQs across 7 themes (3 resolved, 7 open) Crate spec stubs for all 11 planned crates (alknet-core through alknet CLI). Key decisions resolved during self-review: - AuthContext resolution is hybrid: endpoint resolves TLS-level auth, handlers resolve protocol-level auth (resolves OQ-02) - ALPN is per-connection not per-stream, corrected ADR-001 (resolves OQ-06) - ALPN naming uses alknet/ prefix without versions (resolves OQ-03) - HandlerError return type on ProtocolHandler trait - alknet/secret removed from ALPN registry until OQ-08 resolved
10 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-15 |
Alknet Overview
What Alknet Is
Alknet is a self-hostable networking toolkit built on QUIC+TLS with ALPN-based protocol dispatch. A single endpoint accepts connections on one port, and the ALPN string negotiated during the TLS handshake routes each connection to the correct protocol handler. Every service — SSH, SFTP, Git, HTTP, DNS, messaging, RPC — is an ALPN on a shared endpoint.
This is the core insight: a service IS an ALPN. One endpoint, one port, many protocols — dispatched by the TLS handshake, not by application-level peeking or separate listeners.
Why ALPN Dispatch
The previous architecture used a three-layer model (StreamInterface/MessageInterface, ListenerConfig, OperationEnv) that required separate listener types, application-level protocol detection via byte-peeking, and complex dispatch paths. ALPN negotiation eliminates all of this:
- Protocol detection happens at the TLS layer — no byte-peeking
- A single endpoint replaces multiple listener types
- Adding a protocol is registering an ALPN string
- Each handler owns its entire wire format
See ADR-001 for the full rationale.
Crate Graph
alknet-secret (standalone, no alknet-core dependency)
│
alknet-core
│ ├── ProtocolHandler trait
│ ├── ALPN router / endpoint
│ ├── BiStream type
│ ├── AuthContext, IdentityProvider
│ └── StaticConfig, DynamicConfig (ArcSwap)
│
├── alknet-ssh (depends on alknet-core, russh)
├── alknet-call (depends on alknet-core, irpc)
├── alknet-git (depends on alknet-core, gix)
├── alknet-sftp (depends on alknet-core, russh-sftp)
├── alknet-msg (depends on alknet-core)
├── alknet-http (depends on alknet-core, axum)
├── alknet-dns (depends on alknet-core, hickory-proto)
│
├── alknet-napi (depends on alknet-call, napi-rs)
│
└── alknet (CLI binary, depends on all handler crates)
Dependency rules:
- No handler crate depends on another handler crate
- All handler crates depend on alknet-core
- alknet-secret has zero alknet crate dependencies
- alknet-napi depends only on alknet-call (call protocol client)
- alknet (CLI) is the only crate that depends on all handler crates
See ADR-003 for the full decomposition rationale.
ProtocolHandler Trait
The central abstraction. Every handler implements one trait:
#[async_trait]
pub trait ProtocolHandler: Send + Sync + 'static {
fn alpn(&self) -> &'static [u8];
async fn handle(&self, stream: BiStream, auth: &AuthContext) -> Result<(), HandlerError>;
}
alpn()returns the handler's ALPN identifier (e.g.,b"alknet/ssh",b"alknet/call")handle()receives a bidirectional stream and anAuthContext(which may be partial — see authentication section), returningHandlerErroron failure- Each handler manages its own wire format
See ADR-002 for the full rationale.
ALPN Registry
| ALPN | Handler | Description |
|---|---|---|
alknet/ssh |
SshAdapter | SSH-2 handshake, channel multiplexing, SOCKS5, port forwarding |
alknet/call |
CallAdapter | JSON-RPC via irpc: operations, streaming, pub/sub |
alknet/git |
GitAdapter | Git smart protocol over QUIC (gix, pkt-line) |
alknet/sftp |
SftpAdapter | SFTP protocol (russh-sftp core) |
alknet/msg |
MessageAdapter | E2E encrypted messaging, mixnet |
alknet/http |
HttpAdapter | axum REST API, dashboard, MCP endpoint |
alknet/dns |
DnsAdapter | DNS over QUIC/TLS, pkarr service discovery |
h3 |
HttpAdapter (WebTransport upgrade) | Browser-compatible WebTransport, then ALPN upgrade |
h2 / http/1.1 |
HttpAdapter | Standard HTTP for browsers, curl |
Note
:
alknet/secretis not in the ALPN registry because alknet-secret is a standalone crate with no alknet-core dependency. Its integration point is an open question (see OQ-08).
Authentication
All handlers resolve identity through the shared IdentityProvider in alknet-core:
pub trait IdentityProvider: Send + Sync + 'static {
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
}
Each handler extracts credentials differently (SSH key fingerprint, AuthToken, Bearer header) but resolves through the same provider. Auth resolution is hybrid: the endpoint resolves what it can (e.g., TLS client certificate → fingerprint), and the handler resolves what it must (e.g., AuthToken in the first call frame). The AuthContext passed to handle() may be partial — handlers complete authentication inside handle().
See ADR-004 for the full rationale.
Call Protocol
alknet-call uses irpc as its foundation. The wire format is length-prefixed JSON (EventEnvelope framing). Operations are registered in an irpc registry with JSON Schema discovery. The call protocol supports request/response, streaming subscriptions, and pub/sub.
The call protocol's TypeScript predecessor can import OpenAPI schemas and expose operations as HTTP endpoints or MCP tools. This bidirectional capability carries forward.
See ADR-005 for the full rationale.
WASM Compatibility
Handlers that operate on byte streams with pure data transformation compile to WASM:
- russh-sftp's protocol core is already transport-agnostic
- hickory-proto is
#![no_std]withwasm-bindgenfeature - The call protocol's JSON framing is inherently cross-language
- Git's pkt-line is simple enough to implement anywhere
A browser gets a WebTransport stream and speaks SFTP, Git, or call protocol directly.
Shared Types
The following types live in alknet-core and are used across handler crates:
| Type | Purpose |
|---|---|
ProtocolHandler |
The trait every handler implements |
BiStream |
Bidirectional QUIC stream (AsyncRead + AsyncWrite) |
AuthContext |
Resolved identity for a connection |
Identity |
Authenticated peer identity |
IdentityProvider |
Trait for resolving credentials to identity |
AuthToken |
Opaque authentication token |
StaticConfig |
Immutable configuration loaded at startup |
DynamicConfig |
Hot-reloadable configuration (ArcSwap) |
ConfigReloadHandle |
Handle for triggering config reloads |
Design Decisions
All design decisions are documented as ADRs in decisions/.
| ADR | Decision | Summary |
|---|---|---|
| 001 | ALPN-Based Protocol Dispatch | Single endpoint, ALPN negotiation routes to handlers |
| 002 | ProtocolHandler Trait | One trait replaces StreamInterface/MessageInterface |
| 003 | Crate Decomposition | One crate per protocol handler, core provides shared infra |
| 004 | Auth as Shared Core | IdentityProvider in core, handlers extract credentials |
| 005 | irpc as Call Protocol Foundation | Call protocol uses irpc for registry, framing, dispatch |
| 006 | ALPN String Convention and Connection Model | alknet/ prefix, one ALPN per connection |
Open Questions
Open questions are tracked in open-questions.md. Key questions affecting this document:
- OQ-01: BiStream type definition (open)
- OQ-02: AuthContext resolution timing (resolved: hybrid — see ADR-004)
- OQ-03: ALPN string naming convention (resolved: see ADR-006)
- OQ-04: Dynamic handler registration at runtime vs static at startup (open)
Failure Modes
| Failure | Behavior |
|---|---|
| ALPN negotiation fails (no intersection) | TLS handshake fails — correct behavior, the client and server have no protocol in common |
Handler handle() returns HandlerError |
Endpoint logs the error, closes the QUIC stream. Other streams on the same connection are unaffected |
| Handler panics | The handler's task is caught by tokio's panic handling. The stream is closed. Other streams and connections are unaffected |
IdentityProvider returns None |
AuthContext is partial. If the handler requires authentication and cannot extract credentials from the stream, it closes the stream with an auth error |
| Config reload fails | ArcSwap<DynamicConfig> keeps the previous valid config. Error is logged. No service interruption |
| BiStream read/write error | QUIC stream-level error. The handler detects this as an I/O error and returns from handle(). The connection itself may remain open for other streams |
What Stays from the Previous Implementation
The reference implementation at /workspace/@alkdev/alknet-main/ contains working code that carries forward, adapted to the new model:
| Module | Lines | Destination | Notes |
|---|---|---|---|
src/auth/* |
~1450 | alknet-core | Identity, IdentityProvider, keys — simplified per ADR-004 |
src/config/* |
~950 | alknet-core | StaticConfig, DynamicConfig, ArcSwap — adapted for ALPN handler config |
src/transport/* |
~1500 | alknet-core | Transport trait, TCP/TLS/iroh — becomes endpoint connection acceptors |
src/call/* |
~1200 | alknet-call | EventEnvelope, registry, framing — becomes ProtocolHandler on alknet/call |
src/interface/ssh.rs |
982 | alknet-ssh | SSH channel handling |
src/server/handler.rs |
974 | alknet-ssh | SSH server handler |
src/server/channel_proxy.rs |
555 | alknet-ssh | Channel proxy |
src/server/serve.rs |
1526 | alknet-core (reference) | Accept loop pattern informs ALPN router, but gets rewritten |
src/client/* |
~1900 | alknet-ssh | SOCKS5 client, connect logic |
src/socks5/* |
~800 | alknet-ssh | SOCKS5 protocol |
The old code is reference, not constraint. Understand what it did and why, then implement against the new ProtocolHandler trait and ALPN router.