Resolved all 11 open questions based on project guidance: Transport: - OQ-01/OQ-07: ACME/Let's Encrypt with domain + IP paths (ADR-008) - OQ-02: Default to n0 relay, --iroh-relay override (ADR-009) - OQ-05: Transport chaining supported natively (ADR-010) Client: - OQ-06: Programmatic-first API, no ~/.ssh/config (ADR-011) Server: - OQ-04: Ed25519 + OpenSSH cert-authority, no password auth (ADR-012) - OQ-08: fail2ban-friendly logging + built-in rate limiting (ADR-013) TUN: - OQ-03/OQ-09: Deferred entirely, recommend tun2proxy (ADR-014) - tun-shim.md marked deprecated NAPI: - OQ-10: Expose both connect() and serve() (ADR-016) - OQ-11: Use napi-rs for FFI bridge (ADR-015) Additional ADRs created during review: - ADR-006: No logging of tunnel destinations (was phantom reference) - ADR-017: Stealth mode protocol multiplexing - ADR-018: Control channel for pubsub over SSH Fixed: ADR-002 status → Superseded, ADR-007 title typo, WRAUTH_SERVER typo, ADR-005 stale wraith-tun refs, undefined ACL feature removed from server.md, --proxy semantic difference documented.
8.9 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-01 |
Client
What
The wraith client establishes an SSH session to a server (via pluggable transport) and exposes a local SOCKS5 proxy for routing traffic through that session. Port forwarding (-L / -R style) covers specific service access like Postgres or Redis.
Why
Users need a way to route traffic through the SSH tunnel. SOCKS5 is the primary interface — it's standard, well-supported by browsers and CLI tools, and needs no privileges. Port forwarding covers specific service access. For VPN-like "route all traffic" behavior, users run tun2proxy alongside wraith (ADR-014).
Architecture
Client Components
┌────────────────────────────────────────────────────────┐
│ wraith connect │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SOCKS5 │ │ Port │ │ Remote │ │
│ │ Server │ │ Forward │ │ Forward │ │
│ │ :1080 │ │ -L spec │ │ -R spec │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Channel Manager │ │
│ │ (opens direct-tcpip, │ │
│ │ forwarded-tcpip streams) │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ┌──────────────▼──────────────────┐ │
│ │ SSH Client (russh) │ │
│ │ Handle<ClientHandler> │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ┌──────────────▼──────────────────┐ │
│ │ Transport │ │
│ │ (Tcp / Tls / Iroh) │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
SOCKS5 Server
The primary client interface. Listens on a local port (default 127.0.0.1:1080), accepts SOCKS5 connections, and for each connection:
- Reads the SOCKS5 handshake (auth method negotiation, target address)
- Opens a
channel_open_direct_tcpip(target_host, target_port, originator_addr, originator_port)on the SSH session - Converts the SSH channel to a stream via
channel.into_stream() - 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.
Port Forwarding
Local port forwards (-L local_addr:local_port:remote_host:remote_port):
- Bind
TcpListeneronlocal_addr:local_port - For each accepted connection, open
channel_open_direct_tcpip(remote_host, remote_port, ...) - Proxy bytes bidirectionally via
copy_bidirectional
Remote port forwards (-R remote_addr:remote_port:local_host:local_port):
- Send
tcpip_forward(remote_addr, remote_port)to request the server listen on a port - When the handler receives
server_channel_open_forwarded_tcpip, connect tolocal_host:local_port - Proxy bytes bidirectionally
Channel Manager
The channel manager owns the Arc<client::Handle<ClientHandler>> and provides methods:
open_direct_tcpip(host, port)— open a tunnel channel to a remote hostopen_streamlocal(socket_path)— open a tunnel to a Unix socketrequest_tcpip_forward(addr, port)— request remote listeningcancel_tcpip_forward(addr, port)— cancel remote listening
It also handles reconnection: if handle.is_closed() returns true, attempt reconnection with exponential backoff.
Reconnection
On transport failure:
- Detect via
handle.is_closed()or transport read error - Exponential backoff reconnect (1s, 2s, 4s, ... max 30s)
- Re-establish transport connection
- Re-authenticate SSH session
- 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.
Programmatic Configuration (ADR-011)
The client uses programmatic configuration — no ~/.ssh/config parsing, no custom config files. Configuration comes from:
- CLI flags:
--server,--identity,--transport, etc. - Library API:
ConnectOptionsandServeOptionsstructs inwraith-core, constructable programmatically - Environment variables:
WRAITH_SERVER,WRAITH_IDENTITYas convenience defaults
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.
CLI Interface
# Basic connection (TCP, default port 22)
wraith connect --server example.com --identity ~/.ssh/id_ed25519
# With TLS
wraith connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519
# With TLS + insecure (self-signed certs)
wraith connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519 --insecure
# With iroh (no public IP needed)
wraith connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519
# With iroh + custom relay
wraith connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519 --iroh-relay https://relay.example.com
# With iroh + proxy (transport chaining)
wraith connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519 --proxy socks5://127.0.0.1:1080
# SOCKS5 on custom port
wraith connect --server example.com --socks5 127.0.0.1:1080 --identity ~/.ssh/id_ed25519
# With port forwards
wraith connect --server example.com --forward 5432:db.internal:5432 --forward 6379:redis.internal:6379
# All options
wraith connect \
--server <addr> \ # TCP/TLS server address (required for tcp/tls)
--peer <endpoint-id> \ # iroh peer ID (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)
--forward <spec> \ # Port forward spec (repeatable)
--remote-forward <spec> \ # Remote port forward spec (repeatable)
--proxy <url> \ # Upstream proxy (socks5:// or http://)
--iroh-relay <url> \ # iroh relay URL (default: n0 relay)
--tls-server-name <host> \ # SNI hostname for TLS
--insecure # Accept self-signed TLS certs
Constraints
- SOCKS5 is always enabled when
wraith connectruns (it's the primary interface). Port forwards are optional. - The client does not log tunnel destinations. The SOCKS5 server connects and proxies — no logging of SOCKS5 request targets.
- Authentication is Ed25519 public key or OpenSSH certificate (ADR-012). No password authentication over SSH.
- Only one SSH session per
wraith connectprocess. Multiple sessions = multiple processes (or a future multiplexer). - No
~/.ssh/configparsing. 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:1080alongside the client, not by a built-in TUN interface (ADR-014).
Open Questions
None — all resolved.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 005 | SOCKS5 first | SOCKS5 is the primary interface; TUN is external (tun2proxy) |
| 011 | Programmatic-first API | No file-based config; options are structs, env vars, or CLI flags |
| 012 | Key + cert-authority | No password auth; OpenSSH cert-authority for multi-user |