Files
alknet/docs/architecture/overview.md
glm-5.1 f77b515968 docs(architecture): add Phase 0 architecture specs for ALPN-as-service model
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
2026-06-15 22:14:58 +00:00

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 an AuthContext (which may be partial — see authentication section), returning HandlerError on 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/secret is 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] with wasm-bindgen feature
  • 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.