Files
alknet/docs/architecture/interface.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

8.0 KiB

status, last_updated
status last_updated
draft 2026-06-07

Interface (Layer 2)

What

The Interface layer sits between Transport (Layer 1) and Protocol (Layer 3). An Interface consumes a Transport::Stream and produces call protocol sessions. SSH is an interface, not a transport — it wraps a byte stream in session semantics. Raw framing (4-byte length prefix + JSON EventEnvelope) is another interface, one without SSH overhead.

Why

In the current architecture, SSH is deeply embedded in ServerHandler. This tangling of transport, interface, and protocol makes it impossible to:

  • Run the call protocol over DNS queries without wrapping SSH inside DNS
  • Use raw framing for local service mesh (no SSH overhead)
  • Support WebTransport direct call protocol for browsers
  • Separate auth mechanics from channel management

The three-layer model (ADR-026) cleanly separates these concerns. Transport produces bytes. Interface parses bytes into sessions. Protocol carries semantics. A connection is always a (Transport, Interface) pair.

Architecture

Three-Layer Model

Layer 3: Protocol    (Call protocol, Operations, OperationEnv)
Layer 2: Interface   (SSH, raw framing, HTTP/WS, DNS control channel)
Layer 1: Transport   (TCP, TLS, iroh, DNS, WebTransport)
  • Layer 1: Transport — produces byte streams (`AsyncRead + AsyncWrite + Unpin
    • Send`). Unchanged per ADR-001.
  • Layer 2: Interface — consumes a Transport::Stream and produces call protocol sessions. SSH does handshake + auth + channel multiplexing. Raw framing does length-prefix parsing.
  • Layer 3: Protocol — carries semantics. Call protocol events, operation registry, service calls. Agnostic to both Transport and Interface below it.

Interface Trait

#[async_trait]
pub trait Interface: Send + Sync + 'static {
    type Session;
    async fn accept(stream: TransportStream, config: &InterfaceConfig) -> Result<Self::Session>;
}

The session produced by an interface is consumed by the call protocol handler. Different interfaces produce different session types, but the call protocol handler receives EventEnvelope frames from any interface.

SshInterface

Wraps the existing ServerHandler logic. This is the most complex interface because SSH provides channel multiplexing, auth negotiation, and proxy management within a single session.

What stays in SshInterface (Layer 2):

  • SSH handshake and session management
  • Auth delegation to IdentityProvider (via auth_publickey() callback)
  • Channel multiplexing (multiple channels per session)
  • alknet-control:0 channel routing to call protocol

What moves to Layer 3 (call protocol handler):

  • Operation registry and dispatch
  • Forwarding policy checks (per ADR-031)
  • Operation context construction (Identity, scopes)

What moves to per-connection state:

  • Port forwarding proxy logic

RawFramingInterface

Reads 4-byte big-endian length prefix + JSON EventEnvelope frames directly from the transport stream. No SSH wrapping. No channel multiplexing — the entire stream is a single call protocol channel.

pub struct RawFramingInterface;

impl Interface for RawFramingInterface {
    type Session = RawFramingSession;
    // Reads length-prefixed EventEnvelope frames from the stream
}

Used for:

  • DNS control channel (DNS transport + raw framing)
  • Local service mesh (TCP + raw framing, no SSH overhead)
  • Browser direct call protocol (WebTransport + raw framing, future)

DNS Control Channel

A (DNS transport, raw framing interface) pair. The DNS transport encodes EventEnvelope frames as DNS query/response pairs. The raw framing interface parses them directly — NOT SSH inside DNS.

Client: Encode EventEnvelope as base32 DNS query labels
  → DNS Transport → DNS Server → Raw Framing Interface → Call Protocol Handler

Server: Return EventEnvelope as DNS TXT record response
  ← Raw Framing Interface ← DNS Transport ← Call Protocol Handler

Valid (Transport, Interface) Pairs

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

InterfaceConfig

Different interfaces require different configuration:

pub enum InterfaceConfig {
    Ssh(SshInterfaceConfig),
    RawFraming(RawFramingConfig),
}

pub struct SshInterfaceConfig {
    pub auth: Arc<dyn IdentityProvider>,
    pub forwarding: Arc<ArcSwap<DynamicConfig>>, // for ForwardingPolicy
    pub host_key: Arc<PrivateKey>,
}

pub struct RawFramingConfig {
    // No SSH-specific config needed
    // Auth is handled by the transport layer (e.g., token auth for WebTransport)
    // or by the call protocol layer
}

Auth Across Interfaces

  • SshInterface: Auth happens during SSH handshake via IdentityProvider::resolve_from_fingerprint(). The authenticated Identity is attached to the session.
  • RawFramingInterface: Auth is handled by the transport (e.g., token auth for WebTransport via IdentityProvider::resolve_from_token()) or by the call protocol layer (operation-level ACL).

Both paths produce the same Identity type (ADR-029).

Server Accept Loop

With the Interface trait, the accept loop becomes:

for listener in listeners {
    let (transport, interface) = listener;
    tokio::spawn(async move {
        loop {
            let stream = transport.accept().await?;
            let session = interface.accept(stream, &config).await?;
            // session produces call protocol events
            // call protocol handler is interface-agnostic
        }
    });
}

Constraints

  • The Interface trait must accommodate both SSH's channel multiplexing and raw framing's single-stream model through the same abstraction.
  • SshInterface is the most invasive refactoring in Phase 1. The existing ServerHandler owns auth, channel management, and proxy logic — extracting these cleanly requires careful design (integration-plan, Phase 1.8).
  • DNS transport implementation is Phase 4 work. The TransportKind::Dns variant and RawFramingInterface are defined now; implementation is deferred.
  • WebTransport is Phase 4 work. The TransportKind::WebTransport variant is a tag only for now.

Open Questions

  • OQ-IF-01: How does the Interface session type relate to the call protocol's EventEnvelope stream? Does every session implement Stream<Item=EventEnvelope>? This needs design during Phase 1.8.

  • OQ-IF-02: Should SshInterface own the ForwardingPolicy check for channel_open_direct_tcpip, or should that move to Layer 3? Current thinking: the forwarding check is a Layer 3 concern (it's policy, not session mechanics), but the channel open/close lifecycle is Layer 2. The Interface reports channel open requests to Layer 3; Layer 3 applies ForwardingPolicy and tells Layer 2 whether to proxy.

Design Decisions

ADR Decision Summary
026 Three-layer model SSH is Layer 2, not Layer 1
033 OperationEnv Protocol is interface-agnostic
029 Identity as core type Auth resolution across interfaces
031 Forwarding policy Layer 3 policy applied to Layer 2 channel requests

References