Review architecture specs, address critical/warning issues, mark reviewed

Address 5 critical and 7 warning issues from architecture review:
- Fix duplicate sentence in napi-and-pubsub.md server side section
- Add wraith- namespace reservation to server.md constraints (ADR-018)
- Document stealth mode TLS-only requirement in server.md
- Create ADR-019 for --proxy dual semantics (client vs server)
- Clarify NAPI connect() vs CLI wraith connect distinction
- Add SOCKS5h default as privacy design decision in client.md
- Expand reconnection section (always-on, re-register port forwards)
- Add graceful shutdown sections to client.md and server.md
- Specify OpenSSH key format for path-or-buffer inputs across all docs
- Resolve pubsub alternative approach ambiguity (ADR-018 is primary)
- Replace server.md handler impl block with behavioral description
- Standardize iroh endpoint ID terminology (base58-encoded)
- Remove iroh API implementation details from transport.md/server.md
- Add error handling pattern as cross-cutting concern in overview.md
- Update all document statuses from draft to reviewed
This commit is contained in:
2026-06-02 07:44:42 +00:00
parent 13b0991fb8
commit af8e7e8b44
8 changed files with 182 additions and 84 deletions

View File

@@ -1,24 +1,24 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# Wraith Architecture
## Current State
Pre-implementation. Feasibility assessment complete. Architecture specification drafted — all open questions resolved, pending review.
Architecture specification reviewed and ready for implementation. All open questions resolved. 19 ADRs accepted.
## Architecture Documents
| Document | Status | Description |
|----------|--------|-------------|
| [overview.md](overview.md) | draft | Package purpose, exports, dependencies |
| [transport.md](transport.md) | draft | Transport abstraction: TCP, TLS, iroh |
| [client.md](client.md) | draft | Client connection, SOCKS5, port forwarding |
| [server.md](server.md) | draft | Server acceptance, channel handling, proxy |
| [overview.md](overview.md) | reviewed | Package purpose, exports, dependencies |
| [transport.md](transport.md) | reviewed | Transport abstraction: TCP, TLS, iroh |
| [client.md](client.md) | reviewed | Client connection, SOCKS5, port forwarding |
| [server.md](server.md) | reviewed | Server acceptance, channel handling, proxy |
| [tun-shim.md](tun-shim.md) | deprecated | TUN interface wrapper — **deferred**, use tun2proxy |
| [napi-and-pubsub.md](napi-and-pubsub.md) | draft | NAPI wrapper and pubsub event target adapter |
| [napi-and-pubsub.md](napi-and-pubsub.md) | reviewed | NAPI wrapper and pubsub event target adapter |
## ADR Table
@@ -42,6 +42,7 @@ Pre-implementation. Feasibility assessment complete. Architecture specification
| [016](decisions/016-napi-expose-connect-and-serve.md) | NAPI exposes both connect() and serve() | Accepted |
| [017](decisions/017-stealth-mode-protocol-multiplexing.md) | Stealth mode — protocol multiplexing on port 443 | Accepted |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel for pubsub over SSH | Accepted |
| [019](decisions/019-proxy-dual-semantics.md) | `--proxy` dual semantics (client vs server) | Accepted |
## Open Questions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# Client
@@ -55,7 +55,7 @@ The primary client interface. Listens on a local port (default `127.0.0.1:1080`)
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.
Supports SOCKS5h (domain names resolved server-side) by default. This prevents DNS leaks — the client never resolves target hostnames locally, sending them to the server for resolution instead. This is consistent with the project's privacy design (ADR-006).
### Port Forwarding
@@ -92,7 +92,9 @@ On transport failure:
4. Re-authenticate SSH session
5. Notify SOCKS5 server and port forwards (in-flight connections fail, new connections work)
Existing TCP connections through the tunnel are lost on reconnect. This is acceptable — same as any VPN.
Reconnection is always enabled. The backoff caps at 30 seconds and continues indefinitely until the user terminates the process. Existing TCP connections through the tunnel are lost on reconnect — this is acceptable and consistent with how VPN connections behave.
The channel manager orchestrates reconnection: it creates a new transport stream (by calling `transport.connect()` again) and establishes a new SSH session over it (ADR-004). This is a full reconnect — there is no "SSH reconnects over the same transport." Port forward listeners (`-L`, `-R`) are re-registered with the new session after reconnection.
### Programmatic Configuration (ADR-011)
@@ -104,6 +106,21 @@ The client uses programmatic configuration — no `~/.ssh/config` parsing, no cu
This approach avoids cross-platform path issues (`~` expansion, Windows `USERPROFILE`) and makes the library API clean for programmatic consumers like the NAPI wrapper. Keys can be provided as file paths or in-memory data.
### Key Material Format
Key inputs (`--identity`, `--authorized-keys`, `--cert-authority`, `--key`) accept either:
- **File path**: A filesystem path to a key file (e.g., `~/.ssh/id_ed25519`, `/etc/wraith/ca.pub`)
- **In-memory data**: Raw key bytes provided programmatically via the library API or NAPI wrapper (as `Vec<u8>` in Rust, `Buffer` in Node.js)
The accepted format is **OpenSSH key format** (the format used by `ssh-keygen` and OpenSSH's `~/.ssh/` files). This includes:
- Private keys: OpenSSH format (begins with `-----BEGIN OPENSSH PRIVATE KEY-----`)
- Public keys: OpenSSH format (e.g., `ssh-ed25519 AAAA... user@host`)
- Certificate authority keys: OpenSSH public key format
- Authorized keys files: Standard OpenSSH `authorized_keys` format
PEM-encoded keys (PKCS#1, PKCS#8) are not supported. Use OpenSSH format keys throughout.
### CLI Interface
```bash
@@ -134,7 +151,7 @@ wraith connect --server example.com --forward 5432:db.internal:5432 --forward 63
# All options
wraith connect \
--server <addr> \ # TCP/TLS server address (required for tcp/tls)
--peer <endpoint-id> \ # iroh peer ID (required for iroh)
--peer <endpoint-id> \ # iroh endpoint ID, base58-encoded (required for iroh)
--transport tcp|tls|iroh \ # Transport mode
--identity <path-or-buffer> \ # SSH private key (path or in-memory)
--socks5 <addr:port> \ # SOCKS5 listen address (default: 127.0.0.1:1080)
@@ -154,6 +171,28 @@ wraith connect \
- Only one SSH session per `wraith connect` process. Multiple sessions = multiple processes (or a future multiplexer).
- No `~/.ssh/config` parsing. Configuration is programmatic via CLI flags, env vars, or library API structs (ADR-011).
- VPN-like "route all traffic" behavior is provided by running `tun2proxy --proxy socks5://127.0.0.1:1080` alongside the client, not by a built-in TUN interface (ADR-014).
- The CLI `wraith connect` command manages a full SSH session with SOCKS5 and port forwarding. The NAPI `connect()` function is a different operation — it opens a single SSH channel as a Duplex stream for programmatic use, with no SOCKS5 server or port forwarding. See napi-and-pubsub.md for details.
## Graceful Shutdown
On SIGTERM or SIGINT:
1. Stop accepting new SOCKS5 connections and port forward connections
2. Send an SSH disconnect message to the server
3. Wait for in-flight channel data to drain (brief timeout, ~2 seconds)
4. Close the transport stream
5. Exit
In-flight connections are not preserved across shutdown — they receive a connection reset. This matches the behavior of standard SSH tunnel tools.
## Error Handling
Error handling follows the project's layered pattern (see overview.md):
- **Transport errors**: Trigger reconnection with exponential backoff (see Reconnection section above). If reconnection fails indefinitely, the process continues retrying until the user terminates it.
- **Auth errors**: Cause reconnection retry. After repeated auth failures, the SOCKS5 server and port-forward listeners remain active but new channel opens fail until reconnection succeeds.
- **Channel-level errors**: Individual channel failures (target unreachable, proxy failure) close that channel without affecting the SSH session or other channels.
- **CLI errors**: Reported to stderr with a non-zero exit code. Fatal errors (invalid flags, key file not found) exit immediately.
## Open Questions
@@ -164,5 +203,7 @@ None — all resolved.
| ADR | Decision | Summary |
|-----|----------|---------|
| [005](decisions/005-socks5-before-tun.md) | SOCKS5 first | SOCKS5 is the primary interface; TUN is external (tun2proxy) |
| [006](decisions/006-no-logging-of-tunnel-destinations.md) | No logging of destinations | Client does not log SOCKS5 request targets (consistent with ADR-006) |
| [011](decisions/011-no-ssh-config-programmatic-api.md) | Programmatic-first API | No file-based config; options are structs, env vars, or CLI flags |
| [012](decisions/012-auth-ed25519-and-cert-authority.md) | Key + cert-authority | No password auth; OpenSSH cert-authority for multi-user |
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |

View File

@@ -0,0 +1,42 @@
# ADR-019: `--proxy` Has Different Semantics on Client vs Server
## Status
Accepted
## Context
The `--proxy` CLI flag appears on both `wraith connect` (client) and `wraith serve` (server), but the two sides proxy fundamentally different things:
- **Client**: `--proxy` routes the *transport connection* through the proxy. For example, `wraith connect --transport iroh --proxy socks5://127.0.0.1:1080` means the iroh endpoint's outbound TCP connections go through the specified SOCKS5 proxy before reaching the iroh relay. The proxy wraps the transport layer.
- **Server**: `--proxy` routes *outbound target connections* through the proxy. For example, `wraith serve --proxy socks5://127.0.0.1:9050` means when an SSH client opens a `direct_tcpip` channel to `db.internal:5432`, the server connects to that target through the specified proxy. The proxy wraps the data-plane connections.
Using the same flag name for both is intentional — from the user's perspective, both mean "route traffic through a proxy." But the layer at which the proxy operates differs, and this needs to be explicit so implementers don't confuse the two.
ADR-010 addressed transport chaining for the client side only. The server-side outbound proxy behavior has no ADR. This ADR documents both semantics and the rationale for sharing the flag name.
## Decision
The `--proxy` flag uses the same name on client and server, with documented different semantics:
| Side | Flag | What gets proxied | Example |
|------|------|-------------------|---------|
| Client | `--proxy` | Transport connection (outbound to server/relay) | `--transport iroh --proxy socks5://...` → iroh endpoint connects through proxy |
| Server | `--proxy` | Outbound target connections (data plane) | `--proxy socks5://...` → direct_tcpip targets reached through proxy |
On the **client**, `--proxy` affects the transport layer. It only applies to transports that make outbound TCP connections (iroh through a proxy, TLS through a proxy). For plain TCP transport, `--proxy` has no meaningful effect since the transport is already a direct TCP connection — use the SOCKS5 server instead.
On the **server**, `--proxy` affects the data plane. All `channel_open_direct_tcpip` outbound connections are routed through the proxy, regardless of transport mode.
This is not a naming collision — it's the same conceptual operation ("route through a proxy") at different layers. The shared name avoids forcing users to learn two proxy flags.
## Consequences
- **Positive**: One flag name (`--proxy`) instead of two. Users already understand "proxy" as "route through this."
- **Positive**: Client-side proxy is minimal implementation — iroh's endpoint builder accepts proxy config natively.
- **Positive**: Server-side proxy is straightforward — all outbound TCP from channel handlers goes through the proxy.
- **Negative**: Implementers must read the correct spec (client vs server) to understand what `--proxy` does for their side. This is mitigated by CLI help text that clearly describes the behavior per side.
- **Negative**: On the client, `--proxy` with `--transport tcp` is effectively a no-op (the transport is already a direct TCP connection to the server). The CLI should handle this case gracefully.
## References
- [ADR-010](010-transport-chaining-cli.md) — client-side transport chaining
- [transport.md](../transport.md) — transport layer spec
- [client.md](../client.md) — client CLI
- [server.md](../server.md) — server outbound proxy

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# NAPI Wrapper & PubSub Event Target
@@ -31,7 +31,7 @@ interface WraithConnectOptions {
// TCP/TLS mode
server?: string; // e.g., "example.com:443"
// iroh mode
peer?: string; // iroh EndpointId (hex)
peer?: string; // iroh endpoint ID (base58-encoded)
// Transport
transport: 'tcp' | 'tls' | 'iroh';
// Auth
@@ -76,10 +76,21 @@ interface WraithServer {
The NAPI layer is **transport-agnostic** — it doesn't know about pubsub's `EventEnvelope`. The pubsub adapter wraps the `Duplex` stream to implement `TypedEventTarget`. This separation ensures the NAPI wrapper is reusable for any stream-based protocol, not tied specifically to pubsub.
### NAPI `connect()` vs CLI `wraith connect`
The NAPI `connect()` function and the CLI `wraith connect` command are fundamentally different operations despite sharing the same name:
- **CLI `wraith connect`**: Starts a full SSH client session with a local SOCKS5 server and optional port forwards. It manages multiple SSH channels over a single session — the user routes traffic through it via SOCKS5 or forwarded ports.
- **NAPI `connect()`**: Opens a single SSH channel and returns it as a `Duplex` stream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly. This is designed for the pubsub/programmatic use case where a single bidirectional byte stream is needed.
For SOCKS5 proxy functionality, use the CLI binary (`wraith connect`). The NAPI wrapper is for programmatic consumers that need a raw stream.
### Programmatic Configuration (ADR-011)
Both `connect()` and `serve()` accept options as plain objects. No file paths are mandatory — keys can be provided as `Buffer` data directly, making programmatic usage straightforward. Environment variables (`WRAITH_SERVER`, `WRAITH_IDENTITY`) provide convenience defaults.
Key material provided as `Buffer` must be in **OpenSSH key format** (the format used by `ssh-keygen`). Private keys: OpenSSH format (`-----BEGIN OPENSSH PRIVATE KEY-----`). Public keys: OpenSSH format (`ssh-ed25519 AAAA...`). PEM-encoded keys (PKCS#1, PKCS#8) are not supported.
### PubSub Event Target Adapter
This implements `TypedEventTarget` from `@alkdev/pubsub`:
@@ -112,13 +123,11 @@ Wire protocol (same as other pubsub adapters):
The wraith server uses a reserved `direct_tcpip` destination (`wraith-control:0`) for the pubsub control channel (ADR-018). When a client connects to this destination:
1. The server's `channel_open_direct_tcpip` handler detects the reserved `wraith-control` target
When a client connects to this destination:
1. The server's `channel_open_direct_ip` handler detects the reserved `wraith-control` target
2. Instead of opening a TCP connection, it bridges the channel to its local pubsub event bus
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
Alternatively, the server can listen on a specific port (e.g., `9736`) for the hub's WebSocket server, and wraith simply port-forwards that port.
Users who prefer not to use the control channel can alternatively run a pubsub hub on a specific port and use standard port forwarding: `wraith connect --forward 9736:hub:9736`. This is a deployment choice, not a separate implementation — wraith's port forwarding works normally for any TCP service.
### Direction Agnostic

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# Open Questions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# Wraith Overview
@@ -72,6 +72,8 @@ The `wraith-core` crate exports the pluggable components for embedding or progra
7. **NAPI exposes both connect() and serve()** — The napi-rs wrapper provides client and server functionality, using napi-rs as the FFI bridge. The NAPI layer is transport-agnostic and not tied to pubsub. (ADR-015, ADR-016)
8. **Error handling follows a consistent layered pattern** — Transport and auth errors cause reconnection (client, with exponential backoff) or connection rejection (server). Channel-level errors (target unreachable, proxy failure) close the individual channel without killing the session. Library API errors propagate via `anyhow::Result` / `thiserror` types. CLI reports errors to stderr with appropriate exit codes. NAPI errors are marshalled as JavaScript exceptions.
## Design Decisions
| ADR | Decision | Summary |
@@ -94,6 +96,7 @@ The `wraith-core` crate exports the pluggable components for embedding or progra
| [016](decisions/016-napi-expose-connect-and-serve.md) | connect + serve | NAPI exposes both client and server from the start |
| [017](decisions/017-stealth-mode-protocol-multiplexing.md) | Stealth mode | Protocol multiplexing on port 443 |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `wraith-control` destination for pubsub |
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |
## Open Questions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# Server
@@ -67,6 +67,10 @@ This enables multi-user deployments where adding one CA line to `authorized_keys
**No password authentication over SSH.** Keys and certificates are sufficient and more secure. If a local SOCKS5 proxy needs its own auth layer, that's a separate concern.
### Key Material Format
Key inputs (`--key`, `--authorized-keys`, `--cert-authority`) accept either file paths or in-memory data (via library API or NAPI wrapper). The accepted format is **OpenSSH key format** throughout — private keys in OpenSSH format (`-----BEGIN OPENSSH PRIVATE KEY-----`), public keys in OpenSSH format (`ssh-ed25519 AAAA... user@host`), and authorized keys files in standard OpenSSH `authorized_keys` format. PEM-encoded keys (PKCS#1, PKCS#8) are not supported.
### TLS Certificate Provisioning
The server supports three TLS certificate modes (ADR-008):
@@ -81,6 +85,10 @@ ACME support is feature-gated behind the `acme` feature flag to keep the base bi
When a client opens a `channel_open_direct_tcpip(host, port, originator_addr, originator_port)`:
**Reserved destination** — If `host` starts with `wraith-` (e.g., `wraith-control`), the server routes the channel internally instead of connecting to a TCP target. The primary reserved destination is `wraith-control:0`, which bridges the channel to the local pubsub event bus (ADR-018).
**Regular destination** — For all other targets:
1. **Connection** — connect to `host:port`, either directly or via the configured outbound proxy
2. **Outbound connection** — connect to the target, either directly or via the configured outbound proxy
3. **Bidirectional proxy**`tokio::io::copy_bidirectional` between the SSH channel stream and the outbound TCP stream
@@ -107,49 +115,23 @@ When `--stealth` is enabled on the server alongside TLS transport:
This makes the server appear as an ordinary web server to port scanners and DPI systems.
### Server Handler (russh)
**Stealth mode requires TLS transport (`--transport tls`).** It has no effect with TCP or iroh transports — in those cases, there is no TLS handshake to peek behind, and protocol multiplexing is impossible. The CLI should reject or warn if `--stealth` is used without `--transport tls`.
```rust
struct WraithServerHandler {
authorized_keys: HashSet<PublicKey>,
cert_authorities: Vec<PublicKey>,
proxy_config: Option<ProxyConfig>,
}
### Server Handler Behavior
impl server::Handler for WraithServerHandler {
type Error = anyhow::Error;
The server handler implements `russh::server::Handler` with two primary responsibilities:
async fn auth_publickey(&mut self, user: &str, key: &PublicKey) -> Auth {
// Check direct key match
if self.authorized_keys.contains(key) {
return Auth::Accept;
}
// Check certificate authority validation
if let Some(cert) = key.as_certificate() {
for ca in &self.cert_authorities {
if cert.verify(ca) && cert.is_valid() {
return Auth::Accept;
}
}
}
Auth::Reject { proceed_with_methods: None, partial_success: false }
}
**Authentication (`auth_publickey`)**:
- Check the presented key against the configured `authorized_keys` set (constant-time comparison)
- If no direct match, check whether the key is a certificate signed by a trusted cert-authority
- Validate certificate signature, expiry, and principal restrictions (e.g., `permit-port-forwarding`, `no-pty`, `source-address`)
- Return `Accept` or `Reject`
async fn channel_open_direct_tcpip(
&mut self,
channel: Channel<server::Msg>,
host: &str,
port: u32,
originator_addr: &str,
originator_port: u32,
session: &mut server::Session,
) -> Result<Channel<server::Msg>, Self::Error> {
// Connect to host:port (directly or via proxy)
// Spawn bidirectional proxy task
Ok(channel)
}
}
```
**Channel handling (`channel_open_direct_tcpip`)**:
- If the destination host starts with `wraith-`, route internally (control channel, ADR-018)
- Otherwise, connect to `host:port` (directly or via the configured outbound proxy)
- Spawn a bidirectional proxy task between the SSH channel and the outbound TCP stream
- Return the channel for data flow
### Logging and Rate Limiting
@@ -236,20 +218,43 @@ wraith serve \
When running with `--transport iroh`, the server:
1. Creates an `iroh::Endpoint` with ALPN value `b"wraith-ssh"`
2. Prints its `EndpointId` (Ed25519 public key) — this is what clients use to connect
3. Uses `iroh::protocol::Router` to accept incoming connections
4. For each connection, accepts a `open_bi()` stream and passes it to `server::run_stream()`
1. Creates an iroh endpoint with ALPN value `b"wraith-ssh"`
2. Prints its endpoint ID (base58-encoded Ed25519 public key) — this is what clients use as the `--peer` value
3. Accepts incoming connections on the endpoint
4. For each connection, accepts a bidirectional stream and passes it to `server::run_stream()`
No listening port is needed. The server connects outbound to the iroh relay (default: n0, override with `--iroh-relay`) and awaits connections from clients who know its `EndpointId`.
No listening port is needed. The server connects outbound to the iroh relay (default: n0, override with `--iroh-relay`) and awaits connections from clients who know its endpoint ID (base58-encoded, printed on startup).
## Constraints
- The server does not log tunnel destinations (ADR-006). Auth events and connection events are logged for fail2ban integration (ADR-013).
- Destination strings beginning with `wraith-` are reserved for internal use (ADR-018). The server must not attempt TCP connections to `wraith-*` destinations — these are intercepted for control channel routing.
- One `ServerHandler` instance per connection. Handler state is not shared between connections (unless explicitly configured via `Arc` shared state for things like connection limits).
- The server binds to a single transport at a time. Running multiple transports (e.g., TCP + iroh) simultaneously requires separate processes or a future multiplexing feature.
- ACME support requires the `acme` feature flag. Without it, only manual TLS certs are supported.
- No password authentication over SSH channels. Key-based and cert-authority only (ADR-012).
- Stealth mode (`--stealth`) requires TLS transport. It has no effect on TCP or iroh transports (ADR-017).
## Graceful Shutdown
On SIGTERM or SIGINT:
1. Stop accepting new connections on the transport listener
2. Send SSH disconnect messages to all active sessions
3. Wait for in-flight channel data to drain (brief timeout, ~2 seconds per session)
4. Close all transport listeners
5. Exit
The server does not wait indefinitely for idle connections to close. After the drain timeout, remaining connections are forcibly terminated. This prevents a slow or stuck client from blocking shutdown indefinitely.
## Error Handling
Error handling follows the project's layered pattern (see overview.md):
- **Transport errors**: Cause connection rejection. The listener remains active — a failed TLS handshake or iroh connection attempt does not affect other incoming connections.
- **Auth errors**: Result in connection rejection with a logged auth failure event (for fail2ban, ADR-013). Repeated failures from one connection trigger disconnect after `--max-auth-attempts`.
- **Channel-level errors**: Individual channel failures (target unreachable, proxy failure) close that channel without affecting the SSH session or other channels. The client receives a channel open failure message.
- **CLI errors**: Reported to stderr with a non-zero exit code. Fatal errors (invalid flags, key file not found, bind failure) exit immediately.
## Open Questions
@@ -267,3 +272,4 @@ None — all resolved.
| [013](decisions/013-fail2ban-friendly-logging.md) | Fail2ban-friendly logging | Structured auth logs + built-in rate limiting |
| [017](decisions/017-stealth-mode-protocol-multiplexing.md) | Stealth mode | Protocol multiplexing on port 443 |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `wraith-control` destination for pubsub |
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-06-01
status: reviewed
last_updated: 2026-06-02
---
# Transport Layer
@@ -61,7 +61,7 @@ pub struct TransportInfo {
pub enum TransportKind {
Tcp,
Tls { server_name: Option<String> },
Iroh { peer_id: String },
Iroh { endpoint_id: String },
}
```
@@ -75,12 +75,7 @@ pub enum TransportKind {
### Iroh Stream Join
Since QUIC splits streams into separate `RecvStream` and `SendStream`, while russh expects a single duplex stream, we combine them:
```rust
// One line. This works because RecvStream: AsyncRead and SendStream: AsyncWrite.
let stream = tokio::io::join(recv_stream, send_stream);
```
Since QUIC splits streams into separate `RecvStream` (implements `AsyncRead`) and `SendStream` (implements `AsyncWrite`), while russh expects a single duplex stream, they are combined using `tokio::io::join(recv_stream, send_stream)` which produces a `Join<RecvStream, SendStream>` implementing both traits.
See ADR-003 for the decision to use `tokio::io::join` over a custom wrapper.
@@ -100,13 +95,13 @@ Transports can be nested. The CLI supports `--transport iroh --proxy socks5://..
wraith connect --transport iroh --proxy socks5://127.0.0.1:1080
```
This routes iroh's outbound TCP connections through the specified SOCKS5 proxy. iroh's `Endpoint::builder` accepts proxy configuration directly, so the implementation is minimal — parse the proxy URL and pass it to the endpoint builder.
This routes iroh's outbound TCP connections through the specified SOCKS5 proxy. The iroh transport supports SOCKS5 and HTTP proxy configuration for its outbound connections — the proxy URL is applied during transport initialization.
For other combinations:
- TCP + TLS is already implicit (TLS wraps TCP in `TlsTransport`)
- TLS + SOCKS5 proxy is also supported via `--proxy` with `--transport tls`
**Note**: `--proxy` has different semantics on the client vs the server:
**Note**: `--proxy` has different semantics on the client vs the server (ADR-019):
- **Client**: `--proxy` routes the *transport connection* through the proxy (e.g., iroh endpoint → SOCKS5 → iroh relay)
- **Server**: `--proxy` routes *outbound target connections* through the proxy (e.g., SSH channel request → SOCKS5 → target host)
@@ -154,3 +149,4 @@ None — all resolved.
| [008](decisions/008-acme-lets-encrypt.md) | ACME/Let's Encrypt | Auto-provision TLS certs, domain and IP paths |
| [009](decisions/009-default-iroh-relay.md) | Default iroh relay | n0 relay by default, `--iroh-relay` override |
| [010](decisions/010-transport-chaining-cli.md) | Transport chaining | `--proxy` works with all transports natively |
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |