Files
alknet/docs/research/pivot/alpn-service-architecture.md

252 lines
14 KiB
Markdown

# ALPN-as-Service Architecture
> Status: Research / Pivot Proposal
> Last updated: 2026-06-14
## 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 TLS/QUIC handshake is the dispatch mechanism — it routes the connection to the correct protocol handler before any application bytes are read.
This insight comes from observing how iroh already works: `Router` dispatches incoming QUIC connections to `ProtocolHandler` implementations based on the ALPN string. The reverse-proxy project at `/workspace/@alkdev/reverse-proxy` uses the same pattern for TLS (ALPN → h2 or http/1.1). Hickory DNS registers ALPN protocols (`dot`, `doq`, `h2`, `h3`) in its TLS/QUIC server configs. The pattern is universal.
## The ProtocolHandler Trait
A single trait replaces the current `StreamInterface` + `MessageInterface` split, the `ListenerConfig` enum, and the three-layer dispatch model:
```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<()>;
}
```
`BiStream` is a joined `(SendStream, RecvStream)` implementing `AsyncRead + AsyncWrite`. `AuthContext` carries the authenticated identity (resolved during the TLS/QUIC handshake or from the first protocol frames, depending on the handler).
Every handler receives a byte stream and manages its own wire format. HTTP is just a handler on ALPN `h2`. DNS is just a handler on its own ALPN. SSH is a handler on `alknet/ssh`. The call protocol is a handler on `alknet/call`.
## Handler Registry
```
alknet Endpoint (QUIC + TLS on a single port)
├── ALPN "alknet/ssh" → SshAdapter
│ • russh doing SSH-2 handshake, auth, channel multiplexing
│ • direct-tcpip, forwarded-tcpip, streamlocal-forward
│ • SOCKS5 server on the client side
│ • This is the "gateway" — richest tunneling primitives
├── ALPN "alknet/call" → CallAdapter
│ • JSON-RPC: call/request + streaming (subscribe)
│ • EventEnvelope framing (length-prefixed JSON)
│ • Operation registry, access control, pending request map
│ • Cross-language: consumable from JS, Python, WASM, anything
├── ALPN "alknet/git" → GitAdapter
│ • gix (Apache-2.0/MIT) for pack generation, ref resolution, object store
│ • Custom pkt-line protocol adapter (~1000 lines)
│ • Capability advertisement v2, ls-refs, fetch, receive-pack
│ • No HTTP layer — git protocol directly over QUIC streams
├── ALPN "alknet/sftp" → SftpAdapter
│ • russh-sftp protocol core (WASM-ready, transport-agnostic)
│ • 26 packet types, custom serde codec, pure data transformation
│ • Only read_packet() couples to I/O — easily adapted
│ • Can compile to WASM for browser SFTP clients
├── ALPN "alknet/msg" → MessageAdapter
│ • E2E encrypted direct messages (encrypt with recipient's public key)
│ • Mixnet support (Chaum 1981): nested encryption, batch-and-reorder
│ • Return addresses as digital pseudonyms
│ • Replicators are naturally mixes — they already relay data
├── ALPN "alknet/http" → HttpAdapter
│ • axum router with auth middleware
│ • REST API, dashboard, MCP endpoint
│ • Maps HTTP requests to call protocol operations
├── ALPN "alknet/dns" → DnsAdapter
│ • hickory-proto for DNS wire format (#![no_std], WASM-compatible)
│ • pkarr::SignedPacket for self-sovereign DNS (iroh-dns pattern)
│ • Service discovery: _alknet.<z32-node-id>.alk.dev TXT
│ • Control channel: AuthToken in query labels (censorship fallback)
│ • Encrypted transports: DoT, DoQ, DoH3 via ALPN dispatch
│ • Mainline DHT fallback for fully decentralized resolution
├── ALPN "h3" → WebTransportAdapter (wtransport)
│ • Browser-compatible WebTransport (W3C standard)
│ • Bidirectional streams + datagrams
│ • Enables browser-to-head without SSH key exchange
│ • AuthToken in CONNECT request headers
├── ALPN "h2" / "http/1.1" → Standard HTTP
│ • For browsers, curl, standard HTTP clients
│ • Same axum router as alknet/http but on standard ALPNs
└── custom ALPNs → third-party adapters
```
## What This Simplifies
### 1. The Interface Layer Collapses
No more `StreamInterface` vs `MessageInterface` split. No more `ListenerConfig` enum with three variants (`Stream`, `Http`, `Dns`). No more server accept loop handling three different listener types. Every handler receives a stream and manages its own protocol. HTTP is just a handler on ALPN `h2`. DNS is just a handler on its own ALPN. SSH is a handler on `alknet/ssh`.
### 2. The Call Protocol Becomes One ALPN Among Many
It's a generic JSON-RPC protocol that supports call/request and streaming. Services that need structured RPC register operations under `alknet/call`. Services with their own wire format (Git, SFTP, SSH) get their own ALPN. Cross-node calls go through `alknet/call`. In-process calls are direct.
### 3. OperationEnv's Three Dispatch Paths Collapse
Dispatch is "which ALPN does this belong to?" — not local/irpc/remote path selection. A handler that needs to call another service opens a stream on `alknet/call` and sends a `call.requested` envelope. The handler doesn't need to know whether the target is local, in-cluster, or cross-node.
### 4. Crate Decomposition Gets Simpler
A core crate provides the endpoint/router + auth/identity. Protocol crates register handlers. No need for trait interop without crate dependencies (the current alknet-storage-implements-IdentityProvider-without-depending-on-core pattern).
### 5. The WASM Story Gets Clean
If we assume a byte stream, protocol parsers compile to WASM:
- russh-sftp's protocol core is already WASM-ready (pure data transformation, no I/O)
- hickory-proto is `#![no_std]` with a `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
### 6. The Reverse-Proxy ALPN Pattern Applies Directly
The reverse-proxy at `/workspace/@alkdev/reverse-proxy` does: TLS handshake → read ALPN → dispatch to h2 or http/1.1 handler. Alknet does the same, just with more ALPNs. The `ArcSwap<DynamicConfig>` pattern for hot-reloading handler routing works identically.
### 7. SSH Is the Tunneling Gateway, Not the Only Interface
SSH provides `direct-tcpip`, `forwarded-tcpip`, `streamlocal-forward` — rich tunneling primitives that other protocols don't have. But SSH is just one ALPN. A node that only needs Git can skip SSH entirely and use `alknet/git` directly. A browser can use `h3` (WebTransport) + `alknet/call` without SSH.
## Auth and Identity
Auth is shared across all handlers. The `IdentityProvider` trait resolves credentials to an `Identity`:
```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 presents credentials differently, but all resolve through the same provider:
| 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()` |
## Distributed Git via ERC721
The git adapter integrates with on-chain identity for decentralized repository management:
```
Creator mints RepoToken (ERC721) → on-chain metadata stores:
• name, owner (UserToken)
• authorized committers (list of UserTokens or key fingerprints)
• optional: preferred replicator set
Committer pushes → GitAdapter verifies:
1. Resolve signer's key → UserToken (via on-chain IdentityProvider)
2. Check UserToken is in RepoToken's committer list
3. Accept push, compute new refs
4. Gossip update to replicator mesh
Replicator receives gossip → verifies independently → stores in local repo
```
Replicators are voluntary nodes that:
- Subscribe to gossip for specific repos (by token ID)
- Serve repos via the git ALPN
- Advertise which repos they replicate via pkarr/DNS records
- May also serve other ALPNs (dns, msg, call)
- Have donation addresses in node metadata (no protocol-level economics)
## Messaging Layers
Three distinct privacy layers on the same relay infrastructure:
| Layer | What's hidden | Mechanism | Use case |
|-------|---------------|-----------|----------|
| E2E encryption | Message content | Encrypt with recipient's public key | Direct messages, file transfer |
| Gossip + call protocol | Nothing hidden by design | Public broadcast, structured RPC | Group chat, repo notifications |
| Mixnet (Chaum 1981) | Sender/recipient metadata | Nested encryption, batch-and-reorder | Pseudonymous commits, anonymous tips |
The replicator doesn't care which layer it's serving. It sees "encrypted blob, forward to X" and does the same thing regardless. A replicator that handles git gossip is also a mix node is also a relay for e2e DMs — it's all encrypted bytes through the same relay infrastructure.
## Crate Decomposition
```
alknet-core Endpoint, ALPN router, auth/identity, config, call protocol
alknet-ssh SshAdapter (russh, SOCKS5, port forwarding)
alknet-call CallAdapter (JSON-RPC, operation registry, access control)
alknet-git GitAdapter (gix, pkt-line protocol)
alknet-sftp SftpAdapter (russh-sftp protocol core)
alknet-msg MessageAdapter (E2E encryption, mixnet)
alknet-http HttpAdapter (axum, REST API)
alknet-dns DnsAdapter (hickory-proto, pkarr, service discovery)
alknet-secret BIP39/SLIP-0010/AES-GCM (standalone)
alknet-napi Node.js native addon (call protocol client)
alknet CLI binary (assembles everything)
```
Each protocol crate depends on `alknet-core` for auth/identity/config. No trait interop without crate dependencies. The CLI binary registers handlers with the endpoint.
## What Stays (Preserved from Current Implementation)
- **Transport implementations** (TCP, TLS, iroh) — become the endpoint's connection acceptors
- **SSH client/server** (russh, SOCKS5, port forwarding, channel proxy) — become `alknet-ssh`
- **Call protocol** (EventEnvelope framing, operation registry, access control, pending request map) — become `alknet-call`
- **Auth/identity** (Ed25519 keys, API keys, `IdentityProvider`, `ConfigIdentityProvider`) — shared in `alknet-core`
- **Dynamic config reload** (`ArcSwap<DynamicConfig>`, `ConfigReloadHandle`) — shared in `alknet-core`
- **Secret crate** (BIP39/SLIP-0010/AES-GCM, irpc service, key caching) — standalone `alknet-secret`
- **NAPI addon** (connect, serve, ThreadsafeFunction callbacks) — becomes call protocol client
- **Stealth mode** (byte-peek protocol detection) — replaced by ALPN negotiation (cleaner, no peeking needed)
## What Gets Dropped/Deferred
- **`StreamInterface` / `MessageInterface` traits** — replaced by `ProtocolHandler`
- **`ListenerConfig` enum** — replaced by ALPN advertisement configuration
- **`OperationEnv` three dispatch paths** — replaced by "call through `alknet/call` ALPN"
- **The metagraph data model** (alknet-storage) — defer until concrete need
- **The flowgraph crate** — defer, observability infrastructure
- **`CredentialProvider` phase progression** (A through D) — simplify to what's needed now
- **38 ADRs for unbuilt features** — archive, revisit if needed
## Migration Path
1. Define `ProtocolHandler` trait and ALPN router in `alknet-core`
2. Extract SSH code into `alknet-ssh` implementing `ProtocolHandler`
3. Extract call protocol into `alknet-call` implementing `ProtocolHandler`
4. Wire existing auth/config into shared `alknet-core`
5. Build GitAdapter on gix + pkt-line protocol
6. Build SftpAdapter on russh-sftp protocol core
7. Build HttpAdapter on axum
8. Build DnsAdapter on hickory-proto + pkarr
9. Build MessageAdapter (E2E + mixnet)
10. Integrate WebTransport via wtransport
11. Update NAPI to be a call protocol client
12. CLI binary registers all handlers
## Reference Projects
| Project | Path | Relevance |
|---------|------|-----------|
| iroh | `/workspace/iroh/` | ALPN-based protocol dispatch on QUIC endpoints |
| reverse-proxy | `/workspace/@alkdev/reverse-proxy` | ALPN routing (h2/http1.1), ArcSwap config, TLS acceptor pattern |
| russh-sftp | `/workspace/russh-sftp/` | Transport-agnostic protocol core, WASM-ready |
| hickory-dns | `/workspace/hickory-dns/` | DNS wire format (#![no_std]), ALPN-registered transports (dot/doq/h2/h3) |
| wtransport | `/workspace/wtransport/` | WebTransport (h3 ALPN), browser-compatible QUIC streams |
| rtc | `/workspace/rtc/` | Sans-I/O WebRTC (datachannel, RTP) — reference for P2P alternative |
| gix | crates.io | Apache-2.0/MIT git operations (pack generation, ref resolution, object store) |