Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
6.4 KiB
ADR-026: Transport/Interface Separation (Three-Layer Model)
Status
Accepted
Context
In the current architecture, SSH is deeply embedded in the server handler. The
ServerHandler owns auth, channel management, and proxy logic — all mixed
together. This makes it impossible to run the call protocol over any transport
that doesn't speak SSH, such as:
- DNS — encoding call protocol frames as DNS TXT queries/responses for censorship resistance
- Raw framing — 4-byte length prefix + JSON
EventEnvelopewithout SSH wrapping, for local service mesh or browser-to-head direct communication - WebTransport — running call protocol over QUIC streams (browsers can't do SSH key exchange)
The DNS control channel concept from research (core.md) currently conflates
"DNS as a transport that moves bytes" with "SSH sessions over those bytes." But
SSH is not a transport — it's a protocol layer that sits on top of a
transport. Separating them enables the DNS control channel to carry call
protocol events directly, without wrapping SSH inside DNS queries.
The same separation enables raw framing (no SSH overhead) for trusted local networks, and WebTransport direct call protocol for browser clients.
Decision
Establish a three-layer model:
Layer 1: Transport
Produces byte streams. A Transport still produces
AsyncRead + AsyncWrite + Unpin + Send. This layer is unchanged from ADR-001.
#[async_trait]
pub trait Transport: Send + Sync + 'static {
type Stream: AsyncRead + AsyncWrite + Unpin + Send + 'static;
async fn connect(&self) -> Result<Self::Stream>;
fn describe(&self) -> String;
}
Transports: TCP, TLS, iroh, DNS (as byte carrier), WebTransport (future).
Layer 2: Interface
Consumes a Transport::Stream and produces call protocol sessions. An
interface is what SSH currently does: wrap a byte stream in session semantics.
#[async_trait]
pub trait Interface: Send + Sync + 'static {
type Session;
async fn accept(stream: TransportStream, config: &InterfaceConfig) -> Result<Self::Session>;
}
Interfaces:
- SSH interface — wraps existing
ServerHandlerlogic. SSH handshake, auth, channel multiplexing. The call protocol runs over a reserved SSH channel (alknet-control:0). - Raw framing interface — 4-byte big-endian length prefix + JSON
EventEnvelope. No SSH overhead. Direct call protocol over the transport stream. - DNS control channel — a (DNS transport, raw framing interface) pair that
encodes/decodes
EventEnvelopeframes as DNS query/response pairs.
Layer 3: Protocol
Carries semantics. Call protocol events, operation registry, service calls.
The protocol is agnostic to both the transport and the interface below it. It
receives EventEnvelope frames from whatever interface produced them.
Connection Model
A connection is always a (Transport, Interface) pair. The valid combinations are enumerated:
| Transport | Interface | Use case |
|---|---|---|
| TLS | SSH | Standard alknet tunnel |
| TCP | SSH | Plain SSH tunnel |
| iroh | SSH | P2P SSH tunnel |
| DNS | raw framing | DNS control channel |
| WebTransport | SSH | Browser SSH tunnel (future) |
| WebTransport | raw framing | Browser call protocol (future) |
| TCP | raw framing | Direct call protocol, local mesh |
The DNS control channel carries call protocol frames directly — it does NOT
wrap SSH inside DNS. This is explicit because the research originally
conflated "SSH tunneling over DNS" with "DNS as a transport for call protocol."
The (DNS, raw framing) pair sends EventEnvelope frames as DNS TXT
queries/responses — no SSH involved.
TransportKind Enum
The TransportKind enum (currently Tcp | Tls | Iroh) gains Dns and
WebTransport variants. Initially these are tags only — no acceptor
implementation. The full DNS and WebTransport implementations are Phase 4 work
per the integration plan.
pub enum TransportKind {
Tcp,
Tls { server_name: Option<String> },
Iroh { endpoint_id: String },
Dns { domain: String },
WebTransport { host: String },
}
ServerHandler Refactor
The existing ServerHandler is refactored into SshInterface. The interface
abstraction means the server's accept loop becomes:
// Pseudocode
let (transport, interface) = listener_config;
let stream = transport.accept().await?;
let session = interface.accept(stream, &config).await?;
// session produces call protocol events
The call protocol handler is interface-agnostic — it receives EventEnvelope
frames from any interface. Auth, forwarding policy, and operation routing happen
at Layer 3, not inside the SSH handler.
Consequences
- Positive: Enables DNS control channel without SSH wrapping. The (DNS, raw framing) pair is a clean (Transport, Interface) combination.
- Positive: Enables raw framing for local service mesh. No SSH overhead for trusted networks.
- Positive: SSH becomes pluggable. The same call protocol handler works with any interface.
- Positive:
ServerHandleris refactored intoSshInterface— a smaller, more focused component that only handles SSH session management. - Positive: Future WebTransport and WebSocket interfaces are additive — they
implement the
Interfacetrait without touching SSH code. - Negative: This is the most invasive code change in Phase 1
(integration-plan, Phase 1.8). SSH auth, channel management, and proxy logic
are currently tangled in
ServerHandler. Extracting them requires careful refactoring to maintain existing behavior. - Negative: The
Interfacetrait is new and untested. The design must accommodate both SSH's channel multiplexing and raw framing's single-stream model through the same abstraction.
References
- research/core.md — Transport layer, DNS transport section
- research/integration-plan.md — Phase 1.8, three-layer model
- transport.md — Current Transport trait (unchanged at Layer 1)
- server.md — Current ServerHandler (will become SshInterface)
- ADR-001 — Transport trait produces stream (unchanged)
- ADR-004 — SSH runs over transport (reinforced by Layer 2)
- ADR-024 — Bidirectional call protocol (Layer 3)