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
This commit is contained in:
65
docs/architecture/README.md
Normal file
65
docs/architecture/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# Alknet Architecture
|
||||
|
||||
## Current State
|
||||
|
||||
**Pre-implementation.** The project has completed a pivot from a three-layer model (StreamInterface/MessageInterface, ListenerConfig, OperationEnv) to an ALPN-as-service model. The greenfield workspace contains only `alknet-secret` (stable) and research/reference material. Architecture specs are being produced following the SDD process (Phase 1).
|
||||
|
||||
## Architecture Documents
|
||||
|
||||
| Document | Status | Description |
|
||||
|----------|--------|-------------|
|
||||
| [overview.md](overview.md) | draft | Workspace-level overview, crate graph, shared types |
|
||||
| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker across all crates |
|
||||
| [crates/alknet-core/spec.md](crates/alknet-core/spec.md) | planned | Core crate: ProtocolHandler, endpoint, router, auth, config |
|
||||
| [crates/alknet-ssh/spec.md](crates/alknet-ssh/spec.md) | planned | SSH handler: russh, SOCKS5, port forwarding |
|
||||
| [crates/alknet-call/spec.md](crates/alknet-call/spec.md) | planned | Call protocol: irpc, operation registry, access control |
|
||||
| [crates/alknet-secret/spec.md](crates/alknet-secret/spec.md) | planned | Key derivation and encryption (already implemented) |
|
||||
| [crates/alknet-sftp/spec.md](crates/alknet-sftp/spec.md) | planned | SFTP handler: russh-sftp protocol core |
|
||||
| [crates/alknet-git/spec.md](crates/alknet-git/spec.md) | planned | Git handler: gix, pkt-line protocol |
|
||||
| [crates/alknet-http/spec.md](crates/alknet-http/spec.md) | planned | HTTP handler: axum, REST API, MCP |
|
||||
| [crates/alknet-dns/spec.md](crates/alknet-dns/spec.md) | planned | DNS handler: hickory-proto, pkarr, service discovery |
|
||||
| [crates/alknet-msg/spec.md](crates/alknet-msg/spec.md) | planned | Messaging: E2E encryption, mixnet |
|
||||
| [crates/alknet-napi/spec.md](crates/alknet-napi/spec.md) | planned | Node.js native addon: call protocol client |
|
||||
| [crates/alknet/spec.md](crates/alknet/spec.md) | planned | CLI binary: handler registration, endpoint startup |
|
||||
|
||||
## ADR Table
|
||||
|
||||
| ADR | Title | Status |
|
||||
|-----|-------|--------|
|
||||
| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | Accepted |
|
||||
| [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | Accepted |
|
||||
| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | Accepted |
|
||||
| [004](decisions/004-auth-as-shared-core.md) | Auth as Shared Core (IdentityProvider) | Accepted |
|
||||
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | Accepted |
|
||||
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
See [open-questions.md](open-questions.md) for the full tracker.
|
||||
|
||||
Key questions affecting current work:
|
||||
- **OQ-01**: BiStream type definition — what exactly does BiStream expose? (open)
|
||||
- **OQ-02**: AuthContext resolution timing — hybrid model resolved (see ADR-004) (resolved)
|
||||
- **OQ-03**: ALPN string naming convention — resolved (see ADR-006) (resolved)
|
||||
- **OQ-04**: Dynamic handler registration at runtime vs static at startup (open)
|
||||
|
||||
## Document Lifecycle
|
||||
|
||||
| Status | Meaning | Transitions |
|
||||
|--------|---------|-------------|
|
||||
| `draft` | Under active development. May change significantly. | → `reviewed` when open questions are resolved |
|
||||
| `reviewed` | Architecture is final. Implementation may begin. Changes require review. | → `stable` when implementation is complete and verified |
|
||||
| `stable` | Locked. Changes require review and may warrant an ADR. | → `deprecated` when superseded |
|
||||
| `deprecated` | Superseded. Kept for reference. | Removed when no longer referenced |
|
||||
|
||||
## References
|
||||
|
||||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||||
- Cleanup plan: `docs/research/pivot/cleanup-plan.md`
|
||||
- SDD process: `docs/sdd_process.md`
|
||||
- Reference implementation: `/workspace/@alkdev/alknet-main/`
|
||||
21
docs/architecture/crates/alknet-call/spec.md
Normal file
21
docs/architecture/crates/alknet-call/spec.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-call
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
Call protocol handler implementing `ProtocolHandler` on ALPN `alknet/call`. Provides JSON-RPC via irpc with operation registry, streaming subscriptions, pub/sub, and access control.
|
||||
|
||||
## Key Questions
|
||||
|
||||
- **OQ-07**: Call protocol scope within a connection — one stream per operation vs multiplexed
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-005: irpc as call protocol foundation
|
||||
26
docs/architecture/crates/alknet-core/spec.md
Normal file
26
docs/architecture/crates/alknet-core/spec.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-core
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet. It will be produced as part of Phase 1 architecture work.
|
||||
|
||||
## Purpose
|
||||
|
||||
Core crate providing the `ProtocolHandler` trait, ALPN router, endpoint, `BiStream`, `AuthContext`, `IdentityProvider`, configuration types, and shared infrastructure used by all handler crates.
|
||||
|
||||
## Key Questions
|
||||
|
||||
- **OQ-01**: BiStream type definition — trait vs concrete type vs newtype
|
||||
- **OQ-05**: Multi-transport endpoint — TCP, TLS, iroh support scope
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-001: ALPN-based protocol dispatch
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- ADR-003: Crate decomposition
|
||||
- ADR-004: Auth as shared core
|
||||
- ADR-006: ALPN string convention and connection model
|
||||
17
docs/architecture/crates/alknet-dns/spec.md
Normal file
17
docs/architecture/crates/alknet-dns/spec.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-dns
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
DNS handler implementing `ProtocolHandler` on ALPN `alknet/dns`. Uses hickory-proto (`#![no_std]`, WASM-compatible) for DNS wire format and pkarr for self-sovereign DNS. Provides service discovery, control channel via AuthToken in query labels, and encrypted DNS transports (DoT, DoQ, DoH3).
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-002: ProtocolHandler trait
|
||||
21
docs/architecture/crates/alknet-git/spec.md
Normal file
21
docs/architecture/crates/alknet-git/spec.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-git
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
Git smart protocol handler implementing `ProtocolHandler` on ALPN `alknet/git`. Uses gix (Apache-2.0/MIT) for pack generation, ref resolution, and object store. Custom pkt-line protocol adapter for QUIC streams. No HTTP layer — git protocol directly over QUIC.
|
||||
|
||||
## Key Questions
|
||||
|
||||
- **OQ-10**: Git adapter scope — smart protocol only or full server?
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-002: ProtocolHandler trait
|
||||
23
docs/architecture/crates/alknet-http/spec.md
Normal file
23
docs/architecture/crates/alknet-http/spec.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-http
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
HTTP handler implementing `ProtocolHandler` on ALPN `alknet/http`. Provides axum router with auth middleware, REST API, dashboard, and MCP endpoint. Also handles standard HTTP ALPNs (`h2`, `http/1.1`) and WebTransport upgrade on `h3`.
|
||||
|
||||
## Key Questions
|
||||
|
||||
- How does HttpAdapter handle both `alknet/http` and standard ALPNs (`h2`, `http/1.1`, `h3`)?
|
||||
- WebTransport upgrade on `h3` — is this a separate handler or integrated into HttpAdapter?
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- ADR-006: ALPN string convention and connection model
|
||||
17
docs/architecture/crates/alknet-msg/spec.md
Normal file
17
docs/architecture/crates/alknet-msg/spec.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-msg
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
Messaging handler implementing `ProtocolHandler` on ALPN `alknet/msg`. Provides E2E encrypted direct messages (encrypt with recipient's public key) and mixnet support (Chaum 1981: nested encryption, batch-and-reorder, return addresses as digital pseudonyms).
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-002: ProtocolHandler trait
|
||||
18
docs/architecture/crates/alknet-napi/spec.md
Normal file
18
docs/architecture/crates/alknet-napi/spec.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-napi
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
Node.js native addon providing a call protocol client. Uses napi-rs for FFI. Depends only on alknet-call (not alknet-core) to keep the dependency tree minimal. Exposes connect/disconnect, call operations, and event subscriptions to JavaScript/TypeScript.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-003: Crate decomposition
|
||||
- ADR-005: irpc as call protocol foundation
|
||||
21
docs/architecture/crates/alknet-secret/spec.md
Normal file
21
docs/architecture/crates/alknet-secret/spec.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-secret
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet. The crate is already implemented and stable.
|
||||
|
||||
## Purpose
|
||||
|
||||
Standalone crate for BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption, and the `SecretProtocol` irpc service. Does not depend on alknet-core.
|
||||
|
||||
## Key Questions
|
||||
|
||||
- **OQ-08**: Secret service integration point — irpc service, ALPN handler, or embedded library?
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-003: Crate decomposition (alknet-secret is standalone)
|
||||
18
docs/architecture/crates/alknet-sftp/spec.md
Normal file
18
docs/architecture/crates/alknet-sftp/spec.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-sftp
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
SFTP handler implementing `ProtocolHandler` on ALPN `alknet/sftp`. Provides russh-sftp protocol core with 26 packet types, custom serde codec, and pure data transformation. WASM-ready: only `read_packet()` couples to I/O.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- russh-sftp reference: `docs/research/references/ssh/russh-sftp/`
|
||||
29
docs/architecture/crates/alknet-ssh/spec.md
Normal file
29
docs/architecture/crates/alknet-ssh/spec.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet-ssh
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet. It will be produced as part of Phase 2 architecture work.
|
||||
|
||||
## Purpose
|
||||
|
||||
SSH handler implementing `ProtocolHandler` on ALPN `alknet/ssh`. Provides russh-based SSH-2 handshake, channel multiplexing, SOCKS5 proxy, and port forwarding (direct-tcpip, forwarded-tcpip, streamlocal-forward).
|
||||
|
||||
## Port Source
|
||||
|
||||
| Old module | Lines | Notes |
|
||||
|---|---|---|
|
||||
| `src/interface/ssh.rs` | 982 | SSH channel handling |
|
||||
| `src/server/handler.rs` | 974 | SSH server handler |
|
||||
| `src/server/channel_proxy.rs` | 555 | Channel proxy |
|
||||
| `src/client/*` | ~1900 | SOCKS5 client, connect logic |
|
||||
| `src/socks5/*` | ~800 | SOCKS5 protocol |
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- ADR-004: Auth as shared core
|
||||
- russh reference: `docs/research/references/ssh/russh/`
|
||||
17
docs/architecture/crates/alknet/spec.md
Normal file
17
docs/architecture/crates/alknet/spec.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
status: planned
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# alknet (CLI)
|
||||
|
||||
> **Status: Planned** — This spec has not been written yet.
|
||||
|
||||
## Purpose
|
||||
|
||||
CLI binary that assembles all handler crates and starts the alknet endpoint. Registers ProtocolHandler implementations with the ALPN router based on configuration. The only crate that depends on all handler crates.
|
||||
|
||||
## References
|
||||
|
||||
- [overview.md](../../overview.md)
|
||||
- ADR-003: Crate decomposition
|
||||
46
docs/architecture/decisions/001-alpn-protocol-dispatch.md
Normal file
46
docs/architecture/decisions/001-alpn-protocol-dispatch.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# ADR-001: ALPN-Based Protocol Dispatch
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The previous architecture used a three-layer model: transports produced byte streams, interfaces defined how to interpret those streams (StreamInterface, MessageInterface), and OperationEnv dispatched operations through local, irpc, or remote paths. This required a ListenerConfig enum with three variants (Stream, Http, Dns), a server accept loop handling three different listener types, and a complex dispatch model that mixed concerns across layers.
|
||||
|
||||
Protocol detection was done by byte-peeking — the server read the first bytes of an incoming connection and guessed which protocol the client was speaking. This is fragile, limits protocol extensibility, and cannot work with encrypted transports where the payload is opaque.
|
||||
|
||||
ALPN (Application-Layer Protocol Negotiation) is a TLS extension where the client advertises supported protocols during the handshake and the server selects one. QUIC builds on this natively — every QUIC connection has an ALPN. This is the same pattern iroh uses: `Router` dispatches incoming QUIC connections to `ProtocolHandler` implementations based on the ALPN string. Hickory DNS registers ALPN protocols (`dot`, `doq`, `h2`, `h3`). The reverse-proxy project at `@alkdev/reverse-proxy` uses the same pattern for TLS.
|
||||
|
||||
The core insight: **a service IS an ALPN**. Every protocol handler registers an ALPN string on a shared QUIC+TLS endpoint. The ALPN negotiation during the handshake routes the connection to the correct handler before any application bytes are read.
|
||||
|
||||
## Decision
|
||||
|
||||
All protocol dispatch in alknet is ALPN-based. A single QUIC+TLS endpoint accepts connections, and the ALPN string selected during the handshake determines which `ProtocolHandler` receives the connection. There is no byte-peeking, no ListenerConfig enum, and no three-layer dispatch model.
|
||||
|
||||
The endpoint advertises the union of all registered handlers' ALPN strings. When a client connects, the TLS/QUIC handshake negotiates the ALPN. If the client's offered ALPNs and the server's advertised ALPNs have no intersection, the handshake fails — this is the correct behavior, not an error to work around.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Single dispatch mechanism replaces three separate listener types
|
||||
- Protocol detection happens at the TLS layer, not application layer — no byte-peeking
|
||||
- Adding a new protocol is registering a new ALPN string — no server code changes
|
||||
- Each handler owns its entire wire format — no shared framing layer
|
||||
- QUIC connections are cheap — a client that needs multiple protocols opens one connection per ALPN, all multiplexed over the same UDP flow
|
||||
- Stealth mode (byte-peek protocol detection on port 443) is unnecessary — ALPN negotiation handles this cleanly
|
||||
- WASM story is clean: handlers receive byte streams, protocol parsers that operate on bytes compile to WASM
|
||||
|
||||
**Negative:**
|
||||
- ALPN is negotiated per-connection, not per-stream — a client that wants to use multiple ALPNs (e.g., SSH and call protocol) opens separate QUIC connections for each. QUIC connections are cheap (multiplexed over the same UDP flow), so this is acceptable, but it means `alknet/call` cannot serve as a multiplexer for other ALPNs within a single connection unless explicitly designed to do so (see ADR-006).
|
||||
- All protocols must be registered at endpoint creation time (or use hot-reload via ArcSwap for dynamic addition)
|
||||
- Custom protocols require reserving ALPN strings — we own the `alknet/` namespace
|
||||
- Debugging requires knowing which ALPN was negotiated (mitigated by logging at the endpoint level)
|
||||
|
||||
## References
|
||||
|
||||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- ADR-003: Crate decomposition
|
||||
- iroh reference: `docs/research/references/iroh/` (ALPN dispatch, ProtocolHandler pattern)
|
||||
- Replaces the old three-layer model (StreamInterface/MessageInterface/OperationEnv)
|
||||
57
docs/architecture/decisions/002-protocol-handler-trait.md
Normal file
57
docs/architecture/decisions/002-protocol-handler-trait.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# ADR-002: ProtocolHandler Trait
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The previous architecture had two separate interface traits: `StreamInterface` (for byte-stream protocols like SSH, raw TCP) and `MessageInterface` (for message-based protocols like DNS, HTTP). This split created complexity — each interface type needed its own listener configuration, its own dispatch path, and its own framing assumptions. The `ListenerConfig` enum had three variants. The server accept loop handled three different listener types.
|
||||
|
||||
In practice, the distinction between "stream" and "message" protocols is artificial at the handler level. SSH starts as a byte stream but internally multiplexes channels and messages. DNS over QUIC is message-based but arrives as a stream of frames. HTTP/2 is both — bidirectional streams with message semantics. Every protocol can be modeled as "receive a byte stream, manage your own wire format."
|
||||
|
||||
iroh's `ProtocolHandler` trait demonstrates this: it takes a bidirectional QUIC stream and the handler is responsible for its own protocol. One trait, one dispatch point.
|
||||
|
||||
## Decision
|
||||
|
||||
A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInterface`:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait ProtocolHandler: Send + Sync + 'static {
|
||||
/// The ALPN string this handler claims (e.g. b"alknet/ssh")
|
||||
fn alpn(&self) -> &'static [u8];
|
||||
|
||||
/// Handle an incoming bidirectional QUIC stream
|
||||
async fn handle(&self, stream: BiStream, auth: &AuthContext) -> Result<(), HandlerError>;
|
||||
}
|
||||
```
|
||||
|
||||
- `alpn()` returns a static byte string — the handler's ALPN identifier
|
||||
- `handle()` receives a `BiStream` (a joined `(SendStream, RecvStream)` implementing `AsyncRead + AsyncWrite`) and an `AuthContext` carrying the authenticated identity, and returns `HandlerError` on failure
|
||||
- Every handler manages its own wire format — no shared framing, no StreamInterface/MessageInterface split
|
||||
- The `ListenerConfig` enum is eliminated — ALPN advertisement configuration replaces it
|
||||
|
||||
**AuthContext resolution is hybrid** (see ADR-004, OQ-02 resolution): the endpoint resolves what it can before calling `handle()` (e.g., TLS client certificate fingerprint), and the handler resolves what it must inside `handle()` (e.g., AuthToken in the first frame of a call stream). The `AuthContext` passed to `handle()` may contain partial identity information — the handler is responsible for completing authentication if the endpoint didn't have enough information.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- One trait, one dispatch point — eliminates the StreamInterface/MessageInterface split and ListenerConfig enum
|
||||
- Each handler owns its wire format — no shared framing assumptions that constrain protocol design
|
||||
- Adding a new protocol is implementing one trait with two methods
|
||||
- Testable in isolation — give a handler a mock BiStream and AuthContext
|
||||
- WASM-compatible in principle — handlers that don't need tokio runtime features compile to WASM
|
||||
|
||||
**Negative:**
|
||||
- Every handler must implement its own framing — no shared "read a length-prefixed message" utility (mitigated: common utilities can live in alknet-core without mandating their use)
|
||||
- Handlers that want message semantics must build them (mitigated: alknet-call provides this as a handler, not a mandatory layer)
|
||||
- AuthContext resolution is hybrid — the endpoint resolves what it can (TLS-level auth), but handlers that need protocol-level credential extraction must do so inside handle(). This means AuthContext may be partial when handle() is called. Handlers must not assume AuthContext is fully resolved.
|
||||
|
||||
## References
|
||||
|
||||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||||
- ADR-001: ALPN-based protocol dispatch
|
||||
- ADR-004: Auth as shared core (IdentityProvider)
|
||||
- iroh ProtocolHandler pattern: `docs/research/references/iroh/`
|
||||
- Replaces StreamInterface, MessageInterface, and ListenerConfig
|
||||
68
docs/architecture/decisions/003-crate-decomposition.md
Normal file
68
docs/architecture/decisions/003-crate-decomposition.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ADR-003: Crate Decomposition
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The previous alknet-core crate was a monolith containing transport, interface, server, client, call, auth, config, socks5, credentials, and HTTP — all in one crate with interdependent modules. This created coupling (interface types depended on auth, server depended on call, everything depended on config) and made it impossible to use individual components independently.
|
||||
|
||||
The new ALPN dispatch model eliminates the need for a shared interface layer. Each handler is self-contained — it receives a byte stream and manages its own protocol. This naturally decomposes into separate crates.
|
||||
|
||||
Key constraints:
|
||||
- Protocol crates must depend on alknet-core for auth/identity/config — but not on each other
|
||||
- alknet-secret is already standalone (no alknet-core dependency) and must remain so
|
||||
- The CLI binary assembles everything — it's the only crate that depends on all handler crates
|
||||
- Some handlers (SFTP, call protocol) need to compile to WASM for browser/client use
|
||||
- irpc is the foundation for the call protocol — it provides the operation registry, framing, and pub/sub patterns
|
||||
|
||||
## Decision
|
||||
|
||||
The workspace decomposes into the following crates:
|
||||
|
||||
| Crate | Responsibility | Depends on |
|
||||
|-------|---------------|------------|
|
||||
| `alknet-core` | ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config | tokio, quinn, rustls, irpc |
|
||||
| `alknet-secret` | BIP39/SLIP-0010/AES-GCM key derivation and encryption, SecretProtocol service | (standalone, no alknet-core) |
|
||||
| `alknet-ssh` | SshAdapter (russh, SOCKS5, port forwarding) | alknet-core, russh |
|
||||
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control) | alknet-core, irpc |
|
||||
| `alknet-git` | GitAdapter (gix, pkt-line protocol) | alknet-core, gix |
|
||||
| `alknet-sftp` | SftpAdapter (russh-sftp protocol core) | alknet-core, russh-sftp |
|
||||
| `alknet-msg` | MessageAdapter (E2E encryption, mixnet) | alknet-core |
|
||||
| `alknet-http` | HttpAdapter (axum, REST API, MCP endpoint) | alknet-core, axum |
|
||||
| `alknet-dns` | DnsAdapter (hickory-proto, pkarr, service discovery) | alknet-core, hickory-proto |
|
||||
| `alknet-napi` | Node.js native addon (call protocol client) | alknet-call, napi-rs |
|
||||
| `alknet` | CLI binary — registers handlers, starts endpoint | all handler crates |
|
||||
|
||||
Dependency flow:
|
||||
```
|
||||
alknet-secret (standalone)
|
||||
alknet-core ← all handler crates ← alknet (CLI)
|
||||
alknet-call ← alknet-napi
|
||||
```
|
||||
|
||||
No handler crate depends on another handler crate. Cross-handler communication goes through the call protocol (alknet-call) or through alknet-core's endpoint.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Each handler can be developed, tested, and versioned independently
|
||||
- WASM-compatible handlers (sftp, call) don't pull in heavy dependencies (russh, axum)
|
||||
- alknet-secret remains standalone — no circular dependency risk
|
||||
- New handlers are added by creating a crate and registering it with the endpoint
|
||||
- Clean separation of concerns — each crate has one job
|
||||
|
||||
**Negative:**
|
||||
- More crates to manage in the workspace — workspace Cargo.toml and version coordination
|
||||
- Shared types (AuthContext, BiStream) must live in alknet-core — if they change, all handlers recompile
|
||||
- The CLI binary has a large dependency tree (all handlers) — but this is expected for a binary that assembles everything
|
||||
- Testing cross-handler behavior requires integration tests in the CLI or a test utility crate
|
||||
|
||||
## References
|
||||
|
||||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||||
- ADR-001: ALPN-based protocol dispatch
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- ADR-004: Auth as shared core (IdentityProvider)
|
||||
- ADR-005: irpc as call protocol foundation
|
||||
69
docs/architecture/decisions/004-auth-as-shared-core.md
Normal file
69
docs/architecture/decisions/004-auth-as-shared-core.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# ADR-004: Auth as Shared Core (IdentityProvider)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The previous architecture had authentication spread across multiple layers: `CredentialProvider` with four phases (A–D), `AuthProtocol` as an irpc service, `server_auth` and `client_auth` as separate modules, and `IdentityProvider` as a trait in alknet-core. Different interface types presented credentials differently — SSH used key fingerprints, HTTP used Bearer tokens, DNS used query labels — but the resolution was ad-hoc and tied to the three-layer model.
|
||||
|
||||
The ALPN dispatch model simplifies this: every handler receives the same `AuthContext`, but the credential extraction (how a handler learns who the peer is) differs per ALPN. The resolution (turning a credential into an `Identity`) should be shared across all handlers.
|
||||
|
||||
## Decision
|
||||
|
||||
Authentication and identity resolution live in `alknet-core` as shared infrastructure. Each handler presents credentials differently, but all resolve through the same `IdentityProvider`:
|
||||
|
||||
```rust
|
||||
pub trait IdentityProvider: Send + Sync + 'static {
|
||||
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
|
||||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
|
||||
}
|
||||
```
|
||||
|
||||
Credential presentation per handler:
|
||||
|
||||
| Handler | Credential presentation | Resolves via |
|
||||
|---------|------------------------|-------------|
|
||||
| SshAdapter | SSH public key handshake | `resolve_from_fingerprint()` |
|
||||
| CallAdapter | AuthToken in first frame | `resolve_from_token()` |
|
||||
| HttpAdapter | `Authorization: Bearer` header | `resolve_from_token()` |
|
||||
| DnsAdapter | AuthToken in query labels | `resolve_from_token()` |
|
||||
| WebTransportAdapter | AuthToken in CONNECT headers | `resolve_from_token()` |
|
||||
| GitAdapter | Signed push certificate | `resolve_from_fingerprint()` |
|
||||
|
||||
Auth resolution is **hybrid** — the endpoint resolves what it can, and handlers resolve what they must:
|
||||
|
||||
1. **Endpoint-level resolution** (before `handle()` is called): If the TLS handshake provides a client certificate, the endpoint resolves the fingerprint to an `Identity` and passes it in `AuthContext`. This is the case for SSH (where the key exchange happens at the protocol level, but the TLS layer may also provide information).
|
||||
|
||||
2. **Handler-level resolution** (inside `handle()`): For protocols that carry credentials in application frames (AuthToken in the first call frame, Bearer header in HTTP), the handler extracts the credential from the stream and calls `IdentityProvider` to resolve it. The handler then enriches or replaces the partial `AuthContext` with the fully resolved `Identity`.
|
||||
|
||||
The `AuthContext` passed to `handle()` may be partial — containing only transport-level information if no TLS client certificate was provided. Handlers must not assume `AuthContext` contains a fully resolved `Identity`. Each handler knows its own credential extraction protocol and is responsible for completing authentication.
|
||||
|
||||
The `CredentialProvider` concept from the previous architecture is simplified: there is no phase progression (A–D). The `IdentityProvider` has two resolution paths — fingerprint and token — and a `ConfigIdentityProvider` implementation that draws from static and dynamic config.
|
||||
|
||||
`alknet-secret` remains independent. It does not depend on `alknet-core` or `IdentityProvider`. The secret service provides derived keys on request; identity resolution is a separate concern.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Unified identity model — every handler resolves identities the same way through `IdentityProvider`
|
||||
- Handlers own their credential extraction — SSH reads key fingerprints, call reads AuthTokens, HTTP reads Bearer headers
|
||||
- Endpoint provides what it can for free (TLS-level auth), handlers complete what they need
|
||||
- Adding a new credential type is adding a method to `IdentityProvider`, not a new phase
|
||||
- alknet-secret stays standalone — no coupling between key derivation and identity resolution
|
||||
- `AuthContext` is a value type — easy to construct in tests, can be partial for handler-level testing
|
||||
|
||||
**Negative:**
|
||||
- `IdentityProvider` is in alknet-core — any change to it recompiles all handlers (mitigated: the trait should be stable; implementation changes don't force recompiles)
|
||||
- Two resolution paths (fingerprint, token) may not cover all future auth schemes (mitigated: the trait can be extended, or a handler can do custom resolution after the initial AuthContext)
|
||||
- Handlers must handle partial AuthContext — the endpoint may not have resolved an Identity, so handlers must be prepared to do credential extraction themselves
|
||||
- WebTransport and browser-based auth needs careful design — AuthToken in CONNECT headers requires the token to be available before the stream is established
|
||||
|
||||
## References
|
||||
|
||||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- ADR-003: Crate decomposition
|
||||
- ADR-005: irpc as call protocol foundation
|
||||
- The previous architecture had equivalent decisions in ADR-023 (unified auth) and ADR-029 (identity as core type), which are archived in the reference implementation at `/workspace/@alkdev/alknet-main/`.
|
||||
@@ -0,0 +1,56 @@
|
||||
# ADR-005: irpc as Call Protocol Foundation
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The call protocol (alknet-call) provides structured RPC — operations, request/response, streaming subscriptions, and pub/sub. This is the primary interface for programmatic interaction with an alknet node. It needs to work across platforms: Rust clients, TypeScript/JavaScript clients (via NAPI), WASM targets, and any language that can speak the wire format.
|
||||
|
||||
The previous implementation used `irpc` for the call protocol's operation registry, framing, and service patterns. irpc provides:
|
||||
- An operation registry with schema-based discovery
|
||||
- Length-prefixed JSON framing (EventEnvelope)
|
||||
- Request/response and streaming patterns
|
||||
- Type-safe operation definitions via derive macros
|
||||
|
||||
The call protocol is derived from a TypeScript implementation of "operations" and "pub/sub" that can wholesale import OpenAPI schemas, wrap MCP servers, and go the other direction — exposing operations as HTTP endpoints, MCP tools, etc. This bidirectional capability is strategically important.
|
||||
|
||||
## Decision
|
||||
|
||||
alknet-call uses irpc as its foundation. The `CallAdapter` implements `ProtocolHandler` on ALPN `alknet/call` and delegates to irpc's operation registry, framing, and dispatch.
|
||||
|
||||
irpc is not replaced or wrapped in an abstraction layer — it IS the call protocol's core. The relationship is:
|
||||
- irpc provides: operation registry, schema discovery, frame encoding/decoding, request/response routing, streaming
|
||||
- alknet-call provides: the ProtocolHandler adapter (BiStream → irpc), AuthContext integration, access control checks, the ALPN registration
|
||||
|
||||
This means:
|
||||
- The wire format is irpc's EventEnvelope framing — length-prefixed JSON
|
||||
- Operation schemas follow irpc's schema model — JSON Schema compatible
|
||||
- The TypeScript "operations" and "pub/sub" patterns that can import OpenAPI schemas and expose MCP tools are supported at the protocol level
|
||||
- Future NAPI and WASM clients speak the same wire format
|
||||
|
||||
The `SecretProtocol` in alknet-secret also uses irpc as its service protocol. This is consistent — alknet-secret's irpc service is an independent service that happens to use the same framing, not a dependency on alknet-call.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Proven operation registry and framing — irpc is already tested in production (iroh uses it)
|
||||
- JSON Schema compatible — OpenAPI import, MCP tool exposure, cross-language client generation
|
||||
- No need to design a custom RPC wire format — irpc's is already battle-tested
|
||||
- The call protocol inherits irpc's streaming and subscription patterns
|
||||
- Consistency with alknet-secret's service model — both use irpc
|
||||
|
||||
**Negative:**
|
||||
- alknet-call depends on irpc — if irpc has limitations or bugs, we're affected (mitigated: irpc is lightweight and we can fork if needed)
|
||||
- JSON framing is not the most compact binary format — for high-throughput scenarios, a binary codec could be added later as an irpc extension
|
||||
- irpc's derive macros add a compilation dependency — but this is standard for Rust RPC frameworks
|
||||
- The call protocol's cross-language story depends on irpc's wire format being documented and stable (mitigated: it's length-prefixed JSON, which is inherently cross-language)
|
||||
|
||||
## References
|
||||
|
||||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||||
- ADR-003: Crate decomposition
|
||||
- ADR-004: Auth as shared core (IdentityProvider)
|
||||
- irpc reference: `docs/research/references/iroh/irpc/` (see individual docs in that directory)
|
||||
- The previous architecture had an equivalent decision in ADR-024 (bidirectional call protocol with EventEnvelope framing), which is archived in the reference implementation at `/workspace/@alkdev/alknet-main/`.
|
||||
@@ -0,0 +1,71 @@
|
||||
# ADR-006: ALPN String Convention and Connection Model
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ADR-001 establishes ALPN-based protocol dispatch. Two questions arise:
|
||||
|
||||
1. **ALPN string naming**: What format do custom ALPN strings follow? Should they include version numbers? How do standard ALPNs (`h2`, `http/1.1`, `h3`) coexist with custom ones?
|
||||
|
||||
2. **Connection model**: ALPN is negotiated per-connection in QUIC/TLS, not per-stream. A client that wants to speak both SSH and call protocol must open two separate QUIC connections, each with its own ALPN. This is different from the claim in earlier drafts that "a single connection can carry multiple protocols via additional streams" — it cannot. However, QUIC connections are cheap (multiplexed over the same UDP flow), so opening multiple connections is acceptable.
|
||||
|
||||
The iroh reference project uses the same model: each `ProtocolHandler` claims an ALPN, and each incoming connection is dispatched to exactly one handler based on the negotiated ALPN.
|
||||
|
||||
## Decision
|
||||
|
||||
### ALPN String Convention
|
||||
|
||||
Custom ALPN strings use the `alknet/` prefix:
|
||||
|
||||
| ALPN | Handler | Type |
|
||||
|------|---------|------|
|
||||
| `alknet/ssh` | SshAdapter | Custom |
|
||||
| `alknet/call` | CallAdapter | Custom |
|
||||
| `alknet/git` | GitAdapter | Custom |
|
||||
| `alknet/sftp` | SftpAdapter | Custom |
|
||||
| `alknet/msg` | MessageAdapter | Custom |
|
||||
| `alknet/http` | HttpAdapter | Custom |
|
||||
| `alknet/dns` | DnsAdapter | Custom |
|
||||
| `h3` | WebTransport → alknet/http | Standard (IANA) |
|
||||
| `h2` | HTTP/2 → alknet/http | Standard (IANA) |
|
||||
| `http/1.1` | HTTP/1.1 → alknet/http | Standard (IANA) |
|
||||
|
||||
Rules:
|
||||
- Custom ALPNs use the format `alknet/<name>` — lowercase, no version number
|
||||
- Standard ALPNs (`h2`, `http/1.1`, `h3`) use their IANA-registered strings and are handled by the HTTP adapter
|
||||
- No version numbers in ALPN strings initially. If protocol compatibility breaks, a new ALPN string is registered (e.g., `alknet/call/v2`). This is simpler than version negotiation and follows the QUIC convention that ALPN mismatch means connection failure
|
||||
- ALPN strings are compile-time constants in each handler's `alpn()` method — no runtime registration of new ALPN strings
|
||||
|
||||
### Connection Model
|
||||
|
||||
**One ALPN per connection.** A client that wants to use multiple ALPNs opens one QUIC connection per ALPN. All connections from the same client are multiplexed over the same UDP flow (QUIC's natural connection multiplexing), so the overhead is minimal.
|
||||
|
||||
This means:
|
||||
- `alknet/call` is a distinct ALPN with its own connection — not a multiplexer for other ALPNs
|
||||
- A client interacting with both SSH and call protocol has two QUIC connections
|
||||
- Within an `alknet/call` connection, multiple QUIC streams can carry independent operations (see ADR-005)
|
||||
- The endpoint logs the negotiated ALPN for each connection for observability
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Simple model: one connection, one protocol — no multiplexing layer needed inside a connection
|
||||
- ALPN strings are predictable and discoverable — `alknet/<name>` is a clear namespace
|
||||
- No version negotiation complexity — incompatible versions get new ALPN strings
|
||||
- QUIC connection multiplexing means multiple ALPN connections share the same UDP flow
|
||||
|
||||
**Negative:**
|
||||
- Multiple ALPNs require multiple connections — a full-featured client might have 3-5 QUIC connections open simultaneously
|
||||
- No version negotiation — an incompatible change requires a new ALPN string, which means old and new clients can coexist only if the server registers both ALPNs
|
||||
- The `alknet/` namespace is owned by this project — third-party extensions need their own prefix
|
||||
|
||||
## References
|
||||
|
||||
- ADR-001: ALPN-based protocol dispatch
|
||||
- ADR-002: ProtocolHandler trait
|
||||
- OQ-03: ALPN string naming convention (resolved by this ADR)
|
||||
- OQ-06: Server-side ALPN vs client-side ALPN (resolved by this ADR)
|
||||
- iroh reference: `docs/research/references/iroh/`
|
||||
165
docs/architecture/open-questions.md
Normal file
165
docs/architecture/open-questions.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# Open Questions
|
||||
|
||||
Questions are organized by theme. Each question has a stable OQ-ID for cross-referencing from spec documents.
|
||||
|
||||
## Theme: Core Types
|
||||
|
||||
### OQ-01: BiStream Type Definition
|
||||
|
||||
- **Origin**: [overview.md](overview.md), future [crates/alknet-core/spec.md](crates/alknet-core/spec.md)
|
||||
- **Status**: open
|
||||
- **Priority**: high
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: ADR-002
|
||||
|
||||
What exactly does BiStream expose? The pivot proposal defines it as a joined `(SendStream, RecvStream)` implementing `AsyncRead + AsyncWrite`. Should it be:
|
||||
- A concrete type wrapping quinn's `SendStream` + `RecvStream`?
|
||||
- A trait `BiStream: AsyncRead + AsyncWrite + Send + Unpin` with a QuinnBiStream implementation?
|
||||
- A type alias or newtype?
|
||||
|
||||
The choice affects WASM compatibility (quinn doesn't compile to WASM) and testing (mock BiStream needs to be constructible without a QUIC connection). If BiStream is a trait, WASM targets can implement it over WebTransport streams.
|
||||
|
||||
### OQ-02: AuthContext Resolution Timing
|
||||
|
||||
- **Origin**: [overview.md](overview.md), future [crates/alknet-core/spec.md](crates/alknet-core/spec.md)
|
||||
- **Status**: resolved
|
||||
- **Priority**: high
|
||||
- **Resolution**: Hybrid model (Option C) — endpoint resolves what it can (e.g., TLS client certificate), handler resolves what it must (e.g., AuthToken in first frame). AuthContext may be partial when `handle()` is called. See ADR-002 and ADR-004.
|
||||
- **Cross-references**: ADR-002, ADR-004
|
||||
|
||||
~~Should AuthContext be fully resolved before `handle()` is called, or should the handler participate in credential extraction?~~
|
||||
|
||||
Resolved: The hybrid approach. The endpoint resolves TLS-level credentials (client certificate fingerprints). Handlers that use protocol-level credentials (AuthToken, Bearer headers) extract them inside `handle()` and call `IdentityProvider` to resolve. The `AuthContext` passed to `handle()` may contain only transport-level information.
|
||||
|
||||
## Theme: ALPN and Routing
|
||||
|
||||
### OQ-03: ALPN String Naming Convention
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: resolved
|
||||
- **Priority**: medium
|
||||
- **Resolution**: Custom ALPNs use `alknet/<name>` prefix (no version), standard ALPNs use IANA strings, no version negotiation initially. See ADR-006.
|
||||
- **Cross-references**: ADR-001, ADR-006
|
||||
|
||||
~~The pivot proposal uses `alknet/ssh`, `alknet/call`, `alknet/git`, etc. Questions:~~
|
||||
|
||||
Resolved: See ADR-006 for the full convention. Custom ALPNs use `alknet/` prefix without version numbers. Standard ALPNs (`h2`, `http/1.1`, `h3`) use their IANA strings and route to HttpAdapter. If a protocol needs a breaking change, a new ALPN string is registered.
|
||||
|
||||
### OQ-04: Dynamic Handler Registration at Runtime vs Static at Startup
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: ADR-001
|
||||
|
||||
Can handlers be registered and deregistered while the endpoint is running, or only at startup?
|
||||
|
||||
The previous implementation used `ArcSwap<DynamicConfig>` for hot-reloading routing rules. The same pattern could apply to handler registration. However:
|
||||
- Adding a handler at runtime requires updating the TLS ALPN advertisement (may require endpoint restart)
|
||||
- Removing a handler at runtime affects in-flight connections
|
||||
- The CLI assembles handlers at startup — is dynamic registration even needed?
|
||||
|
||||
## Theme: Transport and Endpoint
|
||||
|
||||
### OQ-05: Multi-Transport Endpoint
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: ADR-001
|
||||
|
||||
The previous implementation supported TCP, TLS, and iroh (QUIC P2P) transports. In the new model, does the endpoint:
|
||||
- Accept connections on a single QUIC listener (quinn) and the ALPN handles the rest?
|
||||
- Also support raw TCP listeners (for clients that don't speak QUIC)?
|
||||
- Support iroh as an additional transport that produces QUIC connections?
|
||||
|
||||
iroh's Router pattern accepts a quinn::Endpoint and dispatches by ALPN. The alknet endpoint likely wraps a quinn::Endpoint with TLS configuration that advertises all registered ALPNs. Iroh connectivity could be an additional transport that produces the same BiStream type.
|
||||
|
||||
### OQ-06: Server-Side ALPN vs Client-Side ALPN
|
||||
|
||||
- **Origin**: ADR-001
|
||||
- **Status**: resolved
|
||||
- **Priority**: low
|
||||
- **Resolution**: One ALPN per connection. Clients open one QUIC connection per ALPN. See ADR-006.
|
||||
- **Cross-references**: ADR-001, ADR-006
|
||||
|
||||
~~The ADR focuses on server-side dispatch (client connects, server selects ALPN). What about the client side? When a client opens a stream to a specific ALPN on an existing connection, does it: - Open a new QUIC connection with the target ALPN? - Open a bidirectional stream on an existing connection with ALPN metadata?~~
|
||||
|
||||
Resolved: ALPN is negotiated per-connection, not per-stream. A client that wants multiple ALPNs opens one QUIC connection per ALPN. QUIC connections are cheap (multiplexed over the same UDP flow). See ADR-006.
|
||||
|
||||
## Theme: Call Protocol
|
||||
|
||||
### OQ-07: Call Protocol Scope Within a Connection
|
||||
|
||||
- **Origin**: ADR-005
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: ADR-005
|
||||
|
||||
If a client opens a connection with ALPN `alknet/call`, can it make multiple operations over that connection? Is each operation a separate QUIC stream, or can operations be multiplexed within a single stream?
|
||||
|
||||
irpc supports both request/response and streaming. The mapping to QUIC streams needs definition:
|
||||
- One stream per request/response pair?
|
||||
- One stream per streaming subscription?
|
||||
- Or a single long-lived stream with multiplexed operations?
|
||||
|
||||
## Theme: Security
|
||||
|
||||
### OQ-08: Secret Service Integration Point
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: ADR-004
|
||||
|
||||
alknet-secret is standalone (no alknet-core dependency). How does the rest of the system access it?
|
||||
- As an irpc service that other services call? (current implementation)
|
||||
- As an ALPN handler on `alknet/secret` that only internal services use?
|
||||
- As a library that alknet-core calls directly (breaking the independence)?
|
||||
- As a library that the CLI embeds, exposing it via the call protocol?
|
||||
|
||||
The answer affects whether alknet-secret needs to know about BiStream and ProtocolHandler, or if it remains purely an irpc service that the CLI bootstraps and other services call over local irpc.
|
||||
|
||||
## Theme: WASM and Browser
|
||||
|
||||
### OQ-09: WASM Target Boundaries
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: OQ-01
|
||||
|
||||
Which crates need to compile to WASM?
|
||||
- alknet-call client (for browser/NAPI usage) — yes
|
||||
- alknet-sftp protocol core — yes (browser SFTP clients)
|
||||
- alknet-git pkt-line parser — potentially
|
||||
- alknet-core (BiStream, AuthContext) — only the types, not the endpoint/router
|
||||
|
||||
This affects the BiStream type definition (OQ-01) and dependency choices. If alknet-call's client needs to work in WASM, it can't depend on tokio or quinn directly — it needs abstracted I/O.
|
||||
|
||||
## Theme: Git and Distributed
|
||||
|
||||
### OQ-10: Git Adapter Scope — Smart Protocol Only or Full Server?
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: open
|
||||
- **Priority**: low
|
||||
- **Resolution**: (pending)
|
||||
- **Cross-references**: ADR-001
|
||||
|
||||
The pivot proposal mentions Git over QUIC streams (no HTTP layer). Does the GitAdapter implement:
|
||||
- Just the git smart protocol (ls-refs, fetch, receive-pack) over QUIC streams?
|
||||
- A full git server with ref management, pack generation, etc.?
|
||||
- The ERC721 integration for on-chain repository management?
|
||||
|
||||
This is deferred per the cleanup plan but worth noting as an open question.
|
||||
192
docs/architecture/overview.md
Normal file
192
docs/architecture/overview.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 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](decisions/001-alpn-protocol-dispatch.md) 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](decisions/003-crate-decomposition.md) for the full decomposition rationale.
|
||||
|
||||
## ProtocolHandler Trait
|
||||
|
||||
The central abstraction. Every handler implements one trait:
|
||||
|
||||
```rust
|
||||
#[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](decisions/002-protocol-handler-trait.md) 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:
|
||||
|
||||
```rust
|
||||
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](decisions/004-auth-as-shared-core.md) 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](decisions/005-irpc-as-call-protocol-foundation.md) 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/](decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | Single endpoint, ALPN negotiation routes to handlers |
|
||||
| [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | One trait replaces StreamInterface/MessageInterface |
|
||||
| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | One crate per protocol handler, core provides shared infra |
|
||||
| [004](decisions/004-auth-as-shared-core.md) | Auth as Shared Core | IdentityProvider in core, handlers extract credentials |
|
||||
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | Call protocol uses irpc for registry, framing, dispatch |
|
||||
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | `alknet/` prefix, one ALPN per connection |
|
||||
|
||||
## Open Questions
|
||||
|
||||
Open questions are tracked in [open-questions.md](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.
|
||||
Reference in New Issue
Block a user