refactor!: rebrand wraith to alknet
Rename all crates, CLI commands, constants, type names, doc comments, and documentation from wraith to alknet. Includes wire-protocol changes: ALPN wraith-ssh -> alknet-ssh, reserved destination prefix wraith- -> alknet-, SSH auth username wraith -> alknet.
This commit is contained in:
@@ -3,7 +3,7 @@ status: draft
|
||||
last_updated: 2026-06-04
|
||||
---
|
||||
|
||||
# Wraith Architecture
|
||||
# Alknet Architecture
|
||||
|
||||
## Current State
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ shared across both auth paths. Identity resolution produces a transport-agnostic
|
||||
|
||||
## Why
|
||||
|
||||
Wraith currently authenticates connections exclusively through SSH public key
|
||||
Alknet currently authenticates connections exclusively through SSH public key
|
||||
auth. Non-SSH transports (WebTransport) cannot perform SSH key exchange — they
|
||||
need a different auth presentation that shares the same key material. The
|
||||
unified auth layer ensures one key set, one identity, one rotation mechanism
|
||||
@@ -48,7 +48,7 @@ AuthToken = base64url(key_id || timestamp || signature)
|
||||
|
||||
Wire format when passed in a WebTransport CONNECT request:
|
||||
```
|
||||
CONNECT https://server:443/wraith?token=<AuthToken>
|
||||
CONNECT https://server:443/alknet?token=<AuthToken>
|
||||
```
|
||||
|
||||
Server verification:
|
||||
@@ -74,7 +74,7 @@ ADR-023.
|
||||
|
||||
### IdentityProvider Trait
|
||||
|
||||
The `IdentityProvider` trait decouples wraith-core from any specific identity
|
||||
The `IdentityProvider` trait decouples alknet-core from any specific identity
|
||||
storage. It resolves a key fingerprint or auth token to an `Identity` with
|
||||
scopes and resources.
|
||||
|
||||
@@ -103,7 +103,7 @@ default scope set. No database required.
|
||||
`accounts` tables plus the ACL graph. Resolves fingerprint → account →
|
||||
organization membership → effective scopes. Uses `ArcSwap` for hot reload.
|
||||
|
||||
The trait is the contract. The backing store is pluggable. Wraith-core never
|
||||
The trait is the contract. The backing store is pluggable. Alknet-core never
|
||||
depends on Honker, SQLite, or any specific database.
|
||||
|
||||
### AuthPolicy Structure
|
||||
@@ -167,7 +167,7 @@ authorization decisions.
|
||||
|
||||
The wtransport library's `SessionRequest` provides:
|
||||
|
||||
- `path()` — URL path (e.g., `/wraith?token=...`)
|
||||
- `path()` — URL path (e.g., `/alknet?token=...`)
|
||||
- `headers()` — HTTP headers (for `Authorization: Bearer ...`)
|
||||
- `origin()` — Browser origin (for CORS-like restrictions)
|
||||
- `remote_address()` — Client UDP address
|
||||
@@ -204,7 +204,7 @@ dependencies needed.
|
||||
|
||||
- Auth tokens are Ed25519-signed with the same key pair used for SSH auth. No
|
||||
separate key management for non-SSH transports.
|
||||
- `IdentityProvider` is the only interface between wraith-core and identity
|
||||
- `IdentityProvider` is the only interface between alknet-core and identity
|
||||
storage. No database dependency at the core level.
|
||||
- The SSH auth path is unchanged. `auth_publickey()` continues to work exactly
|
||||
as it does today. Token auth is additive.
|
||||
|
||||
@@ -327,7 +327,7 @@ defined in `@alkdev/operations`. The TypeScript implementation provides:
|
||||
|
||||
The Rust implementation mirrors these types and behaviors. TypeScript consumers
|
||||
continue using `@alkdev/operations` over `@alkdev/pubsub` adapters (including
|
||||
the `event-target-wraith` adapter). Rust consumers use core's registry directly.
|
||||
the `event-target-alknet` adapter). Rust consumers use core's registry directly.
|
||||
Both speak the same wire protocol and can interoperate.
|
||||
|
||||
The key principle: **the same `EventEnvelope` can flow from a Rust handler
|
||||
|
||||
@@ -7,11 +7,11 @@ last_updated: 2026-06-02
|
||||
|
||||
## 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.
|
||||
The alknet 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).
|
||||
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 alknet (ADR-014).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -19,7 +19,7 @@ Users need a way to route traffic through the SSH tunnel. SOCKS5 is the primary
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ wraith connect │
|
||||
│ alknet connect │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ SOCKS5 │ │ Port │ │ Remote │ │
|
||||
@@ -101,8 +101,8 @@ The channel manager orchestrates reconnection: it creates a new transport stream
|
||||
The client uses programmatic configuration — no `~/.ssh/config` parsing, no custom config files. Configuration comes from:
|
||||
|
||||
1. **CLI flags**: `--server`, `--identity`, `--transport`, etc.
|
||||
2. **Library API**: `ConnectOptions` and `ServeOptions` structs in `wraith-core`, constructable programmatically
|
||||
3. **Environment variables**: `WRAITH_SERVER`, `WRAITH_IDENTITY` as convenience defaults
|
||||
2. **Library API**: `ConnectOptions` and `ServeOptions` structs in `alknet-core`, constructable programmatically
|
||||
3. **Environment variables**: `ALKNET_SERVER`, `ALKNET_IDENTITY` as 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.
|
||||
|
||||
@@ -110,7 +110,7 @@ This approach avoids cross-platform path issues (`~` expansion, Windows `USERPRO
|
||||
|
||||
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`)
|
||||
- **File path**: A filesystem path to a key file (e.g., `~/.ssh/id_ed25519`, `/etc/alknet/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:
|
||||
@@ -125,31 +125,31 @@ PEM-encoded keys (PKCS#1, PKCS#8) are not supported. Use OpenSSH format keys thr
|
||||
|
||||
```bash
|
||||
# Basic connection (TCP, default port 22)
|
||||
wraith connect --server example.com --identity ~/.ssh/id_ed25519
|
||||
alknet connect --server example.com --identity ~/.ssh/id_ed25519
|
||||
|
||||
# With TLS
|
||||
wraith connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519
|
||||
alknet 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
|
||||
alknet 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
|
||||
alknet 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
|
||||
alknet 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
|
||||
alknet 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
|
||||
alknet 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
|
||||
alknet connect --server example.com --forward 5432:db.internal:5432 --forward 6379:redis.internal:6379
|
||||
|
||||
# All options
|
||||
wraith connect \
|
||||
alknet connect \
|
||||
--server <addr> \ # TCP/TLS server address (required for tcp/tls)
|
||||
--peer <endpoint-id> \ # iroh endpoint ID, base58-encoded (required for iroh)
|
||||
--transport tcp|tls|iroh \ # Transport mode
|
||||
@@ -165,13 +165,13 @@ wraith connect \
|
||||
|
||||
## Constraints
|
||||
|
||||
- SOCKS5 is always enabled when `wraith connect` runs (it's the primary interface). Port forwards are optional.
|
||||
- SOCKS5 is always enabled when `alknet connect` runs (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 connect` process. Multiple sessions = multiple processes (or a future multiplexer).
|
||||
- Only one SSH session per `alknet 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.
|
||||
- The CLI `alknet 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
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
Wraith needs to support multiple transport modes (TCP, TLS, iroh) for SSH sessions. Each mode has different connection establishment logic but produces the same result: a bidirectional byte stream. Without an abstraction, each transport would need its own SSH connection code path.
|
||||
Alknet needs to support multiple transport modes (TCP, TLS, iroh) for SSH sessions. Each mode has different connection establishment logic but produces the same result: a bidirectional byte stream. Without an abstraction, each transport would need its own SSH connection code path.
|
||||
|
||||
russh's `client::connect_stream()` and `server::run_stream()` both accept `AsyncRead + AsyncWrite + Unpin + Send`, meaning SSH is already transport-agnostic at the API level. The design question is whether to enshrine this in wraith's own type system or handle each transport case-by-case.
|
||||
russh's `client::connect_stream()` and `server::run_stream()` both accept `AsyncRead + AsyncWrite + Unpin + Send`, meaning SSH is already transport-agnostic at the API level. The design question is whether to enshrine this in alknet's own type system or handle each transport case-by-case.
|
||||
|
||||
## Decision
|
||||
Define a `Transport` trait that produces `AsyncRead + AsyncWrite + Unpin + Send` streams. Each transport (TCP, TLS, iroh) implements this trait. The SSH layer calls `transport.connect()` and passes the result to `russh::client::connect_stream()`.
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
Superseded by ADR-014
|
||||
|
||||
## Context
|
||||
TUN interface creation requires root privileges or `CAP_NET_ADMIN` on Linux, Administrator on Windows, or platform-specific VPN APIs on macOS/iOS/Android. If the core wraith binary required these privileges, the attack surface of root-required code would include the entire SSH implementation, key handling, and transport negotiation.
|
||||
TUN interface creation requires root privileges or `CAP_NET_ADMIN` on Linux, Administrator on Windows, or platform-specific VPN APIs on macOS/iOS/Android. If the core alknet binary required these privileges, the attack surface of root-required code would include the entire SSH implementation, key handling, and transport negotiation.
|
||||
|
||||
The primary use cases (SOCKS5 proxy, port forwarding) need no privileges at all. Only the "route all traffic through TUN" use case needs root.
|
||||
|
||||
## Decision
|
||||
The TUN functionality is a separate `wraith-tun` binary that:
|
||||
The TUN functionality is a separate `alknet-tun` binary that:
|
||||
1. Creates a TUN device (requires root / CAP_NET_ADMIN)
|
||||
2. Reads IP packets from it
|
||||
3. Forwards each connection to the core wraith's SOCKS5 port (127.0.0.1:1080)
|
||||
3. Forwards each connection to the core alknet's SOCKS5 port (127.0.0.1:1080)
|
||||
4. Proxies bytes between TUN packets and SOCKS5 connections
|
||||
|
||||
The core `wraith connect` binary never needs root. The `wraith-tun` binary is ~200-500 lines and does nothing except TUN ↔ SOCKS5 forwarding.
|
||||
The core `alknet connect` binary never needs root. The `alknet-tun` binary is ~200-500 lines and does nothing except TUN ↔ SOCKS5 forwarding.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Root-required code surface is tiny and auditable.
|
||||
|
||||
@@ -19,7 +19,7 @@ This is directly enabled by russh's `connect_stream()` and `run_stream()` APIs,
|
||||
- **Positive**: Adding a new transport requires implementing the `Transport` trait, not modifying SSH code.
|
||||
- **Positive**: Testing is straightforward — mock transports produce in-memory streams.
|
||||
- **Positive**: Security audit is clean — the SSH implementation has no network-facing code.
|
||||
- **Positive**: The transport can be layered. Iroh connecting through a SOCKS5 proxy (which itself tunnels through wraith) is just a transport that calls out to a SOCKS5 library before establishing the QUIC connection.
|
||||
- **Positive**: The transport can be layered. Iroh connecting through a SOCKS5 proxy (which itself tunnels through alknet) is just a transport that calls out to a SOCKS5 library before establishing the QUIC connection.
|
||||
- **Negative**: SSH keepalive and reconnection must be handled at the transport level. If the transport stream dies, the SSH session dies. Reconnection means establishing a new transport + new SSH session. There's no "SSH reconnects over the same transport" — you get a new session.
|
||||
- **Negative**: Multiple SSH sessions over the same iroh connection require the iroh `Endpoint` (not stream) to be shared between sessions. The transport trait produces one stream per `connect()` call. The iroh `Endpoint` must be created externally and shared. (The `IrohTransport` struct holds an `Arc<Endpoint>`.)
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@ SOCKS5 is the core because:
|
||||
TUN forwards to SOCKS5 rather than directly to SSH because:
|
||||
- The SOCKS5 code already handles TCP connection establishment and bidirectional proxying
|
||||
- TUN's job is just IP packet → SOCKS5 connection, not IP packet → SSH channel
|
||||
- The `wraith-tun` binary stays minimal (~200-500 lines)
|
||||
- The `alknet-tun` binary stays minimal (~200-500 lines)
|
||||
- No root code in the core binary
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Core binary is root-free. TUN functionality is provided by the external `tun2proxy` tool (ADR-014).
|
||||
- **Positive**: SOCKS5 is testable without TUN — just `curl` against it.
|
||||
- **Positive**: The TUN approach is validated by tun2proxy, a well-tested existing tool. No custom TUN code to maintain.
|
||||
- **Negative**: VPN-like behavior requires running `tun2proxy` alongside `wraith connect` — two processes instead of one integrated binary.
|
||||
- **Negative**: VPN-like behavior requires running `tun2proxy` alongside `alknet connect` — two processes instead of one integrated binary.
|
||||
- **Negative**: SOCKS5 doesn't capture UDP (except DNS via SOCKS5h). TUN mode via tun2proxy handles this separately.
|
||||
|
||||
## References
|
||||
|
||||
@@ -30,7 +30,7 @@ This separation ensures fail2ban has enough data to detect abusive IPs while des
|
||||
- **Positive**: Tunnel destinations are never written to disk or any observable log. This is the same guarantee OpenSSH makes with `LogLevel VERBOSE` or below.
|
||||
- **Positive**: Reduces legal and privacy exposure for server operators.
|
||||
- **Positive**: fail2ban can still work — it needs source IPs and auth failures, not destinations.
|
||||
- **Negative**: Server operators cannot audit what destinations clients are accessing. If an operator needs this for compliance, they must implement it outside wraith (e.g., network-level logging at the target host).
|
||||
- **Negative**: Server operators cannot audit what destinations clients are accessing. If an operator needs this for compliance, they must implement it outside alknet (e.g., network-level logging at the target host).
|
||||
- **Negative**: Debugging connectivity issues is harder without destination logs. Mitigated by client-side logging (the client knows what it's connecting to).
|
||||
|
||||
## References
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The NAPI wrapper for wraith could expose different granularity levels:
|
||||
The NAPI wrapper for alknet could expose different granularity levels:
|
||||
|
||||
1. **Full SSH API**: Expose channel multiplexing, `open_direct_tcpip`, `tcpip_forward`, session management. The TypeScript layer would manage channels.
|
||||
2. **Single duplex stream**: The NAPI wrapper establishes one SSH channel and returns it as a Node.js `Duplex` stream. TypeScript multiplexing (if needed) happens at the pubsub layer.
|
||||
|
||||
@@ -10,7 +10,7 @@ There are two ACME flows:
|
||||
1. **Domain-based**: Standard flow with DNS-01 or HTTP-01 challenge. Certificate is tied to a domain name, auto-renews via certbot/systemd timer. Requires port 80 or DNS access for challenges.
|
||||
2. **IP-based**: Short-lived certificates via TLS-ALPN-01 challenge on port 443. No domain needed, but cert is short-lived (days, not months). Simpler for quick setups but requires the ACME client to run continuously.
|
||||
|
||||
Both flows are important for wraith's usability. Without ACME, TLS mode requires manual cert setup — a significant barrier for users who want "SSH over port 443" for censorship resistance.
|
||||
Both flows are important for alknet's usability. Without ACME, TLS mode requires manual cert setup — a significant barrier for users who want "SSH over port 443" for censorship resistance.
|
||||
|
||||
## Decision
|
||||
Support both ACME certificate provisioning paths:
|
||||
@@ -21,10 +21,10 @@ Support both ACME certificate provisioning paths:
|
||||
|
||||
3. **Manual certs** (`--tls-cert` / `--tls-key`): Always supported for users with existing certificates or specific PKI setups.
|
||||
|
||||
The implementation should use the `rustls-acme` crate (or similar pure-Rust ACME client) to avoid an external certbot dependency. This keeps wraith self-contained as a single binary.
|
||||
The implementation should use the `rustls-acme` crate (or similar pure-Rust ACME client) to avoid an external certbot dependency. This keeps alknet self-contained as a single binary.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Users can run `wraith serve --transport tls --acme-domain example.com` and get working TLS with zero manual cert management.
|
||||
- **Positive**: Users can run `alknet serve --transport tls --acme-domain example.com` and get working TLS with zero manual cert management.
|
||||
- **Positive**: IP-based ACME covers the quick-setup case without requiring a domain.
|
||||
- **Positive**: Consistent with our production infrastructure (certbot + Let's Encrypt is already our standard).
|
||||
- **Negative**: ACME adds complexity to the server binary (challenge responder, cert store, renewal timer).
|
||||
|
||||
@@ -18,7 +18,7 @@ Default to n0's relay servers. Allow override via `--iroh-relay <url>` CLI flag.
|
||||
This matches iroh's own defaults — n0's relay is the standard starting point. Users who need production reliability self-host.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Zero-config iroh transport for testing and development. `wraith serve --transport iroh` just works.
|
||||
- **Positive**: Zero-config iroh transport for testing and development. `alknet serve --transport iroh` just works.
|
||||
- **Positive**: Self-hosting is a single flag override, not a complex setup requirement.
|
||||
- **Negative**: Default depends on n0's infrastructure. If n0's relay is down, default iroh connections fail (but this is the same experience as every iroh user).
|
||||
- **Negative**: Privacy-conscious users must remember to `--iroh-relay` to avoid n0. Mitigated by documentation.
|
||||
|
||||
@@ -7,10 +7,10 @@ Accepted
|
||||
Transport chaining allows combining iroh with an upstream proxy, e.g.:
|
||||
|
||||
```bash
|
||||
wraith connect --transport iroh --proxy socks5://127.0.0.1:1080
|
||||
alknet connect --transport iroh --proxy socks5://127.0.0.1:1080
|
||||
```
|
||||
|
||||
This routes iroh's outbound TCP connections through a SOCKS5 proxy, which could itself be another wraith instance. This is important for:
|
||||
This routes iroh's outbound TCP connections through a SOCKS5 proxy, which could itself be another alknet instance. This is important for:
|
||||
- Nested tunnel topologies
|
||||
- Environments where iroh needs to go through an existing proxy
|
||||
- Composing transports in flexible ways
|
||||
|
||||
@@ -7,21 +7,21 @@ Accepted
|
||||
The client and server both need configuration (host addresses, keys, transport options, etc.). There are several approaches:
|
||||
|
||||
1. **Read `~/.ssh/config`**: Parse OpenSSH config for default host/key/port. Reduces CLI verbosity for frequent connections.
|
||||
2. **Custom config file**: Wraith-specific config file (TOML/YAML) with host definitions.
|
||||
2. **Custom config file**: Alknet-specific config file (TOML/YAML) with host definitions.
|
||||
3. **Programmatic API only**: Configuration comes from CLI flags or the library API. No file parsing. `~/.ssh/` path conventions are cross-platform trouble (`~` expansion, Windows paths, etc.).
|
||||
4. **Hybrid**: `--config` flag pointing to a wraith-specific config file, but no OpenSSH config parsing.
|
||||
4. **Hybrid**: `--config` flag pointing to a alknet-specific config file, but no OpenSSH config parsing.
|
||||
|
||||
## Decision
|
||||
Option 3: Programmatic-first API. Configuration is provided via:
|
||||
- **CLI**: explicit flags (`--server`, `--identity`, `--transport`, etc.)
|
||||
- **Library API**: `wraith_core::client::ConnectOptions` and `wraith_core::server::ServeOptions` structs, constructable programmatically
|
||||
- **Environment variables**: for a few convenience defaults (e.g., `WRAITH_SERVER`, `WRAITH_IDENTITY`)
|
||||
- **Library API**: `alknet_core::client::ConnectOptions` and `alknet_core::server::ServeOptions` structs, constructable programmatically
|
||||
- **Environment variables**: for a few convenience defaults (e.g., `ALKNET_SERVER`, `ALKNET_IDENTITY`)
|
||||
|
||||
No `~/.ssh/config` parsing, no wraith-specific config files. This approach:
|
||||
No `~/.ssh/config` parsing, no alknet-specific config files. This approach:
|
||||
- Avoids cross-platform path issues (`~` expansion, Windows `USERPROFILE`, etc.)
|
||||
- Makes the library API clean and straightforward for programmatic consumers (NAPI wrapper, pubsub)
|
||||
- Keeps the CLI simple and explicit — no hidden behavior from config files
|
||||
- Matches the design principle that the library crate (`wraith-core`) is the primary interface
|
||||
- Matches the design principle that the library crate (`alknet-core`) is the primary interface
|
||||
|
||||
If users want config-file behavior in the future, it can be added as a separate layer that populates the options structs. But the core doesn't need to know about files.
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ This matches what fail2ban needs: source IP + failure indicator. Our existing fa
|
||||
This ensures that even without fail2ban, the server rejects obviously abusive connections.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: fail2ban can parse wraith logs the same way it parses SSH and nginx logs on our production systems.
|
||||
- **Positive**: fail2ban can parse alknet logs the same way it parses SSH and nginx logs on our production systems.
|
||||
- **Positive**: Built-in rate limiting provides protection on platforms without fail2ban.
|
||||
- **Positive**: No privacy-sensitive data in logs (no tunnel destinations).
|
||||
- **Negative**: Slightly more code in the server for connection tracking per IP.
|
||||
- **Negative**: Users with custom fail2ban filters need to write regex for wraith's log format (documented examples provided).
|
||||
- **Negative**: Users with custom fail2ban filters need to write regex for alknet's log format (documented examples provided).
|
||||
|
||||
## References
|
||||
- [server.md](../server.md)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The original plan included a TUN shim (`wraith-tun`) as Phase 3 — a separate root-requiring process that creates a TUN device and forwards IP packets through wraith's SOCKS5 port. This would provide VPN-like "route all traffic" behavior.
|
||||
The original plan included a TUN shim (`alknet-tun`) as Phase 3 — a separate root-requiring process that creates a TUN device and forwards IP packets through alknet's SOCKS5 port. This would provide VPN-like "route all traffic" behavior.
|
||||
|
||||
However, TUN implementation has significant complexities:
|
||||
- Platform differences (Linux TUN, macOS utun, Windows wintun.dll)
|
||||
@@ -16,21 +16,21 @@ However, TUN implementation has significant complexities:
|
||||
The core SOCKS5 interface already works for the vast majority of use cases. For users who truly need VPN-like "route all traffic" behavior, `tun2proxy` is an existing, well-tested tool that does exactly this: creates a TUN device and routes traffic through a SOCKS5 proxy.
|
||||
|
||||
## Decision
|
||||
Defer TUN implementation entirely. Remove `wraith-tun` from the architecture. Instead:
|
||||
Defer TUN implementation entirely. Remove `alknet-tun` from the architecture. Instead:
|
||||
|
||||
1. **Core interface**: wraith's local SOCKS5 proxy (always available, no root required)
|
||||
2. **VPN-like behavior**: Users who need it run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside `wraith connect`
|
||||
1. **Core interface**: alknet's local SOCKS5 proxy (always available, no root required)
|
||||
2. **VPN-like behavior**: Users who need it run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside `alknet connect`
|
||||
3. **Documentation**: Recommend tun2proxy in the README/wiki for "route all traffic" use cases
|
||||
|
||||
This removes TUN from the project scope while still providing a path to VPN-like behavior. If demand justifies it later, `wraith-tun` can be added as a thin wrapper around tun2proxy's pattern.
|
||||
This removes TUN from the project scope while still providing a path to VPN-like behavior. If demand justifies it later, `alknet-tun` can be added as a thin wrapper around tun2proxy's pattern.
|
||||
|
||||
The `tun` feature flag and `wraith-tun` binary are removed from the architecture. The `tun-rs` dependency is removed.
|
||||
The `tun` feature flag and `alknet-tun` binary are removed from the architecture. The `tun-rs` dependency is removed.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Significantly reduces project scope and complexity. No TUN code to write, test, or maintain across platforms.
|
||||
- **Positive**: tun2proxy is already well-tested for this exact use case.
|
||||
- **Positive**: Core binary remains unprivileged. No root code anywhere in the project.
|
||||
- **Positive**: Cleaner architecture — wraith only does SSH tunneling + SOCKS5. tun2proxy does TUN.
|
||||
- **Positive**: Cleaner architecture — alknet only does SSH tunneling + SOCKS5. tun2proxy does TUN.
|
||||
- **Negative**: Users need two tools instead of one for VPN-like behavior. Mitigated by documentation.
|
||||
- **Negative**: tun2proxy is an external dependency in practice, though it's widely available in package managers.
|
||||
- **Negative**: No first-class Windows/macOS TUN story. tun2proxy handles these platforms but users need to install it separately.
|
||||
|
||||
@@ -4,35 +4,35 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The NAPI wrapper needs to provide TypeScript/Node.js consumers with access to wraith's functionality. The primary use case is `@alkdev/pubsub`'s event target system, which needs both directions:
|
||||
The NAPI wrapper needs to provide TypeScript/Node.js consumers with access to alknet's functionality. The primary use case is `@alkdev/pubsub`'s event target system, which needs both directions:
|
||||
|
||||
1. **connect()**: Establish a client connection to a wraith server. Used by workers/spokes that need to tunnel events through a wraith server.
|
||||
2. **serve()**: Start a wraith server from Node.js. Used by hubs that want to accept wraith connections and route events.
|
||||
1. **connect()**: Establish a client connection to a alknet server. Used by workers/spokes that need to tunnel events through a alknet server.
|
||||
2. **serve()**: Start a alknet server from Node.js. Used by hubs that want to accept alknet connections and route events.
|
||||
|
||||
The previous decision (ADR-007) was to expose only `connect()` for MVP, deferring `serve()`. However, the pubsub integration requires both: a spoke needs `connect()` to reach a hub, and a hub could use `serve()` to accept connections without running a separate `wraith serve` process.
|
||||
The previous decision (ADR-007) was to expose only `connect()` for MVP, deferring `serve()`. However, the pubsub integration requires both: a spoke needs `connect()` to reach a hub, and a hub could use `serve()` to accept connections without running a separate `alknet serve` process.
|
||||
|
||||
More importantly, both `connect()` and `serve()` are fundamental operations of the wraith library. Since the NAPI wrapper is a thin layer over `wraith-core`, exposing both is straightforward — they're just Rust functions behind `#[napi]` attributes.
|
||||
More importantly, both `connect()` and `serve()` are fundamental operations of the alknet library. Since the NAPI wrapper is a thin layer over `alknet-core`, exposing both is straightforward — they're just Rust functions behind `#[napi]` attributes.
|
||||
|
||||
## Decision
|
||||
The NAPI wrapper exposes both `connect()` and `serve()` from the start:
|
||||
|
||||
```typescript
|
||||
// @alkdev/wraith
|
||||
function connect(options: WraithConnectOptions): Promise<Duplex>;
|
||||
function serve(options: WraithServeOptions): Promise<WraithServer>;
|
||||
// @alkdev/alknet
|
||||
function connect(options: AlknetConnectOptions): Promise<Duplex>;
|
||||
function serve(options: AlknetServeOptions): Promise<AlknetServer>;
|
||||
```
|
||||
|
||||
- `connect()` returns a `Duplex` stream (as per ADR-007)
|
||||
- `serve()` returns a `WraithServer` object with a `close()` method and events for new connections
|
||||
- `serve()` returns a `AlknetServer` object with a `close()` method and events for new connections
|
||||
|
||||
The NAPI layer is transport-agnostic — it doesn't know about pubsub's `EventEnvelope`. The pubsub event target adapter wraps the `Duplex` stream to implement `TypedEventTarget`. This separation ensures the NAPI wrapper is reusable for any stream-based protocol, not just pubsub.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Pubsub can use both directions without running a separate binary for the server side.
|
||||
- **Positive**: The NAPI wrapper becomes a complete bridge — any Node.js process can be either a client or server.
|
||||
- **Positive**: Implementation is still minimal — `serve()` is just `wraith_core::server::run()` behind `#[napi]`.
|
||||
- **Negative**: Slightly larger API surface (two functions + `WraithServer` type instead of just `connect()`).
|
||||
- **Negative**: Server-side NAPI needs to handle multiple concurrent connections, which adds complexity to `WraithServer`.
|
||||
- **Positive**: Implementation is still minimal — `serve()` is just `alknet_core::server::run()` behind `#[napi]`.
|
||||
- **Negative**: Slightly larger API surface (two functions + `AlknetServer` type instead of just `connect()`).
|
||||
- **Negative**: Server-side NAPI needs to handle multiple concurrent connections, which adds complexity to `AlknetServer`.
|
||||
|
||||
## References
|
||||
- [napi-and-pubsub.md](../napi-and-pubsub.md)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
When running a wraith server with TLS transport on port 443, the server should be indistinguishable from a regular HTTPS web server to port scanners and deep packet inspection (DPI) systems. This is important for censorship circumvention — if SSH traffic on port 443 is detectable, it can be blocked.
|
||||
When running a alknet server with TLS transport on port 443, the server should be indistinguishable from a regular HTTPS web server to port scanners and deep packet inspection (DPI) systems. This is important for censorship circumvention — if SSH traffic on port 443 is detectable, it can be blocked.
|
||||
|
||||
After the TLS handshake completes, the server sees a raw byte stream. SSH protocol identification starts with `SSH-2.0-`, while HTTP starts with HTTP method verbs (GET, POST, etc.). The server can inspect the first bytes to determine the protocol.
|
||||
|
||||
@@ -20,7 +20,7 @@ This makes the server appear as an nginx web server returning 404 errors to all
|
||||
The fake response uses `Server: nginx` headers to match the most common web server profile.
|
||||
|
||||
## Consequences
|
||||
- **Positive**: TLS+wraith servers on port 443 are indistinguishable from ordinary HTTPS sites to automated scanners.
|
||||
- **Positive**: TLS+alknet servers on port 443 are indistinguishable from ordinary HTTPS sites to automated scanners.
|
||||
- **Positive**: Simple implementation — just peek at the first bytes and branch.
|
||||
- **Positive**: Consistent with censorship circumvention best practices.
|
||||
- **Negative**: Legitimate HTTPS traffic to the same port gets a 404. If the same IP needs to serve real web content, use a reverse proxy (nginx/haproxy) in front that routes by SNI or path.
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The NAPI wrapper and pubsub integration need a way to use wraith's SSH channel as a data plane for event routing. When a `wraith connect` client opens an SSH session to a server, the `direct_tcpip` channel type is used to reach specific TCP targets (host:port).
|
||||
The NAPI wrapper and pubsub integration need a way to use alknet's SSH channel as a data plane for event routing. When a `alknet connect` client opens an SSH session to a server, the `direct_tcpip` channel type is used to reach specific TCP targets (host:port).
|
||||
|
||||
For the pubsub use case, the client needs a dedicated bidirectional stream to the server's event bus — not a TCP connection to a random host. There are several approaches:
|
||||
|
||||
1. **Special destination**: Use `direct_tcpip` with a reserved destination (e.g., `wraith-control:0`) that the server recognizes and routes internally instead of connecting to a TCP target.
|
||||
1. **Special destination**: Use `direct_tcpip` with a reserved destination (e.g., `alknet-control:0`) that the server recognizes and routes internally instead of connecting to a TCP target.
|
||||
2. **Port forwarding**: The server runs a pubsub hub on a specific port (e.g., 9736) and the client uses normal port forwarding (`-L 9736:hub:9736`).
|
||||
3. **Custom channel type**: Define a new SSH channel type beyond `direct_tcpip` and `forwarded_tcpip`.
|
||||
|
||||
## Decision
|
||||
Use approach 1: a reserved `direct_tcpip` destination string. When the server receives a `channel_open_direct_tcpip` request for `wraith-control:0`:
|
||||
Use approach 1: a reserved `direct_tcpip` destination string. When the server receives a `channel_open_direct_tcpip` request for `alknet-control:0`:
|
||||
|
||||
1. The `channel_open_direct_tcpip` handler detects the special target via string matching
|
||||
2. Instead of connecting to a TCP target, it bridges the channel to the local pubsub event bus
|
||||
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
|
||||
|
||||
The destination string `wraith-control` is reserved. Regular TCP targets are hostnames or IP addresses, so there is no collision risk.
|
||||
The destination string `alknet-control` is reserved. Regular TCP targets are hostnames or IP addresses, so there is no collision risk.
|
||||
|
||||
Approach 2 (port forwarding to a specific port) is still supported as an alternative — the client can use `--forward 9736:localhost:9736` if the server runs a pubsub hub on that port. But the control channel approach is simpler and doesn't require a separate listening port.
|
||||
|
||||
@@ -27,11 +27,11 @@ Approach 3 (custom channel type) was rejected because russh's `direct_tcpip` han
|
||||
|
||||
## Consequences
|
||||
- **Positive**: Simple implementation — just string matching in the server's `channel_open_direct_tcpip` handler.
|
||||
- **Positive**: No separate port or service needs to run on the server. The control channel is built into wraith.
|
||||
- **Positive**: No separate port or service needs to run on the server. The control channel is built into alknet.
|
||||
- **Positive**: Compatible with the NAPI wrapper's single-duplex-stream model.
|
||||
- **Positive**: Port forwarding to a specific port is still available as an alternative.
|
||||
- **Negative**: The string `wraith-control` is a magic constant. It should be defined as a constant in the crate.
|
||||
- **Negative**: Regular TCP destinations accidentally matching `wraith-control` would be misrouted. Mitigated by reserving the entire `wraith-` prefix namespace.
|
||||
- **Negative**: The string `alknet-control` is a magic constant. It should be defined as a constant in the crate.
|
||||
- **Negative**: Regular TCP destinations accidentally matching `alknet-control` would be misrouted. Mitigated by reserving the entire `alknet-` prefix namespace.
|
||||
|
||||
## References
|
||||
- [napi-and-pubsub.md](../napi-and-pubsub.md)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The `--proxy` CLI flag appears on both `wraith connect` (client) and `wraith serve` (server), but the two sides proxy fundamentally different things:
|
||||
The `--proxy` CLI flag appears on both `alknet connect` (client) and `alknet 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.
|
||||
- **Client**: `--proxy` routes the *transport connection* through the proxy. For example, `alknet 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.
|
||||
- **Server**: `--proxy` routes *outbound target connections* through the proxy. For example, `alknet 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.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Wraith currently authenticates connections exclusively through SSH public key
|
||||
Alknet currently authenticates connections exclusively through SSH public key
|
||||
auth in the SSH handshake. This works for SSH-over-any-transport (TCP, TLS,
|
||||
iroh) because SSH carries its own auth protocol. But WebTransport and other
|
||||
HTTP-level transports cannot perform SSH key exchange — browsers speak HTTP/3,
|
||||
@@ -17,8 +17,8 @@ identity system (API keys, JWTs, session tokens). This creates two problems:
|
||||
same person connecting via SSH and WebTransport appears as two different
|
||||
identities.
|
||||
|
||||
The `IdentityProvider` trait is needed to decouple wraith-core from any
|
||||
specific identity storage (config file vs. database). Without it, wraith-core
|
||||
The `IdentityProvider` trait is needed to decouple alknet-core from any
|
||||
specific identity storage (config file vs. database). Without it, alknet-core
|
||||
would either hardcode config-file-based auth or take a database dependency —
|
||||
neither is acceptable for a library crate.
|
||||
|
||||
@@ -42,7 +42,7 @@ AuthToken = base64url(key_id || timestamp || signature)
|
||||
Server extracts the fingerprint, looks it up in the same `authorized_keys`
|
||||
set, verifies the signature, and checks the timestamp window (default ±300s).
|
||||
|
||||
**`IdentityProvider` trait**: Decouples wraith-core from identity storage. The
|
||||
**`IdentityProvider` trait**: Decouples alknet-core from identity storage. The
|
||||
trait resolves a fingerprint or token to an `Identity`. Default implementation
|
||||
loads from `DynamicConfig.auth` (no database). Hub implementation can back it
|
||||
with `@alkdev/storage`.
|
||||
@@ -60,7 +60,7 @@ the key material.
|
||||
- **Positive**: One key set, one rotation, one `reloadAuth()` call. Adding a
|
||||
key to `authorized_keys` immediately grants access via both SSH and
|
||||
WebTransport.
|
||||
- **Positive**: `IdentityProvider` trait makes wraith-core independent of any
|
||||
- **Positive**: `IdentityProvider` trait makes alknet-core independent of any
|
||||
specific database. Default: config file. Hub: `@alkdev/storage`.
|
||||
- **Positive**: Browser clients can authenticate using Ed25519 keys via
|
||||
SubtleCrypto (Chrome 105+, Firefox 130+, Safari 17+). Deno supports it
|
||||
|
||||
@@ -5,7 +5,7 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The wraith control channel (ADR-018) routes from client → server's event bus.
|
||||
The alknet control channel (ADR-018) routes from client → server's event bus.
|
||||
This is unidirectional: clients can send events to the server, but the server
|
||||
cannot call operations on the client. In the hub/spoke model, spokes (dev env
|
||||
containers) connect to a hub and expose operations (fs, bash, search) that the
|
||||
@@ -35,7 +35,7 @@ Core-provided operations use short paths without a spoke prefix
|
||||
(`/services/list`, `/services/schema`). Spoke operations are prefixed
|
||||
(`/dev1/fs/readFile`).
|
||||
|
||||
This generalizes ADR-018's control channel: the `wraith-*` destination becomes
|
||||
This generalizes ADR-018's control channel: the `alknet-*` destination becomes
|
||||
a transport for `EventEnvelope` frames with call protocol semantics, instead of
|
||||
raw pubsub dispatch.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The current control channel (ADR-018) is hardcoded: `wraith-control:0` bridges
|
||||
The current control channel (ADR-018) is hardcoded: `alknet-control:0` bridges
|
||||
to the local pubsub event bus. If NAPI wants to expose `fs.readFile` or
|
||||
`bash.exec` as callable operations, it has no way to register these with core's
|
||||
channel routing. The NAPI handler would need to intercept channel data outside
|
||||
|
||||
@@ -7,14 +7,14 @@ last_updated: 2026-06-02
|
||||
|
||||
## What
|
||||
|
||||
Two integration layers that enable TypeScript/JavaScript consumers to use wraith as a transport:
|
||||
Two integration layers that enable TypeScript/JavaScript consumers to use alknet as a transport:
|
||||
|
||||
1. **NAPI wrapper** (`@alkdev/wraith`) — A Node.js native addon (via napi-rs) exposing `connect()` and `serve()` that return duplex streams
|
||||
2. **PubSub event target** (`@alkdev/pubsub` adapter) — An implementation of the `TypedEventTarget` interface that routes events over wraith's SSH channel
|
||||
1. **NAPI wrapper** (`@alkdev/alknet`) — A Node.js native addon (via napi-rs) exposing `connect()` and `serve()` that return duplex streams
|
||||
2. **PubSub event target** (`@alkdev/pubsub` adapter) — An implementation of the `TypedEventTarget` interface that routes events over alknet's SSH channel
|
||||
|
||||
## Why
|
||||
|
||||
The wraith Rust binary serves CLI users. But the broader ecosystem (pubsub, operations, agent spokes) is TypeScript-first. These integration layers let TypeScript code use wraith's transport without reimplementing SSH.
|
||||
The alknet Rust binary serves CLI users. But the broader ecosystem (pubsub, operations, agent spokes) is TypeScript-first. These integration layers let TypeScript code use alknet's transport without reimplementing SSH.
|
||||
|
||||
The NAPI surface is intentionally minimal — it exposes transport connections as duplex streams, not the full SSH protocol. The pubsub adapter wraps those streams with `EventEnvelope` serialization.
|
||||
|
||||
@@ -25,9 +25,9 @@ The NAPI surface is intentionally minimal — it exposes transport connections a
|
||||
The wrapper uses napi-rs (ADR-015) and exposes two functions (ADR-016):
|
||||
|
||||
```typescript
|
||||
// @alkdev/wraith (TypeScript side)
|
||||
// @alkdev/alknet (TypeScript side)
|
||||
|
||||
interface WraithConnectOptions {
|
||||
interface AlknetConnectOptions {
|
||||
// TCP/TLS mode
|
||||
server?: string; // e.g., "example.com:443"
|
||||
// iroh mode
|
||||
@@ -45,7 +45,7 @@ interface WraithConnectOptions {
|
||||
proxy?: string; // upstream SOCKS5/HTTP proxy URL
|
||||
}
|
||||
|
||||
interface WraithServeOptions {
|
||||
interface AlknetServeOptions {
|
||||
// Transport
|
||||
transport: 'tcp' | 'tls' | 'iroh';
|
||||
// Auth
|
||||
@@ -63,12 +63,12 @@ interface WraithServeOptions {
|
||||
}
|
||||
|
||||
// Returns a Duplex stream for the SSH channel
|
||||
function connect(options: WraithConnectOptions): Promise<Duplex>;
|
||||
function connect(options: AlknetConnectOptions): Promise<Duplex>;
|
||||
|
||||
// Returns a server object with close() and connection events
|
||||
function serve(options: WraithServeOptions): Promise<WraithServer>;
|
||||
function serve(options: AlknetServeOptions): Promise<AlknetServer>;
|
||||
|
||||
interface WraithServer {
|
||||
interface AlknetServer {
|
||||
close(): Promise<void>;
|
||||
onConnection(callback: (stream: Duplex, info: ConnectionInfo) => void): void;
|
||||
}
|
||||
@@ -76,18 +76,18 @@ 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`
|
||||
### NAPI `connect()` vs CLI `alknet connect`
|
||||
|
||||
The NAPI `connect()` function and the CLI `wraith connect` command are fundamentally different operations despite sharing the same name:
|
||||
The NAPI `connect()` function and the CLI `alknet 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.
|
||||
- **CLI `alknet 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.
|
||||
For SOCKS5 proxy functionality, use the CLI binary (`alknet 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.
|
||||
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 (`ALKNET_SERVER`, `ALKNET_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.
|
||||
|
||||
@@ -96,20 +96,20 @@ Key material provided as `Buffer` must be in **OpenSSH key format** (the format
|
||||
This implements `TypedEventTarget` from `@alkdev/pubsub`:
|
||||
|
||||
```typescript
|
||||
// @alkdev/pubsub (new adapter: event-target-wraith.ts)
|
||||
// @alkdev/pubsub (new adapter: event-target-alknet.ts)
|
||||
|
||||
export interface WraithEventTargetOptions {
|
||||
stream: Duplex; // from @alkdev/wraith.connect() or serve()
|
||||
export interface AlknetEventTargetOptions {
|
||||
stream: Duplex; // from @alkdev/alknet.connect() or serve()
|
||||
}
|
||||
|
||||
export interface WraithEventTarget<TEvent extends TypedEvent>
|
||||
export interface AlknetEventTarget<TEvent extends TypedEvent>
|
||||
extends TypedEventTarget<TEvent> {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export function createWraithEventTarget<TEvent extends TypedEvent>(
|
||||
options: WraithEventTargetOptions
|
||||
): WraithEventTarget<TEvent>;
|
||||
export function createAlknetEventTarget<TEvent extends TypedEvent>(
|
||||
options: AlknetEventTargetOptions
|
||||
): AlknetEventTarget<TEvent>;
|
||||
```
|
||||
|
||||
Wire protocol (same as other pubsub adapters):
|
||||
@@ -121,20 +121,20 @@ Wire protocol (same as other pubsub adapters):
|
||||
|
||||
### On the Server Side
|
||||
|
||||
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:
|
||||
The alknet server uses a reserved `direct_tcpip` destination (`alknet-control:0`) for the pubsub control channel (ADR-018). When a client connects to this destination:
|
||||
|
||||
1. The server's `channel_open_direct_ip` handler detects the reserved `wraith-control` target
|
||||
1. The server's `channel_open_direct_ip` handler detects the reserved `alknet-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
|
||||
|
||||
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.
|
||||
Users who prefer not to use the control channel can alternatively run a pubsub hub on a specific port and use standard port forwarding: `alknet connect --forward 9736:hub:9736`. This is a deployment choice, not a separate implementation — alknet's port forwarding works normally for any TCP service.
|
||||
|
||||
### Direction Agnostic
|
||||
|
||||
Because wraith supports both local and remote port forwarding, the event target works in either direction:
|
||||
Because alknet supports both local and remote port forwarding, the event target works in either direction:
|
||||
|
||||
- **Worker connects to hub**: `wraith connect --forward 9736:hub:9736` then create WebSocket event target pointing at `ws://localhost:9736`
|
||||
- **Hub connects to worker**: `wraith connect --remote-forward 9736:worker:9736` — same result, opposite initiator
|
||||
- **Worker connects to hub**: `alknet connect --forward 9736:hub:9736` then create WebSocket event target pointing at `ws://localhost:9736`
|
||||
- **Hub connects to worker**: `alknet connect --remote-forward 9736:worker:9736` — same result, opposite initiator
|
||||
|
||||
The pubsub adapter doesn't care which side initiated the SSH session. It just needs a byte stream.
|
||||
|
||||
@@ -157,4 +157,4 @@ None — all resolved.
|
||||
| [011](decisions/011-no-ssh-config-programmatic-api.md) | Programmatic-first API | No file-based config; options are structs or env vars |
|
||||
| [015](decisions/015-napi-rs-for-ffi-bridge.md) | napi-rs for FFI | Standard Node.js native addon tooling |
|
||||
| [016](decisions/016-napi-expose-connect-and-serve.md) | Both connect() and serve() | NAPI exposes client and server sides from the start |
|
||||
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel for pubsub | Reserved `wraith-control` destination for event bus |
|
||||
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel for pubsub | Reserved `alknet-control` destination for event bus |
|
||||
@@ -66,14 +66,14 @@ last_updated: 2026-06-04
|
||||
- **Origin**: [tun-shim.md](tun-shim.md)
|
||||
- **Status**: ~~resolved~~
|
||||
- **Priority**: ~~low~~ —
|
||||
- **Resolution**: ADR-014 — TUN is deferred entirely from the wraith project. For VPN-like behavior, users run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside wraith. This eliminates all TUN-related scope questions (Windows, TCP reconstruction, etc.).
|
||||
- **Resolution**: ADR-014 — TUN is deferred entirely from the alknet project. For VPN-like behavior, users run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside alknet. This eliminates all TUN-related scope questions (Windows, TCP reconstruction, etc.).
|
||||
- **Cross-references**: [ADR-014](decisions/014-defer-tun-recommend-socks5-proxy.md)
|
||||
|
||||
### OQ-09: TCP reconstruction approach for TUN
|
||||
- **Origin**: [tun-shim.md](tun-shim.md)
|
||||
- **Status**: ~~resolved~~
|
||||
- **Priority**: ~~medium~~ —
|
||||
- **Resolution**: ADR-014 — TUN is deferred from wraith. tun2proxy (external tool) handles this if users need VPN-like behavior.
|
||||
- **Resolution**: ADR-014 — TUN is deferred from alknet. tun2proxy (external tool) handles this if users need VPN-like behavior.
|
||||
- **Cross-references**: [ADR-014](decisions/014-defer-tun-recommend-socks5-proxy.md)
|
||||
|
||||
## NAPI / PubSub
|
||||
@@ -122,7 +122,7 @@ last_updated: 2026-06-04
|
||||
- **Resolution**: (pending — needs R&D in WebTransport transport session)
|
||||
- **Cross-references**: [auth.md](auth.md), OQ-19
|
||||
|
||||
### OQ-16: Transport-specific forwarding policy (e.g., WebTransport clients restricted to wraith-* channels)
|
||||
### OQ-16: Transport-specific forwarding policy (e.g., WebTransport clients restricted to alknet-* channels)
|
||||
- **Origin**: [research/configuration.md](../research/configuration.md)
|
||||
- **Status**: open
|
||||
- **Priority**: low
|
||||
@@ -133,7 +133,7 @@ last_updated: 2026-06-04
|
||||
- **Origin**: [research/configuration.md](../research/configuration.md)
|
||||
- **Status**: ~~resolved~~
|
||||
- **Priority**: ~~medium~~ —
|
||||
- **Resolution**: ADR-023 — Unified auth with shared key material. SSH transports use SSH pubkey auth. Non-SSH transports (WebTransport) use Ed25519-signed timestamp tokens. Both verify against the same `authorized_keys` set. The presentation differs per transport, but the identity is unified. `AuthPolicy` holds both `SshAuthConfig` and `TokenAuthConfig`, with `TokenKeySource::Shared` as the default (same keys for both paths). `IdentityProvider` trait decouples wraith-core from identity storage.
|
||||
- **Resolution**: ADR-023 — Unified auth with shared key material. SSH transports use SSH pubkey auth. Non-SSH transports (WebTransport) use Ed25519-signed timestamp tokens. Both verify against the same `authorized_keys` set. The presentation differs per transport, but the identity is unified. `AuthPolicy` holds both `SshAuthConfig` and `TokenAuthConfig`, with `TokenKeySource::Shared` as the default (same keys for both paths). `IdentityProvider` trait decouples alknet-core from identity storage.
|
||||
- **Cross-references**: [ADR-023](decisions/023-unified-auth-shared-key-material.md), [auth.md](auth.md), OQ-15
|
||||
|
||||
## Auth
|
||||
|
||||
@@ -3,33 +3,33 @@ status: reviewed
|
||||
last_updated: 2026-06-02
|
||||
---
|
||||
|
||||
# Wraith Overview
|
||||
# Alknet Overview
|
||||
|
||||
## Purpose
|
||||
|
||||
Wraith is a self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol. It enables:
|
||||
Alknet 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.
|
||||
The core insight: SSH tunnels work because SSH is fundamental infrastructure. Blocking it breaks the internet. Alknet makes SSH tunneling accessible through a simple CLI with pluggable transports.
|
||||
|
||||
## Exports
|
||||
|
||||
### Binary: `wraith`
|
||||
### Binary: `alknet`
|
||||
|
||||
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)
|
||||
alknet serve — Start the server (accepts SSH connections)
|
||||
alknet connect — Start the client (opens SSH session, exposes SOCKS5/port-forwards)
|
||||
```
|
||||
|
||||
### Library: `wraith-core`
|
||||
### Library: `alknet-core`
|
||||
|
||||
The `wraith-core` crate exports the pluggable components for embedding or programmatic use:
|
||||
The `alknet-core` crate exports the pluggable components for embedding or programmatic use:
|
||||
|
||||
- `Transport` trait — produces a duplex stream for SSH to run over
|
||||
- `TcpTransport` — direct TCP connection
|
||||
@@ -60,7 +60,7 @@ The `wraith-core` crate exports the pluggable components for embedding or progra
|
||||
|
||||
1. **SSH runs over transport, not alongside** — The transport layer produces a single `AsyncRead+AsyncWrite+Unpin+Send` stream. SSH runs over that stream via `russh::client::connect_stream()` / `russh::server::run_stream()`. The SSH layer never knows what transport it's on. (ADR-001, ADR-004)
|
||||
|
||||
2. **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 `tun2proxy` alongside wraith's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014)
|
||||
2. **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 `tun2proxy` alongside alknet's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014)
|
||||
|
||||
3. **No logging of tunnel destinations** — The server logs auth attempts and connections (for fail2ban) but does not log `channel_open_direct_tcpip` destinations, DNS lookups, or bytes transferred. (ADR-006, ADR-013)
|
||||
|
||||
@@ -91,11 +91,11 @@ The `wraith-core` crate exports the pluggable components for embedding or progra
|
||||
| [011](decisions/011-no-ssh-config-programmatic-api.md) | Programmatic-first | No file-based config; options are structs, env vars, CLI flags |
|
||||
| [012](decisions/012-auth-ed25519-and-cert-authority.md) | Key + cert-authority | Ed25519 keys + OpenSSH CA; no password auth |
|
||||
| [013](decisions/013-fail2ban-friendly-logging.md) | Fail2ban-friendly | Structured auth logs + built-in rate limiting |
|
||||
| [014](decisions/014-defer-tun-recommend-socks5-proxy.md) | Defer TUN | Use tun2proxy for VPN-like behavior; no wraith-tun binary |
|
||||
| [014](decisions/014-defer-tun-recommend-socks5-proxy.md) | Defer TUN | Use tun2proxy for VPN-like behavior; no alknet-tun binary |
|
||||
| [015](decisions/015-napi-rs-for-ffi-bridge.md) | napi-rs | Standard Node.js native addon tooling |
|
||||
| [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 |
|
||||
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `alknet-control` destination for pubsub |
|
||||
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -7,7 +7,7 @@ last_updated: 2026-06-02
|
||||
|
||||
## What
|
||||
|
||||
The wraith server accepts SSH connections (via pluggable transport) and handles `channel_open_direct_tcpip` requests by connecting to the requested target — either directly or through an outbound proxy.
|
||||
The alknet server accepts SSH connections (via pluggable transport) and handles `channel_open_direct_tcpip` requests by connecting to the requested target — either directly or through an outbound proxy.
|
||||
|
||||
## Why
|
||||
|
||||
@@ -19,7 +19,7 @@ The server is the tunnel endpoint. It receives SSH channels requesting TCP conne
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ wraith serve │
|
||||
│ alknet serve │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ SSH Server (russh) │ │
|
||||
@@ -85,7 +85,7 @@ 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).
|
||||
**Reserved destination** — If `host` starts with `alknet-` (e.g., `alknet-control`), the server routes the channel internally instead of connecting to a TCP target. The primary reserved destination is `alknet-control:0`, which bridges the channel to the local pubsub event bus (ADR-018).
|
||||
|
||||
**Regular destination** — For all other targets:
|
||||
|
||||
@@ -128,7 +128,7 @@ The server handler implements `russh::server::Handler` with two primary responsi
|
||||
- Return `Accept` or `Reject`
|
||||
|
||||
**Channel handling (`channel_open_direct_tcpip`)**:
|
||||
- If the destination host starts with `wraith-`, route internally (control channel, ADR-018)
|
||||
- If the destination host starts with `alknet-`, 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
|
||||
@@ -161,44 +161,44 @@ These provide abuse protection on platforms without fail2ban (macOS, Windows, BS
|
||||
|
||||
```bash
|
||||
# Basic server (SSH on port 22)
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key
|
||||
|
||||
# With TLS (manual certs)
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--transport tls \
|
||||
--tls-cert /etc/ssl/cert.pem \
|
||||
--tls-key /etc/ssl/key.pem
|
||||
|
||||
# With TLS (auto ACME, domain-based)
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--transport tls \
|
||||
--acme-domain example.com
|
||||
|
||||
# With TLS + stealth (fake nginx 404 to scanners)
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--transport tls \
|
||||
--acme-domain example.com \
|
||||
--stealth
|
||||
|
||||
# With iroh transport (no public IP needed)
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--transport iroh
|
||||
|
||||
# With outbound proxy
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--proxy socks5://127.0.0.1:9050
|
||||
|
||||
# With certificate authority authentication
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--cert-authority /etc/wraith/ca.pub
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--cert-authority /etc/alknet/ca.pub
|
||||
|
||||
# With rate limiting
|
||||
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
|
||||
--max-connections-per-ip 5 \
|
||||
--max-auth-attempts 3
|
||||
|
||||
# All options
|
||||
wraith serve \
|
||||
alknet serve \
|
||||
--key <path-or-buffer> \ # SSH host key (required)
|
||||
--authorized-keys <path> \ # Authorized keys file
|
||||
--cert-authority <path> \ # CA public key for cert-auth
|
||||
@@ -218,7 +218,7 @@ wraith serve \
|
||||
|
||||
When running with `--transport iroh`, the server:
|
||||
|
||||
1. Creates an iroh endpoint with ALPN value `b"wraith-ssh"`
|
||||
1. Creates an iroh endpoint with ALPN value `b"alknet-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()`
|
||||
@@ -228,7 +228,7 @@ No listening port is needed. The server connects outbound to the iroh relay (def
|
||||
## 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.
|
||||
- Destination strings beginning with `alknet-` are reserved for internal use (ADR-018). The server must not attempt TCP connections to `alknet-*` 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.
|
||||
@@ -271,5 +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 |
|
||||
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `alknet-control` destination for pubsub |
|
||||
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |
|
||||
@@ -92,7 +92,7 @@ See ADR-009 for the decision to default to n0's relay with override.
|
||||
Transports can be nested. The CLI supports `--transport iroh --proxy socks5://...` natively (ADR-010):
|
||||
|
||||
```bash
|
||||
wraith connect --transport iroh --proxy socks5://127.0.0.1:1080
|
||||
alknet connect --transport iroh --proxy socks5://127.0.0.1:1080
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -5,21 +5,21 @@ last_updated: 2026-06-01
|
||||
|
||||
# TUN Shim (Deprecated)
|
||||
|
||||
> **Note**: TUN functionality has been deferred from the wraith project. For VPN-like "route all traffic" behavior, use `tun2proxy` alongside wraith's SOCKS5 proxy. See ADR-014 for the rationale.
|
||||
> **Note**: TUN functionality has been deferred from the alknet project. For VPN-like "route all traffic" behavior, use `tun2proxy` alongside alknet's SOCKS5 proxy. See ADR-014 for the rationale.
|
||||
|
||||
## What Changed
|
||||
|
||||
The `wraith-tun` separate process and all TUN-related code is out of scope. The recommended approach for VPN-like behavior is:
|
||||
The `alknet-tun` separate process and all TUN-related code is out of scope. The recommended approach for VPN-like behavior is:
|
||||
|
||||
```bash
|
||||
# Terminal 1: wraith SOCKS5 proxy (no root required)
|
||||
wraith connect --server example.com --identity ~/.ssh/id_ed25519
|
||||
# Terminal 1: alknet SOCKS5 proxy (no root required)
|
||||
alknet connect --server example.com --identity ~/.ssh/id_ed25519
|
||||
|
||||
# Terminal 2: tun2proxy routes all traffic through wraith's SOCKS5
|
||||
# Terminal 2: tun2proxy routes all traffic through alknet's SOCKS5
|
||||
sudo tun2proxy --proxy socks5://127.0.0.1:1080
|
||||
```
|
||||
|
||||
This keeps the core wraith binary free of TUN complexity and leverages an existing, well-tested tool for TUN-to-SOCKS5 bridging.
|
||||
This keeps the core alknet binary free of TUN complexity and leverages an existing, well-tested tool for TUN-to-SOCKS5 bridging.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -8,33 +8,33 @@ phase: exploration
|
||||
|
||||
## Problem
|
||||
|
||||
Wraith's configuration is loaded once at startup and never changes. This has
|
||||
Alknet's configuration is loaded once at startup and never changes. This has
|
||||
three specific failures:
|
||||
|
||||
1. **No hot reload of authentication credentials.** Adding or removing an
|
||||
authorized key requires restarting the server process. In a hub/spoke
|
||||
deployment where keys are managed via a database (see
|
||||
`@alkdev/storage`'s `peer_credentials` table), the wraith process must be
|
||||
`@alkdev/storage`'s `peer_credentials` table), the alknet process must be
|
||||
restarted every time a key is added, revoked, or rotated. This is
|
||||
operationally unacceptable for a production service.
|
||||
|
||||
2. **No port forwarding access control.** Any authenticated client can open a
|
||||
`direct-tcpip` channel to any destination. There is no policy governing
|
||||
which hosts, ports, or `wraith-*` control channels a client may access. This
|
||||
which hosts, ports, or `alknet-*` control channels a client may access. This
|
||||
is a security gap — a compromised key grants unrestricted network access
|
||||
through the tunnel.
|
||||
|
||||
3. **No structured configuration beyond CLI flags.** ADR-011 chose
|
||||
programmatic-first configuration for the alpha. This was correct — it
|
||||
avoided cross-platform path issues and kept the API surface small. But as
|
||||
wraith moves toward publishable releases, operators need config files for
|
||||
alknet moves toward publishable releases, operators need config files for
|
||||
reproducible deployments, and the NAPI layer needs programmatic reload
|
||||
capability that the current `ServeOptions` builder pattern doesn't support.
|
||||
|
||||
### What's Not The Problem
|
||||
|
||||
- This does not propose depending on Honker, SQLite, or any specific data
|
||||
source at the `wraith-core` level. The core provides a reload mechanism;
|
||||
source at the `alknet-core` level. The core provides a reload mechanism;
|
||||
data sources plug in from outside.
|
||||
- This does not propose file-watching (potential attack vector, unnecessary
|
||||
complexity). CLI usage loads config once at startup. Programmatic usage
|
||||
@@ -131,7 +131,7 @@ pub enum TargetPattern {
|
||||
Host(String),
|
||||
Cidr(IpNetwork),
|
||||
PortRange(String, Range<u16>),
|
||||
WraithPrefix,
|
||||
AlknetPrefix,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -169,14 +169,14 @@ max_connections_per_ip = 5
|
||||
max_auth_attempts = 3
|
||||
|
||||
[server.tls]
|
||||
cert = "/etc/wraith/tls/cert.pem"
|
||||
key = "/etc/wraith/tls/key.pem"
|
||||
cert = "/etc/alknet/tls/cert.pem"
|
||||
key = "/etc/alknet/tls/key.pem"
|
||||
|
||||
[server.iroh]
|
||||
relay = "https://relay.alk.dev"
|
||||
|
||||
[auth]
|
||||
host_key = "/etc/wraith/ssh/host_key"
|
||||
host_key = "/etc/alknet/ssh/host_key"
|
||||
|
||||
[forwarding]
|
||||
default = "deny"
|
||||
@@ -186,7 +186,7 @@ target = "localhost:*"
|
||||
action = "allow"
|
||||
|
||||
[[forwarding.rules]]
|
||||
target = "wraith-*"
|
||||
target = "alknet-*"
|
||||
action = "allow"
|
||||
|
||||
[[forwarding.rules]]
|
||||
@@ -202,7 +202,7 @@ Rules are evaluated in order; first match wins.
|
||||
The NAPI layer exposes the reload handle:
|
||||
|
||||
```typescript
|
||||
interface WraithServer {
|
||||
interface AlknetServer {
|
||||
reloadAuth(auth: { authorizedKeys?: Buffer, certAuthority?: Buffer }): void;
|
||||
reloadForwarding(policy: ForwardingPolicyConfig): void;
|
||||
reloadAll(config: DynamicConfig): void;
|
||||
@@ -214,7 +214,7 @@ interface ForwardingPolicyConfig {
|
||||
}
|
||||
|
||||
interface ForwardingRuleConfig {
|
||||
target: string; // "localhost:*", "10.0.0.0/8:80", "wraith-*"
|
||||
target: string; // "localhost:*", "10.0.0.0/8:80", "alknet-*"
|
||||
action: 'allow' | 'deny';
|
||||
principals?: string[]; // default ["*"]
|
||||
}
|
||||
@@ -255,7 +255,7 @@ This is a convenience layer on top of `ConnectOptions`, not a replacement.
|
||||
| Core Rust | `StaticConfig` struct | `ArcSwap<DynamicConfig>` | `ConfigReloadHandle::reload()` |
|
||||
| NAPI | `serve()` options | Same `ArcSwap` | `server.reloadAuth()`, `server.reloadForwarding()` |
|
||||
|
||||
The CLI doesn't need a reload mechanism. When you're running wraith from the
|
||||
The CLI doesn't need a reload mechanism. When you're running alknet from the
|
||||
command line, restarting is fine. The reload mechanism exists for programmatic
|
||||
consumers that manage credentials in a database.
|
||||
|
||||
@@ -337,8 +337,8 @@ listen = "0.0.0.0:443"
|
||||
stealth = true
|
||||
|
||||
[listeners.tls]
|
||||
cert = "/etc/wraith/tls/cert.pem"
|
||||
key = "/etc/wraith/tls/key.pem"
|
||||
cert = "/etc/alknet/tls/cert.pem"
|
||||
key = "/etc/alknet/tls/key.pem"
|
||||
|
||||
[[listeners]]
|
||||
transport = "tcp"
|
||||
@@ -354,8 +354,8 @@ listen = "0.0.0.0:443"
|
||||
# WebTransport shares port 443 with TLS because QUIC is UDP, TLS is TCP
|
||||
|
||||
[listeners.webtransport]
|
||||
cert = "/etc/wraith/tls/cert.pem"
|
||||
key = "/etc/wraith/tls/key.pem"
|
||||
cert = "/etc/alknet/tls/cert.pem"
|
||||
key = "/etc/alknet/tls/key.pem"
|
||||
```
|
||||
|
||||
The `[[listeners]]` array-of-tables pattern means each listener is an
|
||||
@@ -376,7 +376,7 @@ const server = await serve({
|
||||
});
|
||||
```
|
||||
|
||||
Single `WraithServer` object, single `reloadAuth()` call affects all
|
||||
Single `AlknetServer` object, single `reloadAuth()` call affects all
|
||||
listeners.
|
||||
|
||||
### Transport Kind and WebTransport
|
||||
@@ -386,7 +386,7 @@ so the handler can behave differently per transport. Adding `WebTransport` to
|
||||
this enum is straightforward — WebTransport connections are identifiable at
|
||||
accept time. The handler behavior is the same (port forwarding only), but
|
||||
the tag enables transport-specific logging and future policy differences
|
||||
(e.g., WebTransport clients can only access `wraith-*` control channels).
|
||||
(e.g., WebTransport clients can only access `alknet-*` control channels).
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
@@ -396,9 +396,9 @@ the tag enables transport-specific logging and future policy differences
|
||||
2. Replace `Arc<ServerAuthConfig>` in `ServerHandler` with
|
||||
`Arc<ArcSwap<DynamicConfig>>`
|
||||
3. Add `ConfigReloadHandle` with `reload(DynamicConfig)` method
|
||||
4. Expose `reloadAuth()` on the NAPI `WraithServer` object
|
||||
4. Expose `reloadAuth()` on the NAPI `AlknetServer` object
|
||||
|
||||
**Scope**: `wraith-core` auth module + `wraith-napi` serve module
|
||||
**Scope**: `alknet-core` auth module + `alknet-napi` serve module
|
||||
|
||||
**Risk**: Low — internal refactor, no protocol changes
|
||||
|
||||
@@ -406,9 +406,9 @@ the tag enables transport-specific logging and future policy differences
|
||||
|
||||
1. Add `ForwardingPolicy` to `DynamicConfig`
|
||||
2. Add policy check to `channel_open_direct_tcpip` before proxy spawn
|
||||
3. Expose `reloadForwarding()` on NAPI `WraithServer`
|
||||
3. Expose `reloadForwarding()` on NAPI `AlknetServer`
|
||||
|
||||
**Scope**: `wraith-core` handler + `wraith-napi`
|
||||
**Scope**: `alknet-core` handler + `alknet-napi`
|
||||
|
||||
**Risk**: Low — new check, default-allow preserves current behavior
|
||||
|
||||
@@ -419,7 +419,7 @@ the tag enables transport-specific logging and future policy differences
|
||||
3. Config file only covers static config + initial auth config path
|
||||
4. Add `serde` derive to `StaticConfig`
|
||||
|
||||
**Scope**: `wraith-cli` (new binary crate) + `wraith-core` config module
|
||||
**Scope**: `alknet-cli` (new binary crate) + `alknet-core` config module
|
||||
|
||||
**Risk**: Medium — new dependency (`toml` crate), new CLI surface to validate
|
||||
|
||||
@@ -429,7 +429,7 @@ the tag enables transport-specific logging and future policy differences
|
||||
2. `--profile production` loads named profile
|
||||
3. CLI flags override profile values
|
||||
|
||||
**Scope**: `wraith-cli`
|
||||
**Scope**: `alknet-cli`
|
||||
|
||||
**Risk**: Low — convenience layer only
|
||||
|
||||
@@ -443,7 +443,7 @@ the tag enables transport-specific logging and future policy differences
|
||||
6. Add `WebTransport` to `TransportKind` enum (initially as a tag only;
|
||||
actual WebTransport acceptor is a separate R&D phase)
|
||||
|
||||
**Scope**: `wraith-core` serve.rs + `wraith-napi` + `wraith-cli`
|
||||
**Scope**: `alknet-core` serve.rs + `alknet-napi` + `alknet-cli`
|
||||
|
||||
**Risk**: Medium — changes the primary API surface of `serve()`. Backwards
|
||||
compat via accepting both `transport: string` (single) and
|
||||
@@ -511,7 +511,7 @@ compat via accepting both `transport: string` (single) and
|
||||
Initially no — all transports get the same port-forwarding-only handler.
|
||||
But WebTransport connections come from browsers, which have different trust
|
||||
assumptions. A future forwarding policy might restrict WebTransport clients
|
||||
to `wraith-*` control channels only (no arbitrary host:port forwarding).
|
||||
to `alknet-*` control channels only (no arbitrary host:port forwarding).
|
||||
This is a policy question, not a transport question. The `TransportKind` tag
|
||||
on the handler enables transport-aware policy rules in `ForwardingPolicy`
|
||||
without changing the handler. Defer to Phase 2 (forwarding policy design).
|
||||
|
||||
Reference in New Issue
Block a user