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
7.4 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 2026-06-02 |
Wraith Overview
Purpose
Wraith is a self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol. It enables:
- Private tunneling of services (Postgres, Redis, internal APIs) over SSH
- Censorship circumvention — SSH over TLS on port 443 looks like HTTPS to DPI
- NAT traversal — iroh transport allows peer-to-peer connections without public IPs or port forwarding
- Service mesh connectivity — a lightweight transport layer for the pubsub/operations event system
The core insight: SSH tunnels work because SSH is fundamental infrastructure. Blocking it breaks the internet. Wraith makes SSH tunneling accessible through a simple CLI with pluggable transports.
Exports
Binary: wraith
A single binary with subcommands:
wraith serve — Start the server (accepts SSH connections)
wraith connect — Start the client (opens SSH session, exposes SOCKS5/port-forwards)
Library: wraith-core
The wraith-core crate exports the pluggable components for embedding or programmatic use:
Transporttrait — produces a duplex stream for SSH to run overTcpTransport— direct TCP connectionTlsTransport— TCP + tokio-rustls TLSIrohTransport— iroh QUIC P2P connectionSocks5Server— local SOCKS5 proxy that forwards through SSH channelsPortForwarder— manages local/remote port forwardsServerHandler— russh server handler with configurable auth and channel policiesConnectOptions/ServeOptions— programmatic configuration structs (no file parsing)
Dependencies
| Dependency | Purpose | Feature-gated |
|---|---|---|
russh |
SSH client & server | No (core) |
tokio |
Async runtime | No (core) |
tokio-rustls |
TLS wrapping | Yes (tls) |
rustls |
TLS implementation | Yes (tls) |
rustls-acme |
ACME/Let's Encrypt auto-cert | Yes (acme) |
iroh |
P2P QUIC transport | Yes (iroh) |
clap |
CLI argument parsing | No (core) |
tracing |
Structured logging | No (core) |
anyhow / thiserror |
Error handling | No (core) |
Note:
tun-rsis no longer a dependency. TUN support is deferred in favor of the externaltun2proxytool (ADR-014).
Architecture Constraints
-
SSH runs over transport, not alongside — The transport layer produces a single
AsyncRead+AsyncWrite+Unpin+Sendstream. SSH runs over that stream viarussh::client::connect_stream()/russh::server::run_stream(). The SSH layer never knows what transport it's on. (ADR-001, ADR-004) -
SOCKS5 is the primary client interface — Port forwarding is built on top of SOCKS5-like channel management. For VPN-like "route all traffic" behavior, users run
tun2proxyalongside wraith's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014) -
No logging of tunnel destinations — The server logs auth attempts and connections (for fail2ban) but does not log
channel_open_direct_tcpipdestinations, DNS lookups, or bytes transferred. (ADR-006, ADR-013) -
Programmatic-first API — Configuration via CLI flags, library API structs (
ConnectOptions,ServeOptions), and environment variables. No~/.ssh/configparsing, no custom config files. (ADR-011) -
Feature flags control transport inclusion —
tls,iroh,acmeare feature-gated so the base install is lean. Users opt in to heavier dependencies. -
Authentication is key-based — Ed25519 public key (default) and OpenSSH certificate authority. No password authentication over SSH. (ADR-012)
-
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)
-
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/thiserrortypes. CLI reports errors to stderr with appropriate exit codes. NAPI errors are marshalled as JavaScript exceptions.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 001 | Pluggable transport | Transport trait produces AsyncRead+AsyncWrite+Unpin+Send, SSH consumes it |
| 002 | TUN shim separate | Superseded — TUN is deferred, use tun2proxy (ADR-014) |
| 003 | iroh stream join | tokio::io::join(recv, send) combines QUIC halves |
| 004 | SSH over transport | SSH never accesses TCP/iroh/TLS directly |
| 005 | SOCKS5 first | SOCKS5 is the primary interface; TUN is external (tun2proxy) |
| 006 | No logging of tunnel destinations | Server logs auth and connections, not destinations |
| 007 | NAPI single stream | NAPI exposes duplex streams, not SSH multiplexing |
| 008 | ACME/Let's Encrypt | Auto-provision TLS certs, domain and IP paths |
| 009 | Default iroh relay | n0 relay by default, --iroh-relay override |
| 010 | Transport chaining | --proxy works with all transports natively |
| 011 | Programmatic-first | No file-based config; options are structs, env vars, CLI flags |
| 012 | Key + cert-authority | Ed25519 keys + OpenSSH CA; no password auth |
| 013 | Fail2ban-friendly | Structured auth logs + built-in rate limiting |
| 014 | Defer TUN | Use tun2proxy for VPN-like behavior; no wraith-tun binary |
| 015 | napi-rs | Standard Node.js native addon tooling |
| 016 | connect + serve | NAPI exposes both client and server from the start |
| 017 | Stealth mode | Protocol multiplexing on port 443 |
| 018 | Control channel | Reserved wraith-control destination for pubsub |
| 019 | Proxy dual semantics | --proxy routes transport on client, data on server |
Open Questions
All open questions have been resolved. See open-questions.md for resolution details.
References
- Feasibility Assessment
- russh API — SSH client/server library
- Dispatch — Reference implementation of russh port forwarding
- iroh — P2P QUIC connections
- tun2proxy — Recommended external TUN-to-SOCKS5 tool
- Production certbot setup — Let's Encrypt on our infrastructure
- Production fail2ban setup — fail2ban with nftables on our infrastructure