Decompose architecture into 35 atomic tasks across 10 generations for implementation
This commit is contained in:
40
tasks/auth/client-auth-handler.md
Normal file
40
tasks/auth/client-auth-handler.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: auth/client-auth-handler
|
||||
name: Implement client-side SSH authentication with Ed25519 key pairs
|
||||
status: pending
|
||||
depends_on:
|
||||
- auth/key-loading
|
||||
- auth/error-types
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the client-side SSH authentication. The client presents an Ed25519 private key during SSH handshake. This creates the `russh::client::Handler` implementation and the `russh::client::ConnectStreamConfig` that uses the loaded key.
|
||||
|
||||
No password auth. The client handler is simpler than the server — it just needs to provide the private key and handle the auth callback from russh.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/auth/client_auth.rs` exports `ClientAuthConfig` and client handler
|
||||
- [ ] `ClientAuthConfig` holds: `private_key: KeyPair`, optional `public_key: PublicKey`
|
||||
- [ ] `ClientAuthConfig::from_key_source(source: KeySource) -> Result<Self>` — loads key via key-loading module
|
||||
- [ ] Implements `russh::client::Handler` with `auth_publickey()` returning the public key
|
||||
- [ ] Client handler returns `russh::client::AuthResult::Accept` or appropriate auth state
|
||||
- [ ] Unit tests: valid key creates handler, auth flow succeeds with mock SSH session
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — "Authentication is Ed25519 public key or OpenSSH certificate (ADR-012)"
|
||||
- docs/architecture/decisions/012-auth-ed25519-and-cert-authority.md — key-based auth only
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
47
tasks/auth/error-types.md
Normal file
47
tasks/auth/error-types.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: auth/error-types
|
||||
name: Define error types for transport, auth, channel, and configuration layers
|
||||
status: pending
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: trivial
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define the error hierarchy per the overview.md layered error pattern:
|
||||
- **Transport errors** — connection failures, TLS handshake failures, iroh endpoint errors
|
||||
- **Auth errors** — key rejection, certificate validation failures, missing keys
|
||||
- **Channel errors** — target unreachable, proxy failure
|
||||
- **Config errors** — invalid flags, key file not found, bind failure
|
||||
|
||||
Use `thiserror` for structured error types propagated via `anyhow::Result` in the public API. The key design: transport/auth errors cause reconnection (client) or rejection (server). Channel-level errors close that channel without killing the session.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/error.rs` exports error types
|
||||
- [ ] `TransportError` enum: `ConnectionFailed`, `HandshakeFailed`, `Timeout`, `ProxyFailed`
|
||||
- [ ] `AuthError` enum: `KeyRejected`, `CertInvalid`, `CertExpired`, `CertPrincipalMismatch`, `NoMatchingKey`
|
||||
- [ ] `ChannelError` enum: `TargetUnreachable`, `ProxyConnectFailed`, `ChannelClosed`
|
||||
- [ ] `ConfigError` enum: `InvalidFlag`, `KeyFileNotFound`, `BindFailed`, `IncompatibleOptions`
|
||||
- [ ] All error types implement `std::error::Error` via `thiserror`, `Display`, and `Debug`
|
||||
- [ ] Error types have `source` chaining where appropriate (e.g., `TransportError::HandshakeFailed { source: std::io::Error }`)
|
||||
- [ ] Re-exported from `crates/wraith-core/src/lib.rs`
|
||||
- [ ] Unit tests for Display output of each error variant
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md — "Error handling follows a consistent layered pattern"
|
||||
- docs/architecture/client.md — error handling section (transport → reconnect, channel → close)
|
||||
- docs/architecture/server.md — error handling section
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
51
tasks/auth/key-loading.md
Normal file
51
tasks/auth/key-loading.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
id: auth/key-loading
|
||||
name: Implement SSH key material loading (file paths and in-memory data)
|
||||
status: pending
|
||||
depends_on:
|
||||
- auth/error-types
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement key material loading that accepts both file paths and in-memory data per the programmatic-first API (ADR-011). Key inputs (`--identity`, `--authorized-keys`, `--cert-authority`, `--key`) accept either:
|
||||
- **File path**: load from filesystem
|
||||
- **In-memory data**: raw key bytes provided programmatically
|
||||
|
||||
All keys must be in **OpenSSH key format** (not PEM/PKCS#1/PKCS#8). This module handles:
|
||||
- Loading private keys (OpenSSH format: `-----BEGIN OPENSSH PRIVATE KEY-----`)
|
||||
- Loading public keys (OpenSSH format: `ssh-ed25519 AAAA... user@host`)
|
||||
- Loading authorized_keys files (standard OpenSSH format)
|
||||
- Parsing `cert-authority` entries in authorized_keys
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/auth/keys.rs` exports key loading functions
|
||||
- [ ] `KeySource` enum: `File(PathBuf)` and `Memory(Vec<u8>)` for unified key input handling
|
||||
- [ ] `load_private_key(source: KeySource) -> Result<russh::key::KeyPair>` — loads OpenSSH private key from file or memory
|
||||
- [ ] `load_public_keys(source: KeySource) -> Result<Vec<russh::key::PublicKey>>` — loads one or more public keys from authorized_keys format
|
||||
- [ ] Parses standard `authorized_keys` format including options (e.g., `cert-authority,permit-port-forwarding ssh-ed25519 AAAA...`)
|
||||
- [ ] `CertAuthorityEntry` struct: `public_key: PublicKey, options: Vec<String>` parsed from authorized_keys cert-authority lines
|
||||
- [ ] Returns `ConfigError::KeyFileNotFound` for missing file paths
|
||||
- [ ] Returns `ConfigError::InvalidFlag` with clear message for PEM-encoded (non-OpenSSH) keys
|
||||
- [ ] Unit tests: load Ed25519 key from file, load from memory, parse authorized_keys with multiple entries, reject PEM format
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — Key Material Format section
|
||||
- docs/architecture/server.md — Key Material Format section
|
||||
- docs/architecture/decisions/012-auth-ed25519-and-cert-authority.md — authorized_keys format with cert-authority
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — programmatic-first, file paths or in-memory
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
47
tasks/auth/server-auth-handler.md
Normal file
47
tasks/auth/server-auth-handler.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: auth/server-auth-handler
|
||||
name: Implement server-side authentication (Ed25519 keys + OpenSSH cert-authority)
|
||||
status: pending
|
||||
depends_on:
|
||||
- auth/key-loading
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the server-side SSH authentication logic per ADR-012:
|
||||
|
||||
1. **Ed25519 public key**: `auth_publickey()` checks presented key against the authorized set using constant-time comparison
|
||||
2. **OpenSSH certificate authority**: validates presented certificate — checks CA signature, expiry, and principal restrictions (`permit-port-forwarding`, `no-pty`, `source-address`)
|
||||
|
||||
No password authentication over SSH. This is the `russh::server::Handler::auth_publickey()` implementation that the server handler will call.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/auth/server_auth.rs` exports `ServerAuthConfig` and auth logic
|
||||
- [ ] `ServerAuthConfig` holds: `authorized_keys: HashSet<PublicKey>`, `cert_authorities: Vec<CertAuthorityEntry>`
|
||||
- [ ] `ServerAuthConfig::from_keys_and_ca()` constructor: loads authorized keys and cert-authority entries from provided key sources
|
||||
- [ ] Auth check function: given a presented key/certificate, return `Accept` or `Reject`
|
||||
- [ ] Ed25519 key matching uses constant-time comparison (via `russh`/`ssh-key` crate builtins)
|
||||
- [ ] Certificate validation checks: CA signature valid, cert not expired, principal restrictions enforced
|
||||
- [ ] Certificate options respected: `permit-port-forwarding`, `no-pty`, `source-address`
|
||||
- [ ] Returns `AuthError::KeyRejected` or `AuthError::CertInvalid`/`CertExpired`/`CertPrincipalMismatch` on failure
|
||||
- [ ] Unit tests: valid key accepted, invalid key rejected, cert-authority signed cert accepted, expired cert rejected, wrong principal rejected
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Authentication section
|
||||
- docs/architecture/decisions/012-auth-ed25519-and-cert-authority.md — ADR for key + cert-authority
|
||||
- docs/architecture/client.md — "Authentication is Ed25519 public key or OpenSSH certificate"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
49
tasks/cli/connect-command.md
Normal file
49
tasks/cli/connect-command.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: cli/connect-command
|
||||
name: Implement `wraith connect` CLI subcommand with clap
|
||||
status: pending
|
||||
depends_on:
|
||||
- client/connect-options
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the `wraith connect` CLI subcommand using `clap` with derive macros. Translates `ConnectOptions` into CLI flags and runs the client session. All options from client.md CLI interface must be supported.
|
||||
|
||||
Environment variable defaults: `WRAITH_SERVER`, `WRAITH_IDENTITY` as convenience defaults per ADR-011.
|
||||
|
||||
`--proxy` with `--transport tcp` should warn or be a no-op (ADR-019: client proxy wraps transport, and TCP transport is already direct).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `wraith connect` subcommand flags match client.md CLI interface: `--server`, `--peer`, `--transport`, `--identity`, `--socks5`, `--forward`, `--remote-forward`, `--proxy`, `--iroh-relay`, `--tls-server-name`, `--insecure`
|
||||
- [ ] `--server` required for tcp/tls transport (validated at parse time or runtime)
|
||||
- [ ] `--peer` required for iroh transport (validated)
|
||||
- [ ] `--identity` required for all transports
|
||||
- [ ] `--transport` defaults to `tcp`
|
||||
- [ ] `--socks5` defaults to `127.0.0.1:1080`
|
||||
- [ ] `--forward` is repeatable (clap `multiple_occurrences`)
|
||||
- [ ] `--remote-forward` is repeatable
|
||||
- [ ] Environment variable defaults: `WRAITH_SERVER` for `--server`, `WRAITH_IDENTITY` for `--identity`
|
||||
- [ ] `--proxy` with `--transport tcp` prints warning (ADR-019: effectively no-op)
|
||||
- [ ] CLI translates args into `ConnectOptions` and calls `ClientSession::new(opts).run().await`
|
||||
- [ ] Errors reported to stderr with non-zero exit code
|
||||
- [ ] `cargo run -p wraith -- connect --help` shows all flags with descriptions
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — CLI Interface section with all flags
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — env var defaults
|
||||
- docs/architecture/decisions/019-proxy-dual-semantics.md — client proxy semantics
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
47
tasks/cli/serve-command.md
Normal file
47
tasks/cli/serve-command.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: cli/serve-command
|
||||
name: Implement `wraith serve` CLI subcommand with clap
|
||||
status: pending
|
||||
depends_on:
|
||||
- server/serve-loop
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the `wraith serve` CLI subcommand using `clap` with derive macros. This translates `ServeOptions` into CLI flags and runs the server. All options from server.md CLI interface must be supported.
|
||||
|
||||
Environment variable defaults: none mandated for serve, but consistent with programmatic-first API.
|
||||
|
||||
The binary is the `wraith` crate at `crates/wraith/src/main.rs`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith/src/main.rs` defines CLI with clap derive: `wraith` with `serve` and `connect` subcommands (connect stub for now)
|
||||
- [ ] `wraith serve` subcommand flags match server.md CLI interface exactly: `--key`, `--authorized-keys`, `--cert-authority`, `--transport`, `--listen`, `--tls-cert`, `--tls-key`, `--acme-domain`, `--stealth`, `--proxy`, `--iroh-relay`, `--max-connections-per-ip`, `--max-auth-attempts`
|
||||
- [ ] `--key` is required (no default)
|
||||
- [ ] `--transport` defaults to `tcp`
|
||||
- [ ] `--listen` defaults to `0.0.0.0:22`
|
||||
- [ ] `--stealth` validates that `--transport tls` is set; error otherwise
|
||||
- [ ] `--transport iroh` prints endpoint ID on startup
|
||||
- [ ] `--acme-domain` requires `acme` feature (compile-time or runtime error if missing)
|
||||
- [ ] Key inputs accept file paths (strings); in-memory key data is a library/API concern, not CLI
|
||||
- [ ] CLI translates args into `ServeOptions` and calls `Server::new(opts).run().await`
|
||||
- [ ] Errors reported to stderr with non-zero exit code
|
||||
- [ ] `cargo run -p wraith -- serve --help` shows all flags with descriptions
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — CLI Interface section with all flags
|
||||
- docs/architecture/overview.md — "A single binary with subcommands"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
59
tasks/client/channel-manager.md
Normal file
59
tasks/client/channel-manager.md
Normal 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
|
||||
59
tasks/client/connect-options.md
Normal file
59
tasks/client/connect-options.md
Normal 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
|
||||
53
tasks/client/port-forwarding.md
Normal file
53
tasks/client/port-forwarding.md
Normal 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
|
||||
50
tasks/client/socks5-server.md
Normal file
50
tasks/client/socks5-server.md
Normal 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
|
||||
38
tasks/meta/auth-layer.md
Normal file
38
tasks/meta/auth-layer.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: meta/auth-layer
|
||||
name: Complete auth layer — error types, key loading, server auth, client auth
|
||||
status: pending
|
||||
depends_on:
|
||||
- auth/error-types
|
||||
- auth/key-loading
|
||||
- auth/server-auth-handler
|
||||
- auth/client-auth-handler
|
||||
scope: system
|
||||
risk: medium
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all auth module tasks. Once complete, the auth layer provides key loading from file or memory, server-side Ed25519 key + cert-authority validation, and client-side key-based authentication.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All auth tasks completed
|
||||
- [ ] Key loading supports file paths and in-memory data in OpenSSH format
|
||||
- [ ] Server accepts Ed25519 keys and cert-authority signed certificates
|
||||
- [ ] Client presents Ed25519 key pairs
|
||||
- [ ] Error types cover transport, auth, channel, and config failures
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md, docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
34
tasks/meta/cli-layer.md
Normal file
34
tasks/meta/cli-layer.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: meta/cli-layer
|
||||
name: Complete CLI layer — wraith serve and wraith connect commands
|
||||
status: pending
|
||||
depends_on:
|
||||
- cli/serve-command
|
||||
- cli/connect-command
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters CLI tasks. Once complete, the `wraith` binary has both `serve` and `connect` subcommands with all flags matching the architecture specs.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Both CLI tasks completed
|
||||
- [ ] `wraith serve --help` and `wraith connect --help` match architecture spec flag lists
|
||||
- [ ] End-to-end: `wraith serve` + `wraith connect` establishes working SSH tunnel
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md, docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
40
tasks/meta/client-layer.md
Normal file
40
tasks/meta/client-layer.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: meta/client-layer
|
||||
name: Complete client layer — SOCKS5, port forwarding, channel manager, ConnectOptions
|
||||
status: pending
|
||||
depends_on:
|
||||
- client/socks5-server
|
||||
- client/port-forwarding
|
||||
- client/channel-manager
|
||||
- client/connect-options
|
||||
scope: system
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all client module tasks. Once complete, the client establishes SSH sessions via any transport, runs a local SOCKS5 proxy, manages port forwards, handles reconnection with exponential backoff, and shuts down gracefully.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All client tasks completed
|
||||
- [ ] SOCKS5 proxy works with DNS leak prevention (SOCKS5h)
|
||||
- [ ] Local and remote port forwarding work
|
||||
- [ ] Channel manager handles reconnection with exponential backoff (1s → 30s cap)
|
||||
- [ ] Port forwards re-registered after reconnection
|
||||
- [ ] ConnectOptions programmatic struct and CLI flags available
|
||||
- [ ] Graceful shutdown on SIGTERM/SIGINT
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
37
tasks/meta/napi-layer.md
Normal file
37
tasks/meta/napi-layer.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: meta/napi-layer
|
||||
name: Complete NAPI layer — project setup, connect(), serve()
|
||||
status: pending
|
||||
depends_on:
|
||||
- napi/project-setup
|
||||
- napi/connect-function
|
||||
- napi/serve-function
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters NAPI tasks. Once complete, the `@alkdev/wraith` Node.js native addon provides `connect()` and `serve()` returning duplex streams for TypeScript consumers.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All NAPI tasks completed
|
||||
- [ ] `connect()` returns Duplex stream, no SOCKS5, no port forwarding
|
||||
- [ ] `serve()` returns WraithServer with close() and onConnection events
|
||||
- [ ] Key material from Buffer (in-memory) and file paths both work
|
||||
- [ ] JS-to-Rust and Rust-to-JS error marshalling works correctly
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
43
tasks/meta/server-layer.md
Normal file
43
tasks/meta/server-layer.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
id: meta/server-layer
|
||||
name: Complete server layer — handler, channel proxy, stealth, rate limiting, control channel, serve loop
|
||||
status: pending
|
||||
depends_on:
|
||||
- server/handler
|
||||
- server/channel-proxy
|
||||
- server/stealth-mode
|
||||
- server/rate-limiting-and-logging
|
||||
- server/control-channel
|
||||
- server/serve-loop
|
||||
scope: system
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all server module tasks. Once complete, the server accepts SSH connections via any transport, authenticates clients, proxies channel traffic to TCP targets (directly or via proxy), handles stealth mode, rate limits connections, routes reserved `wraith-` destinations, and shuts down gracefully.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All server tasks completed
|
||||
- [ ] Server handles SSH connections over TCP, TLS, and iroh transports
|
||||
- [ ] Authentication via Ed25519 keys and cert-authority
|
||||
- [ ] Channel proxying with direct, SOCKS5, and HTTP CONNECT outbound modes
|
||||
- [ ] Stealth mode detects SSH vs HTTP and returns fake nginx 404
|
||||
- [ ] Rate limiting and structured logging
|
||||
- [ ] Control channel routing for `wraith-*` destinations
|
||||
- [ ] Graceful shutdown
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
39
tasks/meta/transport-layer.md
Normal file
39
tasks/meta/transport-layer.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
id: meta/transport-layer
|
||||
name: Complete transport layer — trait, TCP, TLS, iroh, ACME
|
||||
status: pending
|
||||
depends_on:
|
||||
- transport/trait-and-types
|
||||
- transport/tcp-transport
|
||||
- transport/tls-transport
|
||||
- transport/iroh-transport
|
||||
- transport/acme-cert-provisioning
|
||||
scope: system
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all transport module tasks. Once complete, the transport layer provides a clean `Transport`/`TransportAcceptor` abstraction with TCP, TLS (feature-gated), iroh (feature-gated), and ACME (feature-gated) implementations. All transports produce the `AsyncRead + AsyncWrite + Unpin + Send` streams that SSH consumes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All transport tasks completed
|
||||
- [ ] `Transport` trait produces duplex streams consumed by `russh::connect_stream()` / `russh::run_stream()`
|
||||
- [ ] TCP, TLS, iroh transports all work end-to-end
|
||||
- [ ] ACME cert provisioning integrates with TLS acceptor
|
||||
- [ ] Feature flags correctly gate optional transports
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
47
tasks/napi/connect-function.md
Normal file
47
tasks/napi/connect-function.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: napi/connect-function
|
||||
name: Implement NAPI connect() — single SSH channel as Duplex stream
|
||||
status: pending
|
||||
depends_on:
|
||||
- napi/project-setup
|
||||
- client/channel-manager
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the NAPI `connect()` function per ADR-007. This is fundamentally different from CLI `wraith connect`:
|
||||
|
||||
- **NAPI `connect()`**: Opens a single SSH channel and returns it as a Node.js `Duplex` stream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly.
|
||||
- **CLI `wraith connect`**: Full SSH client session with SOCKS5 server and port forwarding.
|
||||
|
||||
The function accepts `WraithConnectOptions` and returns `Promise<Duplex>`. The NAPI layer handles transport selection, SSH authentication, and channel setup, then hands the caller a stream.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `#[napi]` function `connect(options: WraithConnectOptions) -> Result<DuplexStream>` in `crates/wraith-napi/src/connect.rs`
|
||||
- [ ] `WraithConnectOptions` struct with napi fields: `server`, `peer`, `transport`, `identity`, `tlsServerName`, `insecure`, `irohRelay`, `proxy`
|
||||
- [ ] Transport creation from options (tcp, tls, iroh) — same logic as CLI but programmatic
|
||||
- [ ] SSH client connection: create transport stream, authenticate, open single `direct_tcpip` channel
|
||||
- [ ] Channel returned as `napi::DuplexStream` for JavaScript consumption
|
||||
- [ ] Key material: `identity` field accepts file path (string) or `Buffer` (in-memory data) per ADR-011
|
||||
- [ ] Error marshalling: Rust errors become JavaScript exceptions with descriptive messages
|
||||
- [ ] TypeScript type: `(options: WraithConnectOptions) => Promise<Duplex>`
|
||||
- [ ] Integration test from JS: connect to a test server, write/receive bytes through stream
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI connect() spec, TypeScript interfaces
|
||||
- docs/architecture/decisions/007-napi-single-stream.md — single duplex stream rationale
|
||||
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
45
tasks/napi/project-setup.md
Normal file
45
tasks/napi/project-setup.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: napi/project-setup
|
||||
name: Set up wraith-napi project with napi-rs build tooling and TypeScript types
|
||||
status: pending
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Set up the napi-rs project for the `@alkdev/wraith` Node.js native addon. This includes the napi-rs build configuration, TypeScript type definitions, and the package structure.
|
||||
|
||||
Per ADR-015 and ADR-016: napi-rs is the FFI bridge, and the wrapper exposes `connect()` and `serve()` functions. The NAPI layer is transport-agnostic — it doesn't know about pubsub's `EventEnvelope`.
|
||||
|
||||
The Cargo.toml skeleton was created in setup/project-init. This task configures the actual napi-rs build pipeline, TypeScript types, and verifies the build works.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-napi/` has `Cargo.toml` with `crate-type = ["cdylib"]`, `napi` and `napi-derive` dependencies
|
||||
- [ ] `crates/wraith-napi/src/lib.rs` with napi module registration
|
||||
- [ ] `packages/wraith-napi/` directory (or similar) with `package.json` named `@alkdev/wraith`
|
||||
- [ ] `packages/wraith-napi/tsconfig.json` for TypeScript type generation
|
||||
- [ ] TypeScript type definitions for `WraithConnectOptions`, `WraithServeOptions`, `WraithServer`, `ConnectionInfo` matching napi-and-pubsub.md interfaces
|
||||
- [ ] `napi.config.js` or `NapiRs.config` with correct cargo path, module name
|
||||
- [ ] Build command: `npm run build` builds the native addon
|
||||
- [ ] Feature flags: `iroh` feature optional; base package includes tcp + tls
|
||||
- [ ] `npm install` and initial build succeed
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI Wrapper section, TypeScript interfaces
|
||||
- docs/architecture/decisions/015-napi-rs-for-ffi-bridge.md — napi-rs choice
|
||||
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
45
tasks/napi/serve-function.md
Normal file
45
tasks/napi/serve-function.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: napi/serve-function
|
||||
name: Implement NAPI serve() — server with connection events returning Duplex streams
|
||||
status: pending
|
||||
depends_on:
|
||||
- napi/project-setup
|
||||
- server/serve-loop
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the NAPI `serve()` function per ADR-016. Returns a `WraithServer` object with a `close()` method and `onConnection` event emitter. Each incoming SSH connection produces a `Duplex` stream.
|
||||
|
||||
The function accepts `WraithServeOptions` and returns `Promise<WraithServer>`. The NAPI layer handles transport binding, SSH server setup, and connection handling.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `#[napi]` function `serve(options: WraithServeOptions) -> Result<WraithServer>` in `crates/wraith-napi/src/serve.rs`
|
||||
- [ ] `WraithServeOptions` struct with napi fields: `transport`, `hostKey`, `authorizedKeys`, `certAuthority`, `tlsCert`, `tlsKey`, `acmeDomain`, `listen`, `irohRelay`
|
||||
- [ ] `WraithServer` napi class with `close() -> Promise<void>` and `onConnection(callback)` event registration
|
||||
- [ ] Each incoming connection produces a `Duplex` stream via the `onConnection` callback
|
||||
- [ ] `ConnectionInfo` struct passed with each connection: `remoteAddr`, `transportKind`
|
||||
- [ ] Key material: `hostKey`, `authorizedKeys` accept file path (string) or `Buffer` (in-memory)
|
||||
- [ ] Server starts transport acceptor, authenticates connections, emits stream events
|
||||
- [ ] `close()` triggers graceful shutdown
|
||||
- [ ] TypeScript type matches napi-and-pubsub.md spec
|
||||
- [ ] Integration test: JS serve() + connect() round-trip works
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI serve() spec, WraithServer interface
|
||||
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
|
||||
- docs/architecture/server.md — server configuration
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
45
tasks/review/complete-system.md
Normal file
45
tasks/review/complete-system.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: review/complete-system
|
||||
name: Review complete system — CLI, NAPI, end-to-end integration
|
||||
status: pending
|
||||
depends_on:
|
||||
- meta/cli-layer
|
||||
- meta/napi-layer
|
||||
- review/server-and-client
|
||||
scope: system
|
||||
risk: low
|
||||
impact: project
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Final review of the complete wraith system. Verify CLI binary works end-to-end, NAPI wrapper provides correct JavaScript API, and both layers properly wrap the core library.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `wraith serve` + `wraith connect` end-to-end: SSH tunnel established, SOCKS5 proxy routes traffic
|
||||
- [ ] All CLI flags work: transport modes (tcp, tls, iroh), auth options, proxy, stealth, rate limits
|
||||
- [ ] Environment variables (`WRAITH_SERVER`, `WRAITH_IDENTITY`) work as defaults
|
||||
- [ ] `--stealth` validates `--transport tls` requirement
|
||||
- [ ] NAPI `connect()` returns Duplex stream; data flows bidirectionally
|
||||
- [ ] NAPI `serve()` accepts connections; `onConnection` emits Duplex streams
|
||||
- [ ] NAPI key material from Buffer works (not just file paths)
|
||||
- [ ] Feature flags: `tls`, `iroh`, `acme` correctly gate optional functionality
|
||||
- [ ] Base build (`cargo build -p wraith-core` with no features) compiles and works
|
||||
- [ ] All tests pass: `cargo test --workspace`
|
||||
- [ ] NAPI tests pass: `cd crates/wraith-napi && npm test`
|
||||
- [ ] `cargo clippy --workspace` passes
|
||||
- [ ] No logging of tunnel destinations anywhere in the system
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md, docs/architecture/napi-and-pubsub.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
45
tasks/review/core-foundation.md
Normal file
45
tasks/review/core-foundation.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: review/core-foundation
|
||||
name: Review core foundation — transport traits, auth, error types, key loading
|
||||
status: pending
|
||||
depends_on:
|
||||
- meta/transport-layer
|
||||
- meta/auth-layer
|
||||
- setup/test-infrastructure
|
||||
scope: broad
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the core foundation layer before proceeding to server/client implementation. Verify that transport abstractions match architecture, auth logic is correct, errors follow the layered pattern, and key loading handles all spec'd formats.
|
||||
|
||||
This is the critical review before building the higher-level server and client components on top of these foundations.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Transport trait matches transport.md: correct bounds, object-safety, describe() method
|
||||
- [ ] TransportAcceptor matches transport.md: returns TransportInfo with correct metadata
|
||||
- [ ] TCP, TLS, iroh transports all produce correct stream types per implementations table
|
||||
- [ ] ACME integration with TLS works (or feature gates correctly prevent compilation without it)
|
||||
- [ ] Key loading handles file paths and in-memory data, rejects PEM format
|
||||
- [ ] authorized_keys parsing handles cert-authority entries with options
|
||||
- [ ] Server auth: Ed25519 key matching (constant-time), cert-authority validation (signature, expiry, principal)
|
||||
- [ ] Client auth: key pair presentation, Handler implementation
|
||||
- [ ] Error types cover all four layers (transport, auth, channel, config)
|
||||
- [ ] All tests pass: `cargo test --workspace`
|
||||
- [ ] `cargo clippy --workspace` passes with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md, docs/architecture/client.md, docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
46
tasks/review/server-and-client.md
Normal file
46
tasks/review/server-and-client.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: review/server-and-client
|
||||
name: Review server and client implementation — full SSH tunnel functionality
|
||||
status: pending
|
||||
depends_on:
|
||||
- meta/server-layer
|
||||
- meta/client-layer
|
||||
- review/core-foundation
|
||||
scope: broad
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the server and client implementations after the core foundation review. This is a critical checkpoint before the CLI and NAPI layers — the server and client must work correctly as a unit before wrapping them in CLI flags or NAPI bindings.
|
||||
|
||||
Verify end-to-end SSH tunnel flow: client connects → SOCKS5 proxy works → port forwards work → reconnection works → server handles channels → proxy modes work → stealth mode works.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Server accepts SSH connections over TCP, TLS, iroh (via integration tests)
|
||||
- [ ] Client establishes SSH sessions and runs SOCKS5 proxy
|
||||
- [ ] Channel proxy: direct TCP, SOCKS5 proxy, HTTP CONNECT proxy all work
|
||||
- [ ] Stealth mode: non-SSH gets nginx 404, SSH connects normally
|
||||
- [ ] Rate limiting: connection limits enforced, auth attempt limits enforced
|
||||
- [ ] Logging: structured `tracing::info!` events match ADR-013 format
|
||||
- [ ] No logging of tunnel destinations (ADR-006)
|
||||
- [ ] Reconnection: transport failure → exponential backoff → reconnect → port forwards re-registered
|
||||
- [ ] Reserved `wraith-` destinations routed to control channel, not TCP proxy
|
||||
- [ ] Graceful shutdown works for both server and client
|
||||
- [ ] All tests pass: `cargo test --workspace`
|
||||
- [ ] `cargo clippy --workspace` passes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md, docs/architecture/client.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
49
tasks/server/channel-proxy.md
Normal file
49
tasks/server/channel-proxy.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: server/channel-proxy
|
||||
name: Implement server channel proxy — direct TCP and outbound proxy connections
|
||||
status: pending
|
||||
depends_on:
|
||||
- server/handler
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the server's channel proxy logic that makes outbound TCP connections on behalf of SSH clients. When `channel_open_direct_tcpip(host, port)` is called for a non-reserved destination:
|
||||
|
||||
1. Connect to `host:port`, either directly or via the configured outbound proxy
|
||||
2. Run `tokio::io::copy_bidirectional` between the SSH channel stream and the outbound TCP stream
|
||||
3. Clean up when either side disconnects
|
||||
|
||||
Supports three outbound proxy modes per server.md: Direct, SOCKS5 proxy, HTTP CONNECT proxy.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/server/channel_proxy.rs` exports channel proxy functions
|
||||
- [ ] `ProxyConfig` enum: `Direct`, `Socks5 { addr: SocketAddr }`, `HttpConnect { addr: SocketAddr }`
|
||||
- [ ] `connect_outbound(target: SocketAddr, proxy: &ProxyConfig) -> Result<TcpStream>` — connects to target directly or via proxy
|
||||
- [ ] Direct mode: `TcpStream::connect(target)`
|
||||
- [ ] SOCKS5 proxy: establishes SOCKS5 handshake, sends CONNECT command for target
|
||||
- [ ] HTTP CONNECT proxy: sends `CONNECT host:port HTTP/1.1` to proxy, reads 200 response
|
||||
- [ ] `proxy_channel(channel: ChannelStream, target: SocketAddr, proxy: &ProxyConfig)` — spawns bidirectional copy task
|
||||
- [ ] Channel errors (target unreachable, proxy failure) close that channel without affecting SSH session
|
||||
- [ ] No logging of tunnel destinations (ADR-006) — only transport/auth events are logged
|
||||
- [ ] Unit tests: direct connection proxy, SOCKS5 proxy handshake, HTTP CONNECT proxy handshake, target unreachable handling
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Channel Handling, Outbound Proxy Modes sections
|
||||
- docs/architecture/decisions/006-no-logging-of-tunnel-destinations.md — no destination logging
|
||||
- docs/architecture/decisions/019-proxy-dual-semantics.md — server `--proxy` meaning
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
50
tasks/server/control-channel.md
Normal file
50
tasks/server/control-channel.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
id: server/control-channel
|
||||
name: Implement wraith-control reserved channel for pubsub event bus bridging (ADR-018)
|
||||
status: pending
|
||||
depends_on:
|
||||
- server/handler
|
||||
- auth/error-types
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the control channel routing per ADR-018. When the server receives a `channel_open_direct_tcpip` request for `wraith-control:0`:
|
||||
|
||||
1. The handler detects the reserved `wraith-` prefix destination
|
||||
2. Instead of making a TCP connection, it bridges the SSH channel to an internal event bus handle
|
||||
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
|
||||
|
||||
The entire `wraith-` prefix is reserved — no TCP connections should be attempted for `wraith-*` destinations. The control channel is optional; servers without pubsub configured should accept the channel and provide a configurable behavior (reject or provide a loopback pipe).
|
||||
|
||||
At this stage, implement the routing logic and a `ControlChannel` trait that consumers can implement. The actual pubsub bridge implementation would be in a separate crate or behind a feature flag.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/server/control_channel.rs` exports `ControlChannelHandler` trait and routing logic
|
||||
- [ ] `WRAITH_CONTROL_DESTINATION` constant defined as `"wraith-control"` (ADR-018)
|
||||
- [ ] `WRAITH_PREFIX` constant defined as `"wraith-"` for namespace reservation
|
||||
- [ ] `ControlChannelHandler` trait: `async fn handle_channel(stream: Box<dyn AsyncRead + AsyncWrite + Unpin + Send>)`
|
||||
- [ ] Server handler detects `wraith-*` prefix and routes to `ControlChannelHandler` instead of TCP proxy
|
||||
- [ ] If no `ControlChannelHandler` configured, reject the channel open request (SSH channel open failure)
|
||||
- [ ] Non-reserved destinations continue through normal TCP proxy path
|
||||
- [ ] Server constraint enforced: no TCP connections to `wraith-*` destinations
|
||||
- [ ] Unit tests: reserved destination detected, non-reserved passes through, prefix matching works
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Channel Handling section (reserved destinations), Constraints section
|
||||
- docs/architecture/decisions/018-control-channel-for-pubsub.md — control channel rationale
|
||||
- docs/architecture/napi-and-pubsub.md — server-side control channel behavior
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
46
tasks/server/handler.md
Normal file
46
tasks/server/handler.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: server/handler
|
||||
name: Implement ServerHandler — russh server handler with auth and channel dispatch
|
||||
status: pending
|
||||
depends_on:
|
||||
- auth/server-auth-handler
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the core `ServerHandler` that implements `russh::server::Handler`. This is the heart of the server. Per server.md, it has two primary responsibilities:
|
||||
|
||||
1. **`auth_publickey()`**: Delegated to `ServerAuthConfig` — checks key against authorized set or validates cert-authority
|
||||
2. **`channel_open_direct_tcpip()`**: Routes the channel — either to a TCP target (directly or via proxy) or internally for reserved `wraith-*` destinations (ADR-018)
|
||||
|
||||
At this stage, implement the handler struct, auth delegation, and the channel dispatch skeleton (actual TCP connection and proxy logic in dependent tasks).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/server/handler.rs` exports `ServerHandler`
|
||||
- [ ] `ServerHandler` implements `russh::server::Handler`
|
||||
- [ ] `ServerHandler` holds: `Arc<ServerAuthConfig>`, `outbound_proxy: Option<ProxyConfig>`, `remote_addr: Option<SocketAddr>`
|
||||
- [ ] `auth_publickey()` delegates to `ServerAuthConfig` and returns `Accept` or `Reject`
|
||||
- [ ] `channel_open_direct_tcpip()` dispatches: if `host.starts_with("wraith-")`, route to internal handler (stub for control channel); otherwise, spawn TCP proxy task (stub that logs and returns error for now)
|
||||
- [ ] One `ServerHandler` instance per connection; state is not shared between connections (unless explicitly Arc'd)
|
||||
- [ ] Structured auth logging via `tracing::info!` with `remote_addr`, `key_fingerprint`, `result` (ADR-013)
|
||||
- [ ] Unit tests: auth delegation works, reserved destination routing logic, unknown channel types rejected
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Server Handler Behavior section, channel handling
|
||||
- docs/architecture/decisions/018-control-channel-for-pubsub.md — reserved `wraith-*` destinations
|
||||
- docs/architecture/decisions/013-fail2ban-friendly-logging.md — structured auth logging
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
50
tasks/server/rate-limiting-and-logging.md
Normal file
50
tasks/server/rate-limiting-and-logging.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
id: server/rate-limiting-and-logging
|
||||
name: Implement server rate limiting and fail2ban-friendly structured logging
|
||||
status: pending
|
||||
depends_on:
|
||||
- server/handler
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the two-layer abuse protection per ADR-013:
|
||||
|
||||
1. **Structured logging** at INFO level for fail2ban integration: auth attempts (remote_addr, user, key_fingerprint, accept/reject), connection opened/closed (remote_addr, transport, duration)
|
||||
2. **Built-in rate limiting**: `--max-connections-per-ip` (reject new connections from IPs with N active connections), `--max-auth-attempts` (disconnect after N failed auth attempts per connection)
|
||||
|
||||
No logging of tunnel destinations, DNS resolutions, or bytes transferred (ADR-006).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/server/rate_limit.rs` exports connection rate limiter
|
||||
- [ ] `ConnectionRateLimiter` tracks active connections per IP using `HashMap<IpAddr, usize>`
|
||||
- [ ] `ConnectionRateLimiter::check(ip) -> bool` — returns `true` if connection allowed, `false` if over limit
|
||||
- [ ] `ConnectionRateLimiter::on_connect(ip)` — increment counter
|
||||
- [ ] `ConnectionRateLimiter::on_disconnect(ip)` — decrement counter
|
||||
- [ ] `AuthAttemptLimiter` tracks failed auth attempts per connection
|
||||
- [ ] `AuthAttemptLimiter::check() -> bool` — returns `true` if under limit
|
||||
- [ ] `AuthAttemptLimiter::on_failure()` — increment failure counter
|
||||
- [ ] Structured `tracing::info!` logging on: auth attempt, connection opened, connection closed
|
||||
- [ ] Log format includes key-value pairs: `remote_addr`, `user`, `key_fingerprint`, `result`, `transport`, `duration`
|
||||
- [ ] No logging of: channel open targets, DNS resolutions, bytes transferred
|
||||
- [ ] Integration with `ServerHandler`: rate limiter checked before auth, auth attempt limiter checked during auth
|
||||
- [ ] Unit tests: connection limit enforced, auth attempt limit enforced, log format verification
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Logging and Rate Limiting section
|
||||
- docs/architecture/decisions/013-fail2ban-friendly-logging.md — logging format, rate limiting flags
|
||||
- docs/architecture/decisions/006-no-logging-of-tunnel-destinations.md — no destination logging
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
54
tasks/server/serve-loop.md
Normal file
54
tasks/server/serve-loop.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: server/serve-loop
|
||||
name: Implement server accept loop, graceful shutdown, and ServeOptions config
|
||||
status: pending
|
||||
depends_on:
|
||||
- server/handler
|
||||
- server/channel-proxy
|
||||
- server/rate-limiting-and-logging
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the server's main accept loop and configuration. This ties together the transport acceptor, server handler, rate limiting, and logging into a coherent server process.
|
||||
|
||||
`ServeOptions` is the programmatic configuration struct (ADR-011) for the server. The accept loop:
|
||||
1. Binds a `TransportAcceptor` based on transport mode
|
||||
2. Accepts incoming connections (respecting rate limits)
|
||||
3. Creates a `ServerHandler` per connection
|
||||
4. Passes the stream to `russh::server::run_stream()`
|
||||
5. Handles graceful shutdown on SIGTERM/SIGINT
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/server/mod.rs` re-exports all server components
|
||||
- [ ] `ServeOptions` struct with fields matching server.md CLI interface: `key`, `authorized_keys`, `cert_authority`, `transport_mode`, `listen_addr`, `tls_cert`, `tls_key`, `acme_domain`, `stealth`, `proxy`, `iroh_relay`, `max_connections_per_ip`, `max_auth_attempts`
|
||||
- [ ] `Server::new(opts: ServeOptions) -> Result<Server>` — creates server with bound acceptor, auth config, rate limiter
|
||||
- [ ] `Server::run()` — enters accept loop, for each connection: check rate limit → create handler → `run_stream()`
|
||||
- [ ] Stealth mode integration: if enabled, protocol detection before `run_stream()`
|
||||
- [ ] Graceful shutdown: `Server::shutdown()` method and signal handler (SIGTERM/SIGINT)
|
||||
- Stop accepting new connections
|
||||
- Send SSH disconnect to active sessions
|
||||
- Wait for drain timeout (~2 seconds per session)
|
||||
- Forcibly terminate remaining connections
|
||||
- [ ] iroh mode: prints endpoint ID on startup
|
||||
- [ ] `ServeOptions::key` and `ServeOptions::authorized_keys` accept `KeySource` (file or in-memory)
|
||||
- [ ] Integration test: start server, client connects via mock transport, session works, shutdown completes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — full server spec including graceful shutdown
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — ServeOptions programmatic struct
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
50
tasks/server/stealth-mode.md
Normal file
50
tasks/server/stealth-mode.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
id: server/stealth-mode
|
||||
name: Implement stealth mode — protocol multiplexing on port 443 (ADR-017)
|
||||
status: pending
|
||||
depends_on:
|
||||
- transport/tls-transport
|
||||
- server/handler
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement stealth mode per ADR-017. When `--stealth` is enabled alongside TLS transport on port 443:
|
||||
|
||||
1. After completing the TLS handshake, peek at the first bytes of the connection
|
||||
2. If the connection starts with `SSH-2.0-`, proceed with `russh::server::run_stream()`
|
||||
3. If the connection starts with anything else (HTTP, random data), respond with `HTTP/1.1 404 Not Found\r\nServer: nginx\r\n\r\n` and close
|
||||
|
||||
This makes the server appear as an nginx web server returning 404 errors to non-SSH connections, making it indistinguishable from a regular HTTPS site to port scanners and DPI systems.
|
||||
|
||||
Stealth mode requires TLS transport. The CLI should reject or warn if `--stealth` is used without `--transport tls`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/server/stealth.rs` exports stealth mode protocol detection
|
||||
- [ ] `detect_protocol(stream: TlsStream) -> ProtocolDetection` — peeks at first bytes to determine SSH vs HTTP
|
||||
- [ ] `ProtocolDetection` enum: `Ssh`, `Http` (or `Unknown`)
|
||||
- [ ] If SSH detected: pass stream to `russh::server::run_stream()`
|
||||
- [ ] If HTTP/unknown detected: write `HTTP/1.1 404 Not Found\r\nServer: nginx\r\n\r\n` then close
|
||||
- [ ] Peek uses `tokio::io::BufReader` or similar buffered read to avoid consuming the SSH banner bytes
|
||||
- [ ] Integration with `TlsAcceptor` flow: after accept + TLS handshake, optionally run protocol detection before passing to russh
|
||||
- [ ] Stealth mode flag validated: requires TLS transport, warn/reject otherwise
|
||||
- [ ] Unit tests: SSH banner detection, HTTP request detection, random data → fake nginx 404
|
||||
- [ ] Integration test: stealth server responds to HTTP scanner with 404, SSH client connects successfully
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Stealth Mode section
|
||||
- docs/architecture/decisions/017-stealth-mode-protocol-multiplexing.md — protocol multiplexing design
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
47
tasks/setup/project-init.md
Normal file
47
tasks/setup/project-init.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: setup/project-init
|
||||
name: Initialize Cargo workspace with wraith, wraith-core, and wraith-napi crates
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Set up the Rust workspace from scratch. The repo currently has only `docs/` and `.git/`. Initialize a Cargo workspace with three crate directories following the architecture spec:
|
||||
|
||||
- **`wraith-core`** — library crate with feature flags (`tls`, `iroh`, `acme`). All core logic lives here.
|
||||
- **`wraith`** — binary crate depending on `wraith-core`. CLI entry point.
|
||||
- **`wraith-napi`** — napi-rs crate for the Node.js native addon (skeleton only at this stage).
|
||||
|
||||
Per overview.md: `russh`, `tokio`, `clap`, `tracing`, `anyhow`/`thiserror` are core dependencies. `tokio-rustls`, `rustls`, `rustls-acme`, `iroh` are feature-gated.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Cargo.toml` workspace root with `[workspace]` members: `crates/wraith-core`, `crates/wraith`, `crates/wraith-napi`
|
||||
- [ ] `crates/wraith-core/Cargo.toml` with library crate, feature flags: `tls` (tokio-rustls + rustls), `iroh` (iroh), `acme` (rustls-acme, implies `tls`)
|
||||
- [ ] Core dependencies listed: `russh`, `tokio` (full), `tracing`, `anyhow`, `thiserror`, `tokio-util`
|
||||
- [ ] `crates/wraith/Cargo.toml` with binary crate, depends on `wraith-core` with default features, `clap` with `derive` feature
|
||||
- [ ] `crates/wraith-napi/Cargo.toml` with `cdylib` crate type, depends on `wraith-core`, `napi` and `napi-derive`
|
||||
- [ ] `crates/wraith-core/src/lib.rs` with module skeleton: `pub mod transport; pub mod client; pub mod server; pub mod auth; pub mod socks5; pub mod error;`
|
||||
- [ ] `crates/wraith/src/main.rs` with minimal `fn main()` skeleton
|
||||
- [ ] `crates/wraith-napi/src/lib.rs` with `#[macro_use] extern crate napi_derive;` and empty skeleton
|
||||
- [ ] `.gitignore` covers `target/`, `node_modules/`
|
||||
- [ ] `cargo check` succeeds for all workspace members
|
||||
- [ ] Feature flags resolve correctly: `cargo check -p wraith-core --features tls`, `--features iroh`, `--features acme`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md — package structure, dependencies, feature flags
|
||||
- docs/architecture/napi-and-pubsub.md — wraith-napi crate purpose
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
39
tasks/setup/test-infrastructure.md
Normal file
39
tasks/setup/test-infrastructure.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
id: setup/test-infrastructure
|
||||
name: Set up test infrastructure with tokio test helpers and integration test skeleton
|
||||
status: pending
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: trivial
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Set up test infrastructure so that subsequent tasks can write tests as they implement. Add test helpers for creating in-memory transport streams (mock transport), and skeleton integration test files for each component.
|
||||
|
||||
The mock transport is critical — it lets us test SSH client/server flows without actual network I/O, per ADR-001's consequence that "mock transports can produce in-memory streams."
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/tests/` directory with empty integration test skeletons: `transport_tests.rs`, `client_tests.rs`, `server_tests.rs`, `auth_tests.rs`
|
||||
- [ ] `crates/wraith-core/src/testutil.rs` module (behind `#[cfg(test)]` or a `testutil` feature) exporting `MockTransport` and `MockStream`
|
||||
- [ ] `MockStream` wraps `tokio::io::DuplexStream` implementing `AsyncRead + AsyncWrite + Unpin + Send`
|
||||
- [ ] `MockTransport` implements `Transport` trait (once defined) returning `MockStream` via `connect()`
|
||||
- [ ] `MockTransportAcceptor` implements `TransportAcceptor` (once defined) returning paired `MockStream` via `accept()`
|
||||
- [ ] `cargo test` succeeds (even if no real tests yet)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — Transport trait contract
|
||||
- docs/architecture/decisions/001-pluggable-transport.md — "mock transports can produce in-memory streams"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
50
tasks/transport/acme-cert-provisioning.md
Normal file
50
tasks/transport/acme-cert-provisioning.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
id: transport/acme-cert-provisioning
|
||||
name: Implement ACME Lets Encrypt certificate provisioning (feature-gated acme)
|
||||
status: pending
|
||||
depends_on:
|
||||
- transport/tls-transport
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement automatic TLS certificate provisioning via ACME (Let's Encrypt). Two modes per ADR-008:
|
||||
|
||||
1. **Domain-based ACME** (`--acme-domain`): Standard flow with HTTP-01 or TLS-ALPN-01 challenges. Domain-bound, auto-renewing.
|
||||
2. **IP-based ACME**: Short-lived certs via TLS-ALPN-01 on port 443. No domain needed.
|
||||
|
||||
Uses `rustls-acme` (pure Rust) to avoid external certbot dependency. Feature-gated behind `acme` (implies `tls`).
|
||||
|
||||
This integrates with `TlsAcceptor` by providing ACME-resolved certificates instead of manual cert/key files.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/transport/acme.rs` (behind `#[cfg(feature = "acme")]`)
|
||||
- [ ] Feature `acme` implies `tls` in Cargo.toml
|
||||
- [ ] `AcmeCertProvider` struct accepts: domain (domain-based) or IP mode flag
|
||||
- [ ] Domain-based mode: uses `rustls-acme` with HTTP-01/TLS-ALPN-01 challenge responder
|
||||
- [ ] IP-based mode: uses `rustls-acme` with TLS-ALPN-01 on port 443
|
||||
- [ ] `AcmeCertProvider` produces a `rustls::ServerConfig` that `TlsAcceptor` can use
|
||||
- [ ] Certificate auto-renewal handled by `rustls-acme` background task
|
||||
- [ ] `TlsAcceptor` updated to accept either manual certs OR an `AcmeCertProvider`
|
||||
- [ ] Integration with `TlsAcceptor::bind_acme()` or similar constructor
|
||||
- [ ] Unit tests for ACME config construction (challenge responder setup)
|
||||
- [ ] Integration test: ACME cert provisioning with Let's Encrypt staging (marked `#[ignore]` for CI)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — TLS certificate provisioning modes
|
||||
- docs/architecture/decisions/008-acme-lets-encrypt.md — ACME design, rustls-acme choice
|
||||
- docs/architecture/transport.md — feature flags, TLS transport constraints
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
56
tasks/transport/iroh-transport.md
Normal file
56
tasks/transport/iroh-transport.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: transport/iroh-transport
|
||||
name: Implement IrohTransport and IrohAcceptor (feature-gated iroh)
|
||||
status: pending
|
||||
depends_on:
|
||||
- transport/trait-and-types
|
||||
- transport/tcp-transport
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement iroh QUIC P2P transport. Per ADR-003, use `tokio::io::join(recv_stream, send_stream)` to combine iroh's split QUIC streams into a single duplex stream that russh can consume.
|
||||
|
||||
Client-side: `IrohTransport` connects to a remote iroh endpoint, opens a bidirectional QUIC stream, and joins the halves.
|
||||
Server-side: `IrohAcceptor` creates an iroh endpoint, accepts incoming connections, accepts bidirectional streams.
|
||||
|
||||
iroh supports proxy configuration natively via `Endpoint::builder()`, which is how `--proxy` works with iroh transport (ADR-010).
|
||||
|
||||
Feature-gated behind `iroh` feature flag.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/transport/iroh.rs` (behind `#[cfg(feature = "iroh")]`)
|
||||
- [ ] `IrohTransport` holds: target endpoint ID (base58-decoded to `NodeId`), relay URL, optional proxy URL
|
||||
- [ ] `IrohTransport::connect()` calls `endpoint.connect(node_id, alpn)`, then `conn.open_bi()`, then `tokio::io::join(recv, send)`
|
||||
- [ ] ALPN value is `b"wraith-ssh"`
|
||||
- [ ] `IrohTransport::describe()` returns e.g. `"iroh://<endpoint-id>"`
|
||||
- [ ] `IrohAcceptor` holds an `iroh::Endpoint` instance
|
||||
- [ ] `IrohAcceptor::bind()` creates endpoint with relay URL and optional proxy config
|
||||
- [ ] `IrohAcceptor::accept()` calls `endpoint.accept()`, then `conn.accept_bi()`, then `tokio::io::join(recv, send)`
|
||||
- [ ] `IrohAcceptor` exposes `endpoint_id()` returning base58-encoded node ID for CLI display
|
||||
- [ ] Default relay is n0's `https://relay.iroh.network/` (ADR-009)
|
||||
- [ ] Proxy URL passed to `Endpoint::builder()` for outbound proxy (ADR-010)
|
||||
- [ ] `TransportInfo.transport_kind` is `TransportKind::Iroh { endpoint_id }`
|
||||
- [ ] Module re-exported from `transport/mod.rs` behind `#[cfg(feature = "iroh")]`
|
||||
- [ ] Unit tests: endpoint creation, stream join produces correct type
|
||||
- [ ] Integration test: iroh client connects to iroh server, stream is duplex (may need iroh relay, mark `#[ignore]` for CI)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — IrohTransport row, iroh stream join, relay config
|
||||
- docs/architecture/decisions/003-iroh-stream-join.md — tokio::io::join rationale
|
||||
- docs/architecture/decisions/009-default-iroh-relay.md — default relay
|
||||
- docs/architecture/decisions/010-transport-chaining-cli.md — proxy configuration
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
41
tasks/transport/tcp-transport.md
Normal file
41
tasks/transport/tcp-transport.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
id: transport/tcp-transport
|
||||
name: Implement TcpTransport and TcpAcceptor
|
||||
status: pending
|
||||
depends_on:
|
||||
- transport/trait-and-types
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the simplest transport: plain TCP. `TcpTransport` connects via `TcpStream::connect(addr)` on the client side. `TcpAcceptor` accepts via `TcpListener::accept()` on the server side. This is the baseline transport that all others build upon conceptually.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/transport/tcp.rs` exports `TcpTransport` and `TcpAcceptor`
|
||||
- [ ] `TcpTransport` holds a `SocketAddr` target address
|
||||
- [ ] `TcpTransport::connect()` calls `TcpStream::connect(addr)` and returns the stream
|
||||
- [ ] `TcpTransport::describe()` returns e.g. `"tcp://1.2.3.4:22"`
|
||||
- [ ] `TcpAcceptor` holds a `TcpListener` and accept address
|
||||
- [ ] `TcpAcceptor::accept()` calls `listener.accept()`, returns `(stream, TransportInfo)` with `remote_addr` set and `TransportKind::Tcp`
|
||||
- [ ] `TcpAcceptor` constructor binds the listener: `TcpAcceptor::bind(addr)` async factory
|
||||
- [ ] Connection timeout handling (tokio default connect timeout is reasonable; document behavior)
|
||||
- [ ] Unit tests: connect creates a stream, accept receives a connection, describe format
|
||||
- [ ] Integration test: client connects to server via TCP, stream is duplex
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — TcpTransport row in implementations table
|
||||
- docs/architecture/overview.md — "TCP on port 22 for basic use"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
54
tasks/transport/tls-transport.md
Normal file
54
tasks/transport/tls-transport.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: transport/tls-transport
|
||||
name: Implement TlsTransport and TlsAcceptor (feature-gated tls)
|
||||
status: pending
|
||||
depends_on:
|
||||
- transport/tcp-transport
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement TLS transport that wraps TCP with `tokio-rustls`. Client-side: `TlsTransport` establishes a TCP connection and wraps it with a TLS client session. Server-side: `TlsAcceptor` accepts TCP connections and wraps them with a TLS server session.
|
||||
|
||||
Supports:
|
||||
- Manual cert/key configuration (`--tls-cert`, `--tls-key`)
|
||||
- insecure mode (accept self-signed certs) for development
|
||||
- `tls_server_name` override for SNI (ADR-010)
|
||||
- Stealth mode support requires peeking at first bytes post-TLS-handshake (handled in server task, but TLS stream must support this)
|
||||
|
||||
Feature-gated behind `tls` feature flag.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/transport/tls.rs` (behind `#[cfg(feature = "tls")]`)
|
||||
- [ ] `TlsTransport` holds: target addr, optional `tls_server_name`, `insecure` flag, optional root cert for verification
|
||||
- [ ] `TlsTransport::connect()` does TCP connect then TLS client handshake via `tokio_rustls::TlsConnector`
|
||||
- [ ] When `insecure`, accepts any certificate (dangerous, `webpki_roots::CertStore` bypass or custom verifier)
|
||||
- [ ] When not `insecure`, verifies server cert against system roots + optional custom CA
|
||||
- [ ] `TlsTransport::describe()` returns e.g. `"tls://example.com:443"`
|
||||
- [ ] `TlsAcceptor` holds: `TcpListener`, `ServerConfig` (from `rustls::ServerConfig`)
|
||||
- [ ] `TlsAcceptor::accept()` does TCP accept then TLS server handshake via `tokio_rustls::TlsAcceptor`
|
||||
- [ ] `TlsAcceptor` constructor accepts: `tls_cert` path/data, `tls_key` path/data, optional ACME config (stub for now)
|
||||
- [ ] `TransportInfo.transport_kind` is `TransportKind::Tls { server_name }`
|
||||
- [ ] Module re-exported from `transport/mod.rs` behind `#[cfg(feature = "tls")]`
|
||||
- [ ] Unit tests for connect/accept with self-signed certs (insecure mode)
|
||||
- [ ] Integration test: full TLS client-to-server connection succeeds
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — TlsTransport row, TLS cert provisioning
|
||||
- docs/architecture/server.md — TLS certificate provisioning modes
|
||||
- docs/architecture/decisions/008-acme-lets-encrypt.md — ACME cert support (feature-gated)
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
44
tasks/transport/trait-and-types.md
Normal file
44
tasks/transport/trait-and-types.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: transport/trait-and-types
|
||||
name: Define Transport trait, TransportAcceptor trait, TransportInfo, and TransportKind types
|
||||
status: pending
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define the core transport abstraction types that everything else builds on. This is the foundation per ADR-001: a `Transport` trait that produces `AsyncRead + AsyncWrite + Unpin + Send` streams, and a `TransportAcceptor` trait for the server side.
|
||||
|
||||
The `TransportInfo` and `TransportKind` types carry metadata about incoming connections (remote address, transport kind) which the server handler needs for logging and auth decisions.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/wraith-core/src/transport/mod.rs` exports `Transport` trait, `TransportAcceptor` trait, `TransportInfo`, `TransportKind`
|
||||
- [ ] `Transport` trait: `async fn connect(&self) -> Result<Self::Stream>` where `Self::Stream: AsyncRead + AsyncWrite + Unpin + Send + 'static`
|
||||
- [ ] `Transport::describe(&self) -> String` for human-readable logging
|
||||
- [ ] `TransportAcceptor` trait: `async fn accept(&self) -> Result<(Self::Stream, TransportInfo)>` with same stream bounds
|
||||
- [ ] `TransportInfo { remote_addr: Option<SocketAddr>, transport_kind: TransportKind }`
|
||||
- [ ] `TransportKind` enum: `Tcp`, `Tls { server_name: Option<String> }`, `Iroh { endpoint_id: String }`
|
||||
- [ ] Traits are `Send + Sync + 'static`
|
||||
- [ ] Re-exported from `crates/wraith-core/src/lib.rs`
|
||||
- [ ] Unit tests verifying trait objects can be constructed (trait is object-safe with `Box<dyn Transport<Stream = ...>>`)
|
||||
- [ ] Documentation comments on all public types referencing ADR-001, ADR-004
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — Transport trait, TransportAcceptor trait, TransportInfo, TransportKind definitions
|
||||
- docs/architecture/decisions/001-pluggable-transport.md — pluggable transport rationale
|
||||
- docs/architecture/decisions/004-ssh-over-transport.md — SSH runs over transport
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user