Files
alknet/docs/architecture/decisions/026-transport-interface-separation.md
glm-5.1 19b3d3a078 docs: write Phase 0 architecture foundation — ADRs 026-034, spec docs, and task updates
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.
2026-06-07 09:32:58 +00:00

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 EventEnvelope without 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 ServerHandler logic. 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 EventEnvelope frames 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: ServerHandler is refactored into SshInterface — a smaller, more focused component that only handles SSH session management.
  • Positive: Future WebTransport and WebSocket interfaces are additive — they implement the Interface trait 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 Interface trait 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