diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1df6f58..3dfe4a9 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 diff --git a/docs/architecture/client.md b/docs/architecture/client.md index 75c76ca..98f8630 100644 --- a/docs/architecture/client.md +++ b/docs/architecture/client.md @@ -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` 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 \ # TCP/TLS server address (required for tcp/tls) - --peer \ # iroh peer ID (required for iroh) + --peer \ # iroh endpoint ID, base58-encoded (required for iroh) --transport tcp|tls|iroh \ # Transport mode --identity \ # SSH private key (path or in-memory) --socks5 \ # 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 | \ No newline at end of file +| [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 | \ No newline at end of file diff --git a/docs/architecture/decisions/019-proxy-dual-semantics.md b/docs/architecture/decisions/019-proxy-dual-semantics.md new file mode 100644 index 0000000..ecd13bf --- /dev/null +++ b/docs/architecture/decisions/019-proxy-dual-semantics.md @@ -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 \ No newline at end of file diff --git a/docs/architecture/napi-and-pubsub.md b/docs/architecture/napi-and-pubsub.md index 8a227f7..ec2d775 100644 --- a/docs/architecture/napi-and-pubsub.md +++ b/docs/architecture/napi-and-pubsub.md @@ -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 diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index e22f4a1..1c1d4f8 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -1,6 +1,6 @@ --- -status: draft -last_updated: 2026-06-01 +status: reviewed +last_updated: 2026-06-02 --- # Open Questions diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index aebb46a..88f43d4 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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 diff --git a/docs/architecture/server.md b/docs/architecture/server.md index b6d7e2f..4769df1 100644 --- a/docs/architecture/server.md +++ b/docs/architecture/server.md @@ -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, - cert_authorities: Vec, - proxy_config: Option, -} +### 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, - host: &str, - port: u32, - originator_addr: &str, - originator_port: u32, - session: &mut server::Session, - ) -> Result, 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 @@ -266,4 +271,5 @@ None — all resolved. | [012](decisions/012-auth-ed25519-and-cert-authority.md) | Key + cert-authority auth | No password auth; support OpenSSH cert-authority | | [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 | \ No newline at end of file +| [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 | \ No newline at end of file diff --git a/docs/architecture/transport.md b/docs/architecture/transport.md index a4d7a53..1d96350 100644 --- a/docs/architecture/transport.md +++ b/docs/architecture/transport.md @@ -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 }, - 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` 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) @@ -153,4 +148,5 @@ None — all resolved. | [004](decisions/004-ssh-over-transport.md) | SSH over transport | SSH never touches TCP/iroh/TLS directly | | [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 | \ No newline at end of file +| [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 | \ No newline at end of file