72 lines
5.5 KiB
Markdown
72 lines
5.5 KiB
Markdown
---
|
|
id: ssh-session-call-protocol-bridge
|
|
name: Bridge SshSession recv/send to call protocol via alknet-control:0 channel
|
|
status: completed
|
|
depends_on: [stream-interface-message-interface-split]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
---
|
|
|
|
## Description
|
|
|
|
Implement `SshSession::recv()` and `SshSession::send()` to bridge SSH channel data to and from the call protocol's `InterfaceEvent`/`EventEnvelope` frames. Currently both methods are stubs: `recv()` always returns `None` and `send()` silently discards.
|
|
|
|
Per the integration plan section 2.1 and interface.md (OQ-IF-01, resolved):
|
|
|
|
The bridge works as follows:
|
|
- When `SshHandler::channel_open_direct_tcpip` detects a destination starting with `alknet-`, it currently accepts the channel but doesn't bridge the data. The `ControlChannelRouter` exists in `control_channel.rs` but has no handler wired.
|
|
- `SshSession::recv()` should read `EventEnvelope` frames from the `alknet-control:0` channel stream (using the 4-byte length prefix + JSON wire format from `call::frame::{encode, decode}`), wrap them in `InterfaceEvent` with the session's `Identity` (obtained during SSH auth).
|
|
- `SshSession::send()` should write `EventEnvelope` frames to the `alknet-control:0` channel stream using the same framing format.
|
|
- The `ControlChannelRouter` should be wired to bridge incoming channel data to the call protocol handler.
|
|
|
|
**Current state of the code**:
|
|
- `SshSession::recv()` returns `None` (stub)
|
|
- `SshSession::send()` discards silently (stub)
|
|
- `ControlChannelRouter` in `control_channel.rs` has `route()` and `has_handler()` but no handler is registered
|
|
- `call::frame::{encode, decode}` functions exist and are well-tested (4-byte BE length prefix + JSON)
|
|
- `SshHandler` detects `alknet-*` destinations in `channel_open_direct_tcpip` but doesn't bridge data
|
|
- `SshHandler` stores `authenticated_identity: Option<Identity>` from SSH auth
|
|
- `InterfaceEvent` struct carries `EventEnvelope` + `Option<Identity>` — already defined
|
|
|
|
**Key design considerations**:
|
|
- The `SshSession` needs access to the SSH channel's data stream to read/write `EventEnvelope` frames. This requires getting the `russh::Channel` data stream and framing it.
|
|
- The `ControlChannelRouter` currently uses `Box<dyn DuplexStream>` — it can be wired as a `ControlChannelHandler` that reads frames from the stream and produces `InterfaceEvent`s.
|
|
- The `alknet-control:0` channel is the first SSH direct-tcpip channel with the `alknet-control` destination. Additional `alknet-*` channels may follow.
|
|
- The session's `Identity` (from SSH auth) must be attached to every `InterfaceEvent` produced by `recv()`.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] `SshSession::recv()` reads `EventEnvelope` frames from the SSH channel data stream and produces `InterfaceEvent` with the session's `Identity`
|
|
- [ ] `SshSession::send()` writes `EventEnvelope` frames to the SSH channel data stream using `call::frame::encode`
|
|
- [ ] `ControlChannelRouter` is wired as the handler for `alknet-control:0` channels, bridging SSH channel data to the call protocol
|
|
- [ ] Frame encoding matches `call::frame::{encode, decode}` — 4-byte big-endian length prefix + UTF-8 JSON body
|
|
- [ ] The session's `Identity` (from `SshHandler::authenticated_identity`) is attached to every `InterfaceEvent` produced by `recv()`
|
|
- [ ] `SshHandler::channel_open_direct_tcpip` correctly routes `alknet-control:0` channels to the `ControlChannelRouter` handler
|
|
- [ ] Unit test: `SshSession` can round-trip an `EventEnvelope` through `send()` and `recv()` (using a mock channel stream)
|
|
- [ ] Unit test: `ControlChannelRouter.with_handler()` successfully routes channel data
|
|
- [ ] All existing server/auth/transport tests continue to pass
|
|
- [ ] No behavioral changes for non-`alknet-*` channel forwarding (port proxy logic unchanged)
|
|
|
|
## References
|
|
|
|
- docs/research/integration-plan.md — Phase 2.1
|
|
- docs/architecture/interface.md — OQ-IF-01 resolution, InterfaceEvent model
|
|
- docs/architecture/call-protocol.md — EventEnvelope, frame encoding
|
|
- crates/alknet-core/src/interface/ssh.rs — SshSession stubs (recv/send)
|
|
- crates/alknet-core/src/server/control_channel.rs — ControlChannelRouter
|
|
- crates/alknet-core/src/call/frame.rs — frame encode/decode
|
|
- crates/alknet-core/src/interface/session.rs — InterfaceEvent, InterfaceSession traits
|
|
|
|
## Notes
|
|
|
|
> This is the highest-risk task in Phase 2. The `russh` channel data stream API needs careful handling — getting a `Channel`'s data stream for async reading/writing is non-trivial and may require understanding russh's `data()` callback pattern vs. the `Channel::into_stream()` method.
|
|
|
|
> Consider implementing incrementally: first wire the `ControlChannelRouter` handler to produce `InterfaceEvent`s from raw channel data, then connect that to `SshSession::recv()`/`send()`. Each step should have passing tests before proceeding.
|
|
|
|
> The `SshSession` struct currently holds a `server::Handle` and a `JoinHandle`. It may need additional fields to track the control channel stream and the authenticated identity for producing `InterfaceEvent`s with identity attached.
|
|
|
|
## Summary
|
|
|
|
> Implemented SshSession recv/send bridge to call protocol via alknet-control:0 channel. Added FrameFramedReader/FrameFramedWriter for async length-prefixed EventEnvelope I/O. SshSession::recv() reads InterfaceEvents from mpsc channel bridged from SSH channel. SshSession::send() writes EventEnvelopes to mpsc channel bridged to SSH channel. ControlChannelBridge implements ControlChannelHandler. SshHandler routes alknet-control:0 channels to bridge task using tokio::select!. Session Identity attached to every InterfaceEvent. |