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.
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::Streamand 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(viaauth_publickey()callback) - Channel multiplexing (multiple channels per session)
alknet-control:0channel 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 authenticatedIdentityis 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.
SshInterfaceis the most invasive refactoring in Phase 1. The existingServerHandlerowns 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::Dnsvariant andRawFramingInterfaceare defined now; implementation is deferred. - WebTransport is Phase 4 work. The
TransportKind::WebTransportvariant is a tag only for now.
Open Questions
-
OQ-IF-01: How does the
Interfacesession type relate to the call protocol'sEventEnvelopestream? Does every session implementStream<Item=EventEnvelope>? This needs design during Phase 1.8. -
OQ-IF-02: Should
SshInterfaceown theForwardingPolicycheck forchannel_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 appliesForwardingPolicyand 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
- research/integration-plan.md — Phase 1.8, valid (Transport, Interface) pairs
- research/core.md — DNS transport, three-layer model
- ADR-026 — Transport/interface separation
- transport.md — Transport trait (unchanged at Layer 1)
- server.md — Current ServerHandler (will become SshInterface)
- identity.md — IdentityProvider, auth across interfaces