Decompose architecture into 35 atomic tasks across 10 generations for implementation

This commit is contained in:
2026-06-02 09:02:55 +00:00
parent b5c59ef3bc
commit 14dbd81195
35 changed files with 1636 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
---
id: client/channel-manager
name: Implement ChannelManager — SSH session management, channel opens, reconnection
status: pending
depends_on:
- auth/client-auth-handler
- transport/trait-and-types
- auth/error-types
scope: moderate
risk: high
impact: component
level: implementation
---
## Description
Implement the `ChannelManager` that owns the `Arc<client::Handle<ClientHandler>>` and provides the core client methods:
- `open_direct_tcpip(host, port)` — open a tunnel channel to a remote host
- `open_streamlocal(socket_path)` — open a tunnel to a Unix socket (stub for now)
- `request_tcpip_forward(addr, port)` — request remote listening
- `cancel_tcpip_forward(addr, port)` — cancel remote listening
Most importantly, the channel manager handles **reconnection** on transport failure:
1. Detect via `handle.is_closed()` or transport read error
2. Exponential backoff reconnect (1s, 2s, 4s, ... max 30s)
3. Re-establish transport connection (call `transport.connect()` again)
4. Re-authenticate SSH session
5. Notify SOCKS5 server and port forwards (in-flight connections fail, new connections work)
Reconnection is always enabled. The backoff caps at 30 seconds and continues indefinitely.
## Acceptance Criteria
- [ ] `crates/wraith-core/src/client/channel_manager.rs` exports `ChannelManager`
- [ ] `ChannelManager` holds: `Arc<Transport>`, `Arc<ClientAuthConfig>`, `Arc<client::Handle<ClientHandler>>` (behind RwLock for reconnection)
- [ ] `ChannelManager::new()` establishes initial transport connection, authenticates, returns manager
- [ ] `open_direct_tcpip(host, port)` — opens SSH channel to target
- [ ] `request_tcpip_forward(addr, port)` — sends `tcpip_forward` request
- [ ] `cancel_tcpip_forward(addr, port)` — sends `cancel_tcpip_forward` request
- [ ] Reconnection detection: monitors `handle.is_closed()`, triggers reconnect on failure
- [ ] Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (cap), continues indefinitely
- [ ] Full reconnect: new transport stream, new SSH session over it (ADR-004)
- [ ] After reconnect: port forward listeners (`-L`, `-R`) re-registered with new session
- [ ] In-flight connections on old session fail gracefully (channel errors, not session-wide)
- [ ] Unit tests: channel open, reconnection trigger, backoff timing, forward re-registration
## References
- docs/architecture/client.md — Channel Manager section, Reconnection section
- docs/architecture/decisions/004-ssh-over-transport.md — full reconnect, not "SSH reconnects over same transport"
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion

View File

@@ -0,0 +1,59 @@
---
id: client/connect-options
name: Implement ConnectOptions struct and client session orchestration with graceful shutdown
status: pending
depends_on:
- client/channel-manager
- client/socks5-server
- client/port-forwarding
- transport/trait-and-types
scope: moderate
risk: medium
impact: component
level: implementation
---
## Description
Implement `ConnectOptions` — the programmatic configuration struct (ADR-011) for the client — and the top-level client session orchestrator that ties together transport, channel manager, SOCKS5 server, and port forwards.
The client session lifecycle:
1. Create transport based on `ConnectOptions`
2. Connect transport, authenticate SSH session
3. Start SOCKS5 server
4. Start port forward listeners
5. Run until SIGTERM/SIGINT or fatal error
6. Graceful shutdown
Graceful shutdown (SIGTERM/SIGINT):
1. Stop accepting new SOCKS5 connections and port forward connections
2. Send SSH disconnect message to server
3. Wait for in-flight data to drain (~2 second timeout)
4. Close transport stream
5. Exit
## Acceptance Criteria
- [ ] `crates/wraith-core/src/client/mod.rs` re-exports all client components
- [ ] `ConnectOptions` struct with fields matching client.md CLI interface: `server`, `peer`, `transport_mode`, `identity`, `socks5_addr`, `forwards`, `remote_forwards`, `proxy`, `iroh_relay`, `tls_server_name`, `insecure`
- [ ] `ConnectOptions::identity` accepts `KeySource` (file or in-memory)
- [ ] `ClientSession::new(opts: ConnectOptions) -> Result<Self>` — creates transport, connects, authenticates
- [ ] `ClientSession::run()` — starts SOCKS5 server, port forwards, waits for shutdown signal
- [ ] SOCKS5 is always enabled when running (per constraint)
- [ ] Port forwards are optional and started based on `ConnectOptions`
- [ ] `ClientSession::shutdown()` — graceful shutdown: stop accepting, send SSH disconnect, drain timeout, close
- [ ] SIGTERM/SIGINT handled via tokio signal
- [ ] Integration test: full client-to-server session via mock transport, SOCKS5 proxy works, shutdown completes
## References
- docs/architecture/client.md — full client spec, CLI interface, graceful shutdown
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — ConnectOptions programmatic struct
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion

View File

@@ -0,0 +1,53 @@
---
id: client/port-forwarding
name: Implement port forwarding — local (-L) and remote (-R) forwards
status: pending
depends_on:
- auth/client-auth-handler
- transport/trait-and-types
- auth/error-types
scope: moderate
risk: medium
impact: component
level: implementation
---
## Description
Implement SSH port forwarding per client.md:
**Local port forwards (`-L local_addr:local_port:remote_host:remote_port`)**:
1. Bind `TcpListener` on `local_addr:local_port`
2. For each accepted connection, open `channel_open_direct_tcpip(remote_host, remote_port, ...)`
3. Proxy bytes bidirectionally via `copy_bidirectional`
**Remote port forwards (`-R remote_addr:remote_port:local_host:local_port`)**:
1. Send `tcpip_forward(remote_addr, remote_port)` to request the server listen on a port
2. When the handler receives `server_channel_open_forwarded_tcpip`, connect to `local_host:local_port`
3. Proxy bytes bidirectionally
Both types are specified as repeatable `--forward` / `--remote-forward` CLI options.
## Acceptance Criteria
- [ ] `crates/wraith-core/src/client/forward.rs` exports `PortForwardSpec`, `LocalForwarder`, `RemoteForwarder`
- [ ] `PortForwardSpec` parses `-L` / `-R` spec strings: `local_addr:local_port:remote_host:remote_port`
- [ ] `LocalForwarder` binds TcpListener, accepts connections, opens SSH direct-tcpip channel for each, proxies bidirectionally
- [ ] `RemoteForwarder` sends `tcpip_forward` request, handles `forwarded-tcpip` channel opens, connects to local target, proxies bidirectionally
- [ ] Both forwarders handle their accept loops concurrently with the SOCKS5 server
- [ ] Connection errors close the individual channel without affecting other forwards or the SSH session
- [ ] Port forward listeners are re-registered after SSH reconnection (depends on channel-manager)
- [ ] Unit tests: spec parsing, local forward proxy, remote forward request handling
## References
- docs/architecture/client.md — Port Forwarding section
- docs/architecture/decisions/005-socks5-before-tun.md — port forwarding as optional complement to SOCKS5
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion

View File

@@ -0,0 +1,50 @@
---
id: client/socks5-server
name: Implement SOCKS5 server — local proxy that forwards through SSH channels
status: pending
depends_on:
- auth/client-auth-handler
- transport/trait-and-types
- auth/error-types
scope: moderate
risk: medium
impact: component
level: implementation
---
## Description
Implement the local SOCKS5 proxy server — the primary client interface (ADR-005). Listens on a local port (default `127.0.0.1:1080`), accepts SOCKS5 connections, and for each connection:
1. Reads the SOCKS5 handshake (auth method negotiation, target address)
2. Opens `channel_open_direct_tcpip(target_host, target_port, originator_addr, originator_port)` on the SSH session
3. Converts the SSH channel to a stream via `channel.into_stream()`
4. Runs `tokio::io::copy_bidirectional(&mut local_socket, &mut ssh_stream)` to proxy data
Supports SOCKS5h (domain names resolved server-side) by default. This prevents DNS leaks — the client never resolves target hostnames locally (ADR-006).
## Acceptance Criteria
- [ ] `crates/wraith-core/src/socks5/mod.rs` exports `Socks5Server`
- [ ] `Socks5Server` binds to configurable listen address (default `127.0.0.1:1080`)
- [ ] SOCKS5 handshake: method negotiation (no-auth only), target address parsing (IPv4, IPv6, domain name)
- [ ] Domain name targets (SOCKS5h) sent unresolved to server — no local DNS resolution
- [ ] For each SOCKS5 connection, opens SSH `direct_tcpip` channel and proxies bytes bidirectionally
- [ ] Connection errors (SSH session down, channel open failed) result in SOCKS5 error response to client
- [ ] No logging of SOCKS5 request targets (ADR-006) — only connection-level events logged
- [ ] SOCKS5 server always enabled when `wraith connect` runs (per client.md constraint)
- [ ] Unit tests: SOCKS5 handshake parsing, address type handling, bidirectional proxy flow (with mock transport)
## References
- docs/architecture/client.md — SOCKS5 Server section
- docs/architecture/decisions/005-socks5-before-tun.md — SOCKS5 as primary interface
- docs/architecture/decisions/006-no-logging-of-tunnel-destinations.md — no DNS leak, no logging
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion