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

221 lines
8.0 KiB
Markdown

---
status: draft
last_updated: 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
```rust
#[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.
```rust
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:
```rust
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:
```rust
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](decisions/026-transport-interface-separation.md) | Three-layer model | SSH is Layer 2, not Layer 1 |
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Protocol is interface-agnostic |
| [029](decisions/029-identity-core-type.md) | Identity as core type | Auth resolution across interfaces |
| [031](decisions/031-forwarding-policy.md) | Forwarding policy | Layer 3 policy applied to Layer 2 channel requests |
## References
- [research/integration-plan.md](../research/integration-plan.md) — Phase 1.8, valid (Transport, Interface) pairs
- [research/core.md](../research/core.md) — DNS transport, three-layer model
- [ADR-026](decisions/026-transport-interface-separation.md) — Transport/interface separation
- [transport.md](transport.md) — Transport trait (unchanged at Layer 1)
- [server.md](server.md) — Current ServerHandler (will become SshInterface)
- [identity.md](identity.md) — IdentityProvider, auth across interfaces