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:
2026-06-05 10:04:32 +00:00
parent af7f4d0006
commit 596c89ce24
101 changed files with 552 additions and 552 deletions

View File

@@ -191,7 +191,7 @@ also include:
Example prompt template: Example prompt template:
``` ```
You are an implementation specialist for the @alkdev/wraith project. You are an implementation specialist for the @alkdev/alknet project.
Your task: {{task}} Your task: {{task}}
@@ -204,7 +204,7 @@ Your task: {{task}}
7. Push: git push origin $(git branch --show-current) 7. Push: git push origin $(git branch --show-current)
8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. <brief summary>", level: "info"}}) 8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. <brief summary>", level: "info"}})
Key project constraints (@alkdev/wraith): Key project constraints (@alkdev/alknet):
- Rust: use cargo build, cargo clippy, cargo fmt, cargo test - Rust: use cargo build, cargo clippy, cargo fmt, cargo test
- No comments in code - No comments in code
- anyhow::Result for application errors, thiserror for library error types - anyhow::Result for application errors, thiserror for library error types

160
Cargo.lock generated
View File

@@ -53,6 +53,67 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "alknet"
version = "0.1.0"
dependencies = [
"alknet-core",
"anyhow",
"clap",
"iroh",
"rustls",
"rustls-acme",
"rustls-pemfile",
"rustls-pki-types",
"tokio",
"url",
]
[[package]]
name = "alknet-core"
version = "0.1.0"
dependencies = [
"alknet-core",
"anyhow",
"async-trait",
"futures",
"ipnetwork",
"iroh",
"rand 0.10.1",
"rand_core 0.6.4",
"rcgen 0.14.8",
"russh",
"rustls",
"rustls-acme",
"rustls-pki-types",
"ssh-key",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-rustls",
"tokio-util",
"tracing",
"url",
"webpki-roots 0.26.11",
]
[[package]]
name = "alknet-napi"
version = "0.1.0"
dependencies = [
"alknet-core",
"async-trait",
"iroh",
"napi",
"napi-derive",
"russh",
"rustls-pemfile",
"rustls-pki-types",
"tokio",
"tracing",
"url",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@@ -382,9 +443,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@@ -511,9 +572,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.44" version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@@ -2262,9 +2323,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]] [[package]]
name = "loom" name = "loom"
@@ -2396,7 +2457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d395473824516f38dd1071a1a37bc57daa7be65b293ebba4ead5f7abb017a2" checksum = "f1d395473824516f38dd1071a1a37bc57daa7be65b293ebba4ead5f7abb017a2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.11.1", "bitflags 2.12.1",
"ctor", "ctor",
"futures", "futures",
"napi-build", "napi-build",
@@ -2593,7 +2654,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
"cfg-if", "cfg-if",
"libc", "libc",
] ]
@@ -3470,7 +3531,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
] ]
[[package]] [[package]]
@@ -3645,7 +3706,7 @@ dependencies = [
"aes", "aes",
"aes-gcm", "aes-gcm",
"async-trait", "async-trait",
"bitflags 2.11.1", "bitflags 2.12.1",
"byteorder", "byteorder",
"bytes", "bytes",
"cbc", "cbc",
@@ -3758,7 +3819,7 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed8949eca4163c18a8f59ff96d32cf61e9c13b9735e21ef32b3907f4aafa1a9" checksum = "9ed8949eca4163c18a8f59ff96d32cf61e9c13b9735e21ef32b3907f4aafa1a9"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
"bytes", "bytes",
"chrono", "chrono",
"dashmap", "dashmap",
@@ -3814,7 +3875,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -4125,7 +4186,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
] ]
[[package]] [[package]]
@@ -4383,7 +4444,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -4669,7 +4730,7 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.4.1", "http 1.4.1",
@@ -5041,7 +5102,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.12.1",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap", "indexmap",
"semver", "semver",
@@ -5543,7 +5604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.11.1", "bitflags 2.12.1",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",
@@ -5588,67 +5649,6 @@ dependencies = [
"windows-core 0.59.0", "windows-core 0.59.0",
] ]
[[package]]
name = "wraith"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"iroh",
"rustls",
"rustls-acme",
"rustls-pemfile",
"rustls-pki-types",
"tokio",
"url",
"wraith-core",
]
[[package]]
name = "wraith-core"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"futures",
"ipnetwork",
"iroh",
"rand 0.10.1",
"rand_core 0.6.4",
"rcgen 0.14.8",
"russh",
"rustls",
"rustls-acme",
"rustls-pki-types",
"ssh-key",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tokio-rustls",
"tokio-util",
"tracing",
"url",
"webpki-roots 0.26.11",
"wraith-core",
]
[[package]]
name = "wraith-napi"
version = "0.1.0"
dependencies = [
"async-trait",
"iroh",
"napi",
"napi-derive",
"russh",
"rustls-pemfile",
"rustls-pki-types",
"tokio",
"tracing",
"url",
"wraith-core",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.3" version = "0.6.3"
@@ -5726,9 +5726,9 @@ dependencies = [
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [ dependencies = [
"stable_deref_trait", "stable_deref_trait",
"yoke-derive", "yoke-derive",

View File

@@ -1,8 +1,8 @@
[workspace] [workspace]
members = [ members = [
"crates/wraith-core", "crates/alknet-core",
"crates/wraith", "crates/alknet",
"crates/wraith-napi", "crates/alknet-napi",
] ]
resolver = "2" resolver = "2"
@@ -10,4 +10,4 @@ resolver = "2"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://git.alk.dev/alkdev/wraith" repository = "https://git.alk.dev/alkdev/alknet"

View File

@@ -1,6 +1,6 @@
# Wraith # Alknet
> **Status: Alpha** — This project is in early development. It depends on solid libraries (russh, tokio, iroh) for core functionality, but the glue code and integration between them has not been fully vetted for production use. Because wraith operates low in the network stack, bugs can cause serious problems downstream (leaked connections, broken tunnels, auth failures). Use with caution and report issues. > **Status: Alpha** — This project is in early development. It depends on solid libraries (russh, tokio, iroh) for core functionality, but the glue code and integration between them has not been fully vetted for production use. Because alknet operates low in the network stack, bugs can cause serious problems downstream (leaked connections, broken tunnels, auth failures). Use with caution and report issues.
A self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol. A self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol.
@@ -9,9 +9,9 @@ A self-hostable SSH-based tunnel tool that provides VPN-like functionality witho
- **Private tunneling** — Route traffic to internal services (Postgres, Redis, APIs) over SSH - **Private tunneling** — Route traffic to internal services (Postgres, Redis, APIs) over SSH
- **Censorship circumvention** — SSH over TLS on port 443 is indistinguishable from HTTPS to DPI - **Censorship circumvention** — SSH over TLS on port 443 is indistinguishable from HTTPS to DPI
- **NAT traversal** — The iroh transport enables peer-to-peer connections without public IPs or port forwarding - **NAT traversal** — The iroh transport enables peer-to-peer connections without public IPs or port forwarding
- **Service mesh connectivity** — Lightweight transport layer for event systems via reserved `wraith-*` destinations - **Service mesh connectivity** — Lightweight transport layer for event systems via reserved `alknet-*` destinations
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.
## Quick start ## Quick start
@@ -24,7 +24,7 @@ cargo build --release
The default build includes TLS and iroh transports. To build a minimal binary with just TCP: The default build includes TLS and iroh transports. To build a minimal binary with just TCP:
```bash ```bash
cargo build --release --no-default-features -p wraith cargo build --release --no-default-features -p alknet
``` ```
### Server ### Server
@@ -34,17 +34,17 @@ cargo build --release --no-default-features -p wraith
ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N ""
# Start the server on port 22 (TCP) # Start the server on port 22 (TCP)
wraith serve --key ssh_host_ed25519_key \ alknet serve --key ssh_host_ed25519_key \
--authorized-keys ~/.ssh/authorized_keys --authorized-keys ~/.ssh/authorized_keys
# TLS with stealth mode (looks like nginx 404 to scanners) # TLS with stealth mode (looks like nginx 404 to scanners)
wraith serve --key ssh_host_ed25519_key \ alknet serve --key ssh_host_ed25519_key \
--transport tls \ --transport tls \
--acme-domain example.com \ --acme-domain example.com \
--stealth --stealth
# iroh (no public IP needed) # iroh (no public IP needed)
wraith serve --key ssh_host_ed25519_key \ alknet serve --key ssh_host_ed25519_key \
--transport iroh --transport iroh
``` ```
@@ -52,21 +52,21 @@ wraith serve --key ssh_host_ed25519_key \
```bash ```bash
# Connect via TCP and start a SOCKS5 proxy on 127.0.0.1:1080 # Connect via TCP and start a SOCKS5 proxy on 127.0.0.1:1080
wraith connect --server example.com:22 \ alknet connect --server example.com:22 \
--identity ~/.ssh/id_ed25519 --identity ~/.ssh/id_ed25519
# Connect via TLS # Connect via TLS
wraith connect --server example.com:443 \ alknet connect --server example.com:443 \
--transport tls \ --transport tls \
--identity ~/.ssh/id_ed25519 --identity ~/.ssh/id_ed25519
# Connect via iroh (peer-to-peer, no public IP) # Connect via iroh (peer-to-peer, no public IP)
wraith connect --peer <endpoint-id> \ alknet connect --peer <endpoint-id> \
--transport iroh \ --transport iroh \
--identity ~/.ssh/id_ed25519 --identity ~/.ssh/id_ed25519
# With port forwarding # With port forwarding
wraith connect --server example.com:22 \ alknet connect --server example.com:22 \
--identity ~/.ssh/id_ed25519 \ --identity ~/.ssh/id_ed25519 \
--forward 5432:db.internal:5432 \ --forward 5432:db.internal:5432 \
--forward 6379:redis.internal:6379 --forward 6379:redis.internal:6379
@@ -80,24 +80,24 @@ Once connected, point any SOCKS5-aware application at `127.0.0.1:1080`:
curl --socks5 127.0.0.1:1080 http://internal-api:8080/health curl --socks5 127.0.0.1:1080 http://internal-api:8080/health
``` ```
For VPN-like "route all traffic" behavior, use [tun2proxy](https://github.com/tun2proxy/tun2proxy) alongside wraith's SOCKS5 proxy (see [ADR-014](docs/architecture/decisions/014-defer-tun-recommend-socks5-proxy.md)). For VPN-like "route all traffic" behavior, use [tun2proxy](https://github.com/tun2proxy/tun2proxy) alongside alknet's SOCKS5 proxy (see [ADR-014](docs/architecture/decisions/014-defer-tun-recommend-socks5-proxy.md)).
## Crates ## Crates
| Crate | Description | | Crate | Description |
|-------|-------------| |-------|-------------|
| `wraith-core` | Core library: transport trait, SOCKS5 server, port forwarding, auth, server handler | | `alknet-core` | Core library: transport trait, SOCKS5 server, port forwarding, auth, server handler |
| `wraith` | CLI binary (`wraith connect` / `wraith serve`) | | `alknet` | CLI binary (`alknet connect` / `alknet serve`) |
| `wraith-napi` | Node.js native addon via napi-rs (`connect()` / `serve()`) | | `alknet-napi` | Node.js native addon via napi-rs (`connect()` / `serve()`) |
## Feature flags ## Feature flags
| Feature | Crate | Default | Description | | Feature | Crate | Default | Description |
|---------|-------|---------|-------------| |---------|-------|---------|-------------|
| `tls` | `wraith-core`, `wraith` | yes | TLS transport (tokio-rustls) | | `tls` | `alknet-core`, `alknet` | yes | TLS transport (tokio-rustls) |
| `iroh` | `wraith-core`, `wraith` | yes | iroh QUIC P2P transport | | `iroh` | `alknet-core`, `alknet` | yes | iroh QUIC P2P transport |
| `acme` | `wraith-core` | no | ACME/Let's Encrypt auto-cert provisioning | | `acme` | `alknet-core` | no | ACME/Let's Encrypt auto-cert provisioning |
| `testutil` | `wraith-core` | no | Test utilities (for internal use) | | `testutil` | `alknet-core` | no | Test utilities (for internal use) |
## Transport modes ## Transport modes
@@ -117,7 +117,7 @@ Key formats are OpenSSH throughout (private keys: `-----BEGIN OPENSSH PRIVATE KE
## Architecture ## Architecture
Wraith's core architectural decision is that SSH never touches the network directly. The transport layer produces a duplex byte stream, and SSH runs over it via `russh::client::connect_stream()` / `russh::server::run_stream()`. This makes transports fully pluggable. Alknet's core architectural decision is that SSH never touches the network directly. The transport layer produces a duplex byte stream, and SSH runs over it via `russh::client::connect_stream()` / `russh::server::run_stream()`. This makes transports fully pluggable.
``` ```
Client Server Client Server
@@ -137,10 +137,10 @@ See [docs/architecture/](docs/architecture/) for full specifications and [ADR in
## Node.js API ## Node.js API
The `wraith-napi` crate provides a Node.js native addon via napi-rs: The `alknet-napi` crate provides a Node.js native addon via napi-rs:
```js ```js
const { connect, serve } = require('wraith-napi'); const { connect, serve } = require('alknet-napi');
// Client: open a duplex stream through SSH // Client: open a duplex stream through SSH
const stream = await connect({ const stream = await connect({
@@ -224,10 +224,10 @@ This is **alpha software**. While it depends on well-established libraries (russ
- **Connection handling edge cases** — reconnection logic, graceful shutdown, resource cleanup - **Connection handling edge cases** — reconnection logic, graceful shutdown, resource cleanup
- **Security review** — the auth layer, rate limiting, and stealth mode should be audited before production use - **Security review** — the auth layer, rate limiting, and stealth mode should be audited before production use
- **API stability** — the library API (`wraith-core`) and NAPI interface may change between versions - **API stability** — the library API (`alknet-core`) and NAPI interface may change between versions
- **Performance** — no load testing or benchmarking has been done yet - **Performance** — no load testing or benchmarking has been done yet
Please test thoroughly and [file issues](https://git.alk.dev/alkdev/wraith/issues) for any problems you encounter. Please test thoroughly and [file issues](https://git.alk.dev/alkdev/alknet/issues) for any problems you encounter.
## License ## License

View File

@@ -1,13 +1,13 @@
[package] [package]
name = "wraith-core" name = "alknet-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
description = "Core library for Wraith: pluggable SSH tunnel transport, SOCKS5 proxy, port forwarding, and authentication" description = "Core library for Alknet: pluggable SSH tunnel transport, SOCKS5 proxy, port forwarding, and authentication"
repository.workspace = true repository.workspace = true
[lib] [lib]
name = "wraith_core" name = "alknet_core"
[features] [features]
default = [] default = []
@@ -36,9 +36,9 @@ async-trait = "0.1"
ipnetwork = "0.21.1" ipnetwork = "0.21.1"
[dev-dependencies] [dev-dependencies]
wraith-core = { path = ".", features = ["testutil", "tls", "iroh"] } alknet-core = { path = ".", features = ["testutil", "tls", "iroh"] }
tempfile = "3" tempfile = "3"
rcgen = "0.14" rcgen = "0.14"
rand_core = "0.6" rand_core = "0.6"
ssh-key = { version = "0.6", features = ["ed25519", "alloc"] } ssh-key = { version = "0.6", features = ["ed25519", "alloc"] }
rand = "0.10.1" rand = "0.10.1"

View File

@@ -41,14 +41,14 @@ impl std::fmt::Display for TransportMode {
} }
} }
/// Programmatic configuration for a wraith client session. /// Programmatic configuration for an alknet client session.
/// ///
/// Construct with `ConnectOptions::new(key_source)` and chain builder methods. /// Construct with `ConnectOptions::new(key_source)` and chain builder methods.
/// Call `validate()` before passing to `ClientSession::new()`. /// Call `validate()` before passing to `ClientSession::new()`.
/// ///
/// ``` /// ```
/// use wraith_core::client::{ConnectOptions, TransportMode}; /// use alknet_core::client::{ConnectOptions, TransportMode};
/// use wraith_core::auth::keys::KeySource; /// use alknet_core::auth::keys::KeySource;
/// ///
/// let opts = ConnectOptions::new(KeySource::File("/path/to/key".into())) /// let opts = ConnectOptions::new(KeySource::File("/path/to/key".into()))
/// .server("example.com:22") /// .server("example.com:22")
@@ -312,7 +312,7 @@ impl<T: Transport> ClientSession<T> {
.await; .await;
}); });
info!("wraith client running: SOCKS5 on {}", socks5_listen); info!("alknet client running: SOCKS5 on {}", socks5_listen);
#[cfg(unix)] #[cfg(unix)]
let signal_done = { let signal_done = {
@@ -439,7 +439,7 @@ impl<T: Transport> ClientSession<T> {
fn derive_username() -> String { fn derive_username() -> String {
std::env::var("USER") std::env::var("USER")
.or_else(|_| std::env::var("USERNAME")) .or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "wraith".to_string()) .unwrap_or_else(|_| "alknet".to_string())
} }
async fn establish_session<T: Transport>( async fn establish_session<T: Transport>(
@@ -567,7 +567,7 @@ mod tests {
.remote_forward("0.0.0.0:8080:127.0.0.1:3000") .remote_forward("0.0.0.0:8080:127.0.0.1:3000")
.proxy("socks5://127.0.0.1:1080") .proxy("socks5://127.0.0.1:1080")
.iroh_relay("https://relay.example.com") .iroh_relay("https://relay.example.com")
.tls_server_name("wraith.test") .tls_server_name("alknet.test")
.insecure(true); .insecure(true);
assert_eq!(opts.server.as_deref(), Some("example.com:22")); assert_eq!(opts.server.as_deref(), Some("example.com:22"));
@@ -577,7 +577,7 @@ mod tests {
assert_eq!(opts.remote_forwards.len(), 1); assert_eq!(opts.remote_forwards.len(), 1);
assert_eq!(opts.proxy.as_deref(), Some("socks5://127.0.0.1:1080")); assert_eq!(opts.proxy.as_deref(), Some("socks5://127.0.0.1:1080"));
assert_eq!(opts.iroh_relay.as_deref(), Some("https://relay.example.com")); assert_eq!(opts.iroh_relay.as_deref(), Some("https://relay.example.com"));
assert_eq!(opts.tls_server_name.as_deref(), Some("wraith.test")); assert_eq!(opts.tls_server_name.as_deref(), Some("alknet.test"));
assert!(opts.insecure); assert!(opts.insecure);
} }

View File

@@ -1,4 +1,4 @@
//! Error types for wraith-core. //! Error types for alknet-core.
//! //!
//! Layered error hierarchy: //! Layered error hierarchy:
//! - `TransportError` — connection/handshake/timeout errors (trigger reconnection on client) //! - `TransportError` — connection/handshake/timeout errors (trigger reconnection on client)

View File

@@ -1,8 +1,8 @@
//! # wraith-core //! # alknet-core
//! //!
//! Core library for [Wraith](https://git.alk.dev/alkdev/wraith), a self-hostable SSH-based //! Core library for [Alknet](https://git.alk.dev/alkdev/alknet), a self-hostable SSH-based
//! tunnel tool. This crate provides the transport abstraction, SOCKS5 server, port forwarding, //! tunnel tool. This crate provides the transport abstraction, SOCKS5 server, port forwarding,
//! authentication, and server handler — everything needed to build a wraith client or server //! authentication, and server handler — everything needed to build an alknet client or server
//! on top of pluggable transports. //! on top of pluggable transports.
//! //!
//! > **Alpha software.** This crate depends on solid libraries (russh, tokio, rustls, iroh) //! > **Alpha software.** This crate depends on solid libraries (russh, tokio, rustls, iroh)
@@ -33,10 +33,10 @@
//! //!
//! ```no_run //! ```no_run
//! use std::sync::Arc; //! use std::sync::Arc;
//! use wraith_core::transport::TcpTransport; //! use alknet_core::transport::TcpTransport;
//! use wraith_core::client::{ClientSession, ConnectOptions, TransportMode}; //! use alknet_core::client::{ClientSession, ConnectOptions, TransportMode};
//! use wraith_core::auth::keys::KeySource; //! use alknet_core::auth::keys::KeySource;
//! use wraith_core::Transport; //! use alknet_core::Transport;
//! //!
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! async fn main() -> anyhow::Result<()> {

View File

@@ -1,6 +1,6 @@
//! Control channel routing for reserved `wraith-*` destinations. //! Control channel routing for reserved `alknet-*` destinations.
//! //!
//! SSH channels opened with a destination starting with `wraith-` are intercepted //! SSH channels opened with a destination starting with `alknet-` are intercepted
//! by the server and routed to a `ControlChannelHandler` instead of proxied to a //! by the server and routed to a `ControlChannelHandler` instead of proxied to a
//! TCP target. See ADR-018 for the design rationale. //! TCP target. See ADR-018 for the design rationale.
@@ -9,11 +9,11 @@ use std::io;
use async_trait::async_trait; use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
pub const WRAITH_CONTROL_DESTINATION: &str = "wraith-control"; pub const ALKNET_CONTROL_DESTINATION: &str = "alknet-control";
pub const WRAITH_PREFIX: &str = "wraith-"; pub const ALKNET_PREFIX: &str = "alknet-";
pub fn is_reserved_destination(host: &str) -> bool { pub fn is_reserved_destination(host: &str) -> bool {
host.starts_with(WRAITH_PREFIX) host.starts_with(ALKNET_PREFIX)
} }
pub trait DuplexStream: AsyncRead + AsyncWrite + Unpin + Send {} pub trait DuplexStream: AsyncRead + AsyncWrite + Unpin + Send {}
@@ -68,21 +68,21 @@ mod tests {
use tokio::io::duplex; use tokio::io::duplex;
#[test] #[test]
fn wraith_control_destination_constant() { fn alknet_control_destination_constant() {
assert_eq!(WRAITH_CONTROL_DESTINATION, "wraith-control"); assert_eq!(ALKNET_CONTROL_DESTINATION, "alknet-control");
} }
#[test] #[test]
fn wraith_prefix_constant() { fn alknet_prefix_constant() {
assert_eq!(WRAITH_PREFIX, "wraith-"); assert_eq!(ALKNET_PREFIX, "alknet-");
} }
#[test] #[test]
fn reserved_destination_detected() { fn reserved_destination_detected() {
assert!(is_reserved_destination("wraith-control")); assert!(is_reserved_destination("alknet-control"));
assert!(is_reserved_destination("wraith-status")); assert!(is_reserved_destination("alknet-status"));
assert!(is_reserved_destination("wraith-events")); assert!(is_reserved_destination("alknet-events"));
assert!(is_reserved_destination("wraith-")); assert!(is_reserved_destination("alknet-"));
} }
#[test] #[test]
@@ -90,17 +90,17 @@ mod tests {
assert!(!is_reserved_destination("example.com")); assert!(!is_reserved_destination("example.com"));
assert!(!is_reserved_destination("localhost")); assert!(!is_reserved_destination("localhost"));
assert!(!is_reserved_destination("192.168.1.1")); assert!(!is_reserved_destination("192.168.1.1"));
assert!(!is_reserved_destination("wraith.example.com")); assert!(!is_reserved_destination("alknet.example.com"));
assert!(!is_reserved_destination("")); assert!(!is_reserved_destination(""));
assert!(!is_reserved_destination("wrait-control")); assert!(!is_reserved_destination("alkne-control"));
assert!(!is_reserved_destination("WRAITH-control")); assert!(!is_reserved_destination("ALKNET-control"));
} }
#[test] #[test]
fn prefix_matching_case_sensitive() { fn prefix_matching_case_sensitive() {
assert!(!is_reserved_destination("Wraith-control")); assert!(!is_reserved_destination("Alknet-control"));
assert!(!is_reserved_destination("WRAITH-control")); assert!(!is_reserved_destination("ALKNET-control"));
assert!(is_reserved_destination("wraith-Control")); assert!(is_reserved_destination("alknet-Control"));
} }
#[test] #[test]
@@ -187,6 +187,6 @@ mod tests {
#[test] #[test]
fn control_channel_destination_matches_prefix() { fn control_channel_destination_matches_prefix() {
assert!(is_reserved_destination(WRAITH_CONTROL_DESTINATION)); assert!(is_reserved_destination(ALKNET_CONTROL_DESTINATION));
} }
} }

View File

@@ -10,7 +10,7 @@ use russh::ChannelId;
use crate::auth::ServerAuthConfig; use crate::auth::ServerAuthConfig;
use crate::server::control_channel::{ use crate::server::control_channel::{
ControlChannelHandler, ControlChannelRouter, WRAITH_PREFIX, ControlChannelHandler, ControlChannelRouter, ALKNET_PREFIX,
}; };
use crate::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter}; use crate::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
@@ -210,7 +210,7 @@ impl Handler for ServerHandler {
originator_port: u32, originator_port: u32,
_session: &mut Session, _session: &mut Session,
) -> Result<bool, Self::Error> { ) -> Result<bool, Self::Error> {
if host_to_connect.starts_with(WRAITH_PREFIX) { if host_to_connect.starts_with(ALKNET_PREFIX) {
if !self.control_channel_router.has_handler() { if !self.control_channel_router.has_handler() {
return Ok(false); return Ok(false);
} }
@@ -576,18 +576,18 @@ mod tests {
} }
#[test] #[test]
fn reserved_wraith_destination_routing() { fn reserved_alknet_destination_routing() {
use crate::server::control_channel::is_reserved_destination; use crate::server::control_channel::is_reserved_destination;
assert!(is_reserved_destination("wraith-control")); assert!(is_reserved_destination("alknet-control"));
assert!(is_reserved_destination("wraith-status")); assert!(is_reserved_destination("alknet-status"));
assert!(is_reserved_destination("wraith-events")); assert!(is_reserved_destination("alknet-events"));
assert!(!is_reserved_destination("example.com")); assert!(!is_reserved_destination("example.com"));
assert!(!is_reserved_destination("localhost")); assert!(!is_reserved_destination("localhost"));
assert!(!is_reserved_destination("wraith.example.com")); assert!(!is_reserved_destination("alknet.example.com"));
} }
#[test] #[test]
fn server_handler_without_control_handler_rejects_wraith_destinations() { fn server_handler_without_control_handler_rejects_alknet_destinations() {
let auth_config = make_empty_auth_config(); let auth_config = make_empty_auth_config();
let handler = make_handler(auth_config, None, None); let handler = make_handler(auth_config, None, None);
assert!(!handler.control_channel_router().has_handler()); assert!(!handler.control_channel_router().has_handler());

View File

@@ -5,7 +5,7 @@
//! auth, connection rate limiting, auth attempt limiting, stealth mode (fake nginx 404), //! auth, connection rate limiting, auth attempt limiting, stealth mode (fake nginx 404),
//! and outbound proxy routing (direct/SOCKS5/HTTP CONNECT). //! and outbound proxy routing (direct/SOCKS5/HTTP CONNECT).
//! //!
//! Destination hosts starting with `wraith-` are reserved for internal use (control channel, ADR-018). //! Destination hosts starting with `alknet-` are reserved for internal use (control channel, ADR-018).
pub mod channel_proxy; pub mod channel_proxy;
pub mod control_channel; pub mod control_channel;
@@ -16,8 +16,8 @@ pub mod stealth;
pub use channel_proxy::{connect_outbound, proxy_channel}; pub use channel_proxy::{connect_outbound, proxy_channel};
pub use control_channel::{ pub use control_channel::{
ControlChannelHandler, ControlChannelRouter, DuplexStream, WRAITH_CONTROL_DESTINATION, ControlChannelHandler, ControlChannelRouter, DuplexStream, ALKNET_CONTROL_DESTINATION,
WRAITH_PREFIX, is_reserved_destination, ALKNET_PREFIX, is_reserved_destination,
}; };
pub use handler::{ProxyConfig, ProxyMode, ServerHandler, TransportKind}; pub use handler::{ProxyConfig, ProxyMode, ServerHandler, TransportKind};
pub use rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter}; pub use rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};

View File

@@ -40,14 +40,14 @@ impl std::fmt::Display for ServeTransportMode {
} }
} }
/// Programmatic configuration for a wraith server. /// Programmatic configuration for an alknet server.
/// ///
/// Construct with `ServeOptions::new(key_source)` and chain builder methods. /// Construct with `ServeOptions::new(key_source)` and chain builder methods.
/// Call `validate()` before passing to `Server::new()`. /// Call `validate()` before passing to `Server::new()`.
/// ///
/// ``` /// ```
/// use wraith_core::server::{ServeOptions, ServeTransportMode}; /// use alknet_core::server::{ServeOptions, ServeTransportMode};
/// use wraith_core::auth::keys::KeySource; /// use alknet_core::auth::keys::KeySource;
/// ///
/// let opts = ServeOptions::new(KeySource::File("/path/to/host_key".into())) /// let opts = ServeOptions::new(KeySource::File("/path/to/host_key".into()))
/// .transport_mode(ServeTransportMode::Tcp) /// .transport_mode(ServeTransportMode::Tcp)
@@ -221,7 +221,7 @@ struct ActiveSession {
join: tokio::task::JoinHandle<()>, join: tokio::task::JoinHandle<()>,
} }
/// The wraith SSH server. /// The alknet SSH server.
/// ///
/// Accepts connections over any `TransportAcceptor`, authenticates via Ed25519 keys /// Accepts connections over any `TransportAcceptor`, authenticates via Ed25519 keys
/// or certificate authority, and proxies `direct-tcpip` channels to their targets. /// or certificate authority, and proxies `direct-tcpip` channels to their targets.
@@ -331,13 +331,13 @@ impl Server {
if self.transport_mode == ServeTransportMode::Iroh { if self.transport_mode == ServeTransportMode::Iroh {
if let Some(id) = endpoint_info { if let Some(id) = endpoint_info {
info!("wraith server running: transport=iroh endpoint_id={}", id); info!("alknet server running: transport=iroh endpoint_id={}", id);
} else { } else {
info!("wraith server running: transport=iroh"); info!("alknet server running: transport=iroh");
} }
} else { } else {
info!( info!(
"wraith server running: transport={} listen={}", "alknet server running: transport={} listen={}",
self.transport_mode, self.listen_addr self.transport_mode, self.listen_addr
); );
} }

View File

@@ -9,7 +9,7 @@ use tokio::io;
use super::{Transport, TransportAcceptor, TransportInfo, TransportKind}; use super::{Transport, TransportAcceptor, TransportInfo, TransportKind};
pub const ALPN: &[u8] = b"wraith-ssh"; pub const ALPN: &[u8] = b"alknet-ssh";
const DEFAULT_RELAY_URL: &str = "https://relay.iroh.network/"; const DEFAULT_RELAY_URL: &str = "https://relay.iroh.network/";
/// A client-side iroh QUIC P2P transport that connects to a remote iroh endpoint. /// A client-side iroh QUIC P2P transport that connects to a remote iroh endpoint.
@@ -31,8 +31,8 @@ pub struct IrohTransport {
impl IrohTransport { impl IrohTransport {
/// Create a new iroh transport with its own dedicated endpoint. /// Create a new iroh transport with its own dedicated endpoint.
/// ///
/// The endpoint is created with the `wraith-ssh` ALPN and the provided /// The endpoint is created with the `alknet-ssh` ALPN and the provided
/// relay URL. Use this when wraith is the only iroh service on this node. /// relay URL. Use this when alknet is the only iroh service on this node.
pub async fn new( pub async fn new(
node_id: NodeId, node_id: NodeId,
relay_url: Option<RelayUrl>, relay_url: Option<RelayUrl>,
@@ -54,9 +54,9 @@ impl IrohTransport {
/// Create an iroh transport using an existing shared endpoint. /// Create an iroh transport using an existing shared endpoint.
/// ///
/// The endpoint must already have the `wraith-ssh` ALPN registered /// The endpoint must already have the `alknet-ssh` ALPN registered
/// (typically via [`iroh::protocol::Router::builder`]). This enables /// (typically via [`iroh::protocol::Router::builder`]). This enables
/// running wraith alongside iroh-blobs, iroh-gossip, iroh-docs, and /// running alknet alongside iroh-blobs, iroh-gossip, iroh-docs, and
/// other protocol handlers on the same QUIC endpoint — one connection /// other protocol handlers on the same QUIC endpoint — one connection
/// per peer, multiplexed by ALPN. /// per peer, multiplexed by ALPN.
pub fn from_endpoint(node_id: NodeId, endpoint: Endpoint) -> Self { pub fn from_endpoint(node_id: NodeId, endpoint: Endpoint) -> Self {
@@ -102,9 +102,9 @@ impl Transport for IrohTransport {
/// [`IrohAcceptor::from_endpoint`] to share an existing iroh `Endpoint` /// [`IrohAcceptor::from_endpoint`] to share an existing iroh `Endpoint`
/// with other protocol handlers (blobs, gossip, docs). /// with other protocol handlers (blobs, gossip, docs).
/// ///
/// When using `from_endpoint`, the wraith-ssh ALPN must be registered /// When using `from_endpoint`, the alknet-ssh ALPN must be registered
/// via an iroh `Router` that calls `Handler::accept()` on incoming /// via an iroh `Router` that calls `Handler::accept()` on incoming
/// connections with the `wraith-ssh` ALPN, then passes the accepted /// connections with the `alknet-ssh` ALPN, then passes the accepted
/// bidirectional stream to `russh::server::run_stream()`. /// bidirectional stream to `russh::server::run_stream()`.
pub struct IrohAcceptor { pub struct IrohAcceptor {
endpoint: Endpoint, endpoint: Endpoint,
@@ -112,9 +112,9 @@ pub struct IrohAcceptor {
} }
impl IrohAcceptor { impl IrohAcceptor {
/// Bind a new iroh endpoint with a dedicated `wraith-ssh` ALPN. /// Bind a new iroh endpoint with a dedicated `alknet-ssh` ALPN.
/// ///
/// Use this when wraith is the only iroh service on this node. /// Use this when alknet is the only iroh service on this node.
pub async fn bind( pub async fn bind(
relay_url: Option<RelayUrl>, relay_url: Option<RelayUrl>,
proxy_url: Option<url::Url>, proxy_url: Option<url::Url>,
@@ -135,14 +135,14 @@ impl IrohAcceptor {
/// Create an iroh acceptor using an existing shared endpoint. /// Create an iroh acceptor using an existing shared endpoint.
/// ///
/// The endpoint must already have the `wraith-ssh` ALPN registered /// The endpoint must already have the `alknet-ssh` ALPN registered
/// (typically via [`iroh::protocol::Router::builder`]). When using a /// (typically via [`iroh::protocol::Router::builder`]). When using a
/// shared endpoint, incoming connections with the `wraith-ssh` ALPN /// shared endpoint, incoming connections with the `alknet-ssh` ALPN
/// are routed by the Router to a `ProtocolHandler` that this acceptor /// are routed by the Router to a `ProtocolHandler` that this acceptor
/// does not manage — the caller is responsible for bridging the /// does not manage — the caller is responsible for bridging the
/// Router's `accept()` callback to this acceptor's stream handling. /// Router's `accept()` callback to this acceptor's stream handling.
/// ///
/// For the standalone case where wraith owns the endpoint, use /// For the standalone case where alknet owns the endpoint, use
/// [`IrohAcceptor::bind`] instead, which handles the accept loop /// [`IrohAcceptor::bind`] instead, which handles the accept loop
/// internally. /// internally.
pub fn from_endpoint(endpoint: Endpoint) -> Self { pub fn from_endpoint(endpoint: Endpoint) -> Self {

View File

@@ -1,4 +1,4 @@
//! Pluggable transport layer for Wraith. //! Pluggable transport layer for Alknet.
//! //!
//! The transport layer produces a duplex byte stream (`AsyncRead + AsyncWrite + Unpin + Send`) //! The transport layer produces a duplex byte stream (`AsyncRead + AsyncWrite + Unpin + Send`)
//! that SSH consumes. This is the core architectural abstraction — SSH never opens its own //! that SSH consumes. This is the core architectural abstraction — SSH never opens its own

View File

@@ -293,9 +293,9 @@ mod tests {
fn tls_transport_builder_methods() { fn tls_transport_builder_methods() {
let addr: SocketAddr = "1.2.3.4:443".parse().unwrap(); let addr: SocketAddr = "1.2.3.4:443".parse().unwrap();
let transport = TlsTransport::new(addr) let transport = TlsTransport::new(addr)
.with_server_name("wraith.test") .with_server_name("alknet.test")
.with_insecure(true); .with_insecure(true);
assert_eq!(transport.tls_server_name, Some("wraith.test".to_string())); assert_eq!(transport.tls_server_name, Some("alknet.test".to_string()));
assert!(transport.insecure); assert!(transport.insecure);
} }
@@ -395,7 +395,7 @@ mod tests {
let mut client = transport.connect().await.unwrap(); let mut client = transport.connect().await.unwrap();
let (mut server, _info) = accept_handle.await.unwrap(); let (mut server, _info) = accept_handle.await.unwrap();
let msg = b"wraith integration test"; let msg = b"alknet integration test";
client.write_all(msg).await.unwrap(); client.write_all(msg).await.unwrap();
let mut buf = vec![0u8; msg.len()]; let mut buf = vec![0u8; msg.len()];
server.read_exact(&mut buf).await.unwrap(); server.read_exact(&mut buf).await.unwrap();

View File

@@ -1,4 +1,4 @@
use wraith_core::testutil::{MockTransport, MockTransportAcceptor, Transport, TransportAcceptor, mock_pair}; use alknet_core::testutil::{MockTransport, MockTransportAcceptor, Transport, TransportAcceptor, mock_pair};
#[tokio::test] #[tokio::test]
async fn mock_transport_connect() { async fn mock_transport_connect() {

View File

@@ -1,16 +1,16 @@
[package] [package]
name = "wraith-napi" name = "alknet-napi"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
description = "Node.js native addon for Wraith via napi-rs: connect() and serve() SSH tunnel functions" description = "Node.js native addon for Alknet via napi-rs: connect() and serve() SSH tunnel functions"
repository.workspace = true repository.workspace = true
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
wraith-core = { path = "../wraith-core", features = ["tls", "iroh"] } alknet-core = { path = "../alknet-core", features = ["tls", "iroh"] }
napi = { version = "3", features = ["async", "error_anyhow"] } napi = { version = "3", features = ["async", "error_anyhow"] }
napi-derive = "3" napi-derive = "3"
tokio = { version = "1", features = ["io-util", "sync", "rt", "macros", "net", "time", "signal"] } tokio = { version = "1", features = ["io-util", "sync", "rt", "macros", "net", "time", "signal"] }

View File

@@ -1,4 +1,4 @@
//! NAPI `connect()` function and `WraithStream` type. //! NAPI `connect()` function and `AlknetStream` type.
//! //!
//! Opens a single SSH channel as a duplex stream for programmatic use. //! Opens a single SSH channel as a duplex stream for programmatic use.
//! Unlike the CLI client, this does not start a SOCKS5 server or port forwards — //! Unlike the CLI client, this does not start a SOCKS5 server or port forwards —
@@ -13,15 +13,15 @@ use russh::client;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use wraith_core::auth::client_auth::{ClientAuthConfig, ClientHandler}; use alknet_core::auth::client_auth::{ClientAuthConfig, ClientHandler};
use wraith_core::auth::keys::KeySource; use alknet_core::auth::keys::KeySource;
use wraith_core::transport::{IrohTransport, TcpTransport, TlsTransport, Transport}; use alknet_core::transport::{IrohTransport, TcpTransport, TlsTransport, Transport};
const DEFAULT_HOST: &str = "wraith-control"; const DEFAULT_HOST: &str = "alknet-control";
const DEFAULT_PORT: u32 = 0; const DEFAULT_PORT: u32 = 0;
#[napi(object)] #[napi(object)]
pub struct WraithConnectOptions { pub struct AlknetConnectOptions {
pub server: Option<String>, pub server: Option<String>,
pub peer: Option<String>, pub peer: Option<String>,
pub transport: String, pub transport: String,
@@ -53,13 +53,13 @@ fn parse_addr(addr_str: &str) -> Result<SocketAddr> {
} }
#[napi] #[napi]
pub struct WraithStream { pub struct AlknetStream {
read: Arc<Mutex<tokio::io::ReadHalf<russh::ChannelStream<client::Msg>>>>, read: Arc<Mutex<tokio::io::ReadHalf<russh::ChannelStream<client::Msg>>>>,
write: Arc<Mutex<tokio::io::WriteHalf<russh::ChannelStream<client::Msg>>>>, write: Arc<Mutex<tokio::io::WriteHalf<russh::ChannelStream<client::Msg>>>>,
} }
#[napi] #[napi]
impl WraithStream { impl AlknetStream {
#[napi] #[napi]
pub async fn read(&self, size: u32) -> Result<Buffer> { pub async fn read(&self, size: u32) -> Result<Buffer> {
let mut buf = vec![0u8; size as usize]; let mut buf = vec![0u8; size as usize];
@@ -96,7 +96,7 @@ impl WraithStream {
} }
#[napi] #[napi]
pub async fn connect(options: WraithConnectOptions) -> Result<WraithStream> { pub async fn connect(options: AlknetConnectOptions) -> Result<AlknetStream> {
let key_source = resolve_key_source(&options.identity)?; let key_source = resolve_key_source(&options.identity)?;
let auth_config = Arc::new( let auth_config = Arc::new(
ClientAuthConfig::from_key_source(key_source) ClientAuthConfig::from_key_source(key_source)
@@ -105,7 +105,7 @@ pub async fn connect(options: WraithConnectOptions) -> Result<WraithStream> {
let transport_mode = options.transport.to_lowercase(); let transport_mode = options.transport.to_lowercase();
let handler = ClientHandler::from_config(&auth_config); let handler = ClientHandler::from_config(&auth_config);
let username = "wraith".to_string(); let username = "alknet".to_string();
let config = Arc::new(client::Config::default()); let config = Arc::new(client::Config::default());
@@ -232,7 +232,7 @@ pub async fn connect(options: WraithConnectOptions) -> Result<WraithStream> {
let stream = channel.into_stream(); let stream = channel.into_stream();
let (read_half, write_half) = tokio::io::split(stream); let (read_half, write_half) = tokio::io::split(stream);
Ok(WraithStream { Ok(AlknetStream {
read: Arc::new(Mutex::new(read_half)), read: Arc::new(Mutex::new(read_half)),
write: Arc::new(Mutex::new(write_half)), write: Arc::new(Mutex::new(write_half)),
}) })

View File

@@ -1,6 +1,6 @@
//! # wraith-napi //! # alknet-napi
//! //!
//! Node.js native addon for [Wraith](https://git.alk.dev/alkdev/wraith) via napi-rs. //! Node.js native addon for [Alknet](https://git.alk.dev/alkdev/alknet) via napi-rs.
//! Exposes `connect()` and `serve()` functions for programmatic SSH tunnel creation. //! Exposes `connect()` and `serve()` functions for programmatic SSH tunnel creation.
//! //!
//! > **Alpha software.** The NAPI interface may change between versions. //! > **Alpha software.** The NAPI interface may change between versions.
@@ -8,7 +8,7 @@
//! # Quick example (Node.js) //! # Quick example (Node.js)
//! //!
//! ```js //! ```js
//! const { connect, serve } = require('wraith-napi'); //! const { connect, serve } = require('alknet-napi');
//! //!
//! // Client: open a duplex SSH stream //! // Client: open a duplex SSH stream
//! const stream = await connect({ //! const stream = await connect({

View File

@@ -1,4 +1,4 @@
//! NAPI `serve()` function and `WraithServer` type. //! NAPI `serve()` function and `AlknetServer` type.
//! //!
//! Starts an SSH server that emits new channel streams via a //! Starts an SSH server that emits new channel streams via a
//! `ThreadsafeFunction` callback. Supports TCP, TLS, and iroh transports. //! `ThreadsafeFunction` callback. Supports TCP, TLS, and iroh transports.
@@ -14,14 +14,14 @@ use russh::Channel;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use wraith_core::auth::keys::KeySource; use alknet_core::auth::keys::KeySource;
use wraith_core::auth::server_auth::ServerAuthConfig; use alknet_core::auth::server_auth::ServerAuthConfig;
use wraith_core::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter}; use alknet_core::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
use wraith_core::server::serve::{ServeOptions, ServeTransportMode, Server}; use alknet_core::server::serve::{ServeOptions, ServeTransportMode, Server};
use wraith_core::transport::{TcpAcceptor, TransportAcceptor}; use alknet_core::transport::{TcpAcceptor, TransportAcceptor};
#[napi(object)] #[napi(object)]
pub struct WraithServeOptions { pub struct AlknetServeOptions {
pub transport: String, pub transport: String,
pub host_key: Option<Either<String, Buffer>>, pub host_key: Option<Either<String, Buffer>>,
pub authorized_keys: Option<Either<String, Buffer>>, pub authorized_keys: Option<Either<String, Buffer>>,
@@ -75,13 +75,13 @@ pub struct ConnectionInfo {
} }
#[napi] #[napi]
pub struct WraithServerStream { pub struct AlknetServerStream {
read: Arc<Mutex<tokio::io::ReadHalf<russh::ChannelStream<server::Msg>>>>, read: Arc<Mutex<tokio::io::ReadHalf<russh::ChannelStream<server::Msg>>>>,
write: Arc<Mutex<tokio::io::WriteHalf<russh::ChannelStream<server::Msg>>>>, write: Arc<Mutex<tokio::io::WriteHalf<russh::ChannelStream<server::Msg>>>>,
} }
#[napi] #[napi]
impl WraithServerStream { impl AlknetServerStream {
#[napi] #[napi]
pub async fn read(&self, size: u32) -> napi::Result<Buffer> { pub async fn read(&self, size: u32) -> napi::Result<Buffer> {
let mut buf = vec![0u8; size as usize]; let mut buf = vec![0u8; size as usize];
@@ -208,7 +208,7 @@ impl russh::server::Handler for NapiServerHandler {
_originator_port: u32, _originator_port: u32,
_session: &mut russh::server::Session, _session: &mut russh::server::Session,
) -> std::result::Result<bool, Self::Error> { ) -> std::result::Result<bool, Self::Error> {
if host_to_connect.starts_with("wraith-") { if host_to_connect.starts_with("alknet-") {
let guard = self.channel_sender.lock().await; let guard = self.channel_sender.lock().await;
if let Some(ref tx) = *guard { if let Some(ref tx) = *guard {
let _ = tx.send(channel); let _ = tx.send(channel);
@@ -385,7 +385,7 @@ impl russh::server::Handler for NapiServerHandler {
type ServerTsfn = ThreadsafeFunction<ConnectionEventWrapper, (), ConnectionEventWrapper>; type ServerTsfn = ThreadsafeFunction<ConnectionEventWrapper, (), ConnectionEventWrapper>;
#[napi] #[napi]
pub struct WraithServer { pub struct AlknetServer {
shutdown_tx: tokio::sync::watch::Sender<bool>, shutdown_tx: tokio::sync::watch::Sender<bool>,
listen_addr: String, listen_addr: String,
endpoint_id: Option<String>, endpoint_id: Option<String>,
@@ -393,7 +393,7 @@ pub struct WraithServer {
} }
struct ConnectionEventWrapper { struct ConnectionEventWrapper {
stream: WraithServerStream, stream: AlknetServerStream,
info: ConnectionInfo, info: ConnectionInfo,
} }
@@ -408,7 +408,7 @@ impl ToNapiValue for ConnectionEventWrapper {
"Failed to create object" "Failed to create object"
)?; )?;
let stream_val = <WraithServerStream as ToNapiValue>::to_napi_value(env, val.stream)?; let stream_val = <AlknetServerStream as ToNapiValue>::to_napi_value(env, val.stream)?;
let key_stream = std::ffi::CString::new("stream").unwrap(); let key_stream = std::ffi::CString::new("stream").unwrap();
napi::check_status!( napi::check_status!(
napi::sys::napi_set_named_property(env, raw_obj, key_stream.as_ptr(), stream_val), napi::sys::napi_set_named_property(env, raw_obj, key_stream.as_ptr(), stream_val),
@@ -439,7 +439,7 @@ impl TypeName for ConnectionEventWrapper {
impl ValidateNapiValue for ConnectionEventWrapper {} impl ValidateNapiValue for ConnectionEventWrapper {}
#[napi] #[napi]
impl WraithServer { impl AlknetServer {
#[napi] #[napi]
pub async fn close(&self) -> napi::Result<()> { pub async fn close(&self) -> napi::Result<()> {
let _ = self.shutdown_tx.send(true); let _ = self.shutdown_tx.send(true);
@@ -470,7 +470,7 @@ impl WraithServer {
} }
#[napi] #[napi]
pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> { pub async fn serve(options: AlknetServeOptions) -> napi::Result<AlknetServer> {
let host_key_source = resolve_key_source(&options.host_key, "hostKey")?; let host_key_source = resolve_key_source(&options.host_key, "hostKey")?;
let authorized_keys_source = resolve_optional_key_source(&options.authorized_keys); let authorized_keys_source = resolve_optional_key_source(&options.authorized_keys);
let cert_authority_source = resolve_optional_key_source(&options.cert_authority); let cert_authority_source = resolve_optional_key_source(&options.cert_authority);
@@ -543,7 +543,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
); );
let private_key = let private_key =
wraith_core::auth::keys::load_private_key(host_key_source).map_err(|e| { alknet_core::auth::keys::load_private_key(host_key_source.clone()).map_err(|e| {
napi::Error::new(napi::Status::InvalidArg, format!("host key error: {}", e)) napi::Error::new(napi::Status::InvalidArg, format!("host key error: {}", e))
})?; })?;
@@ -573,7 +573,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
.await; .await;
}); });
Ok(WraithServer { Ok(AlknetServer {
shutdown_tx, shutdown_tx,
listen_addr: actual_listen, listen_addr: actual_listen,
endpoint_id: None, endpoint_id: None,
@@ -581,7 +581,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
}) })
} }
ServeTransportMode::Tls => { ServeTransportMode::Tls => {
use wraith_core::transport::TlsAcceptor; use alknet_core::transport::TlsAcceptor;
let addr = parse_addr(listen_addr_str)?; let addr = parse_addr(listen_addr_str)?;
@@ -654,7 +654,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
); );
let private_key = let private_key =
wraith_core::auth::keys::load_private_key(host_key_source).map_err(|e| { alknet_core::auth::keys::load_private_key(host_key_source.clone()).map_err(|e| {
napi::Error::new(napi::Status::InvalidArg, format!("host key error: {}", e)) napi::Error::new(napi::Status::InvalidArg, format!("host key error: {}", e))
})?; })?;
@@ -684,7 +684,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
.await; .await;
}); });
Ok(WraithServer { Ok(AlknetServer {
shutdown_tx, shutdown_tx,
listen_addr: actual_listen, listen_addr: actual_listen,
endpoint_id: None, endpoint_id: None,
@@ -692,7 +692,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
}) })
} }
ServeTransportMode::Iroh => { ServeTransportMode::Iroh => {
use wraith_core::transport::IrohAcceptor; use alknet_core::transport::IrohAcceptor;
let relay_url: Option<iroh::RelayUrl> = match options.iroh_relay.as_deref() { let relay_url: Option<iroh::RelayUrl> = match options.iroh_relay.as_deref() {
Some(u) => Some(u.parse().map_err(|e| { Some(u) => Some(u.parse().map_err(|e| {
@@ -736,7 +736,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
); );
let private_key = let private_key =
wraith_core::auth::keys::load_private_key(host_key_source).map_err(|e| { alknet_core::auth::keys::load_private_key(host_key_source).map_err(|e| {
napi::Error::new(napi::Status::InvalidArg, format!("host key error: {}", e)) napi::Error::new(napi::Status::InvalidArg, format!("host key error: {}", e))
})?; })?;
@@ -766,7 +766,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
.await; .await;
}); });
Ok(WraithServer { Ok(AlknetServer {
shutdown_tx, shutdown_tx,
listen_addr: String::new(), listen_addr: String::new(),
endpoint_id: Some(iroh_endpoint_id), endpoint_id: Some(iroh_endpoint_id),
@@ -836,7 +836,7 @@ async fn run_accept_loop<A>(
Some(ch) => { Some(ch) => {
let channel_stream = ch.into_stream(); let channel_stream = ch.into_stream();
let (read_half, write_half) = tokio::io::split(channel_stream); let (read_half, write_half) = tokio::io::split(channel_stream);
let server_stream = WraithServerStream { let server_stream = AlknetServerStream {
read: Arc::new(Mutex::new(read_half)), read: Arc::new(Mutex::new(read_half)),
write: Arc::new(Mutex::new(write_half)), write: Arc::new(Mutex::new(write_half)),
}; };

View File

@@ -1,23 +1,23 @@
[package] [package]
name = "wraith" name = "alknet"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
description = "CLI binary for Wraith: self-hostable SSH tunnel tool with pluggable transports" description = "CLI binary for Alknet: self-hostable SSH tunnel tool with pluggable transports"
repository.workspace = true repository.workspace = true
[[bin]] [[bin]]
name = "wraith" name = "alknet"
path = "src/main.rs" path = "src/main.rs"
[features] [features]
default = ["tls", "iroh"] default = ["tls", "iroh"]
tls = ["wraith-core/tls", "dep:rustls-pemfile", "dep:rustls-pki-types"] tls = ["alknet-core/tls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
iroh = ["wraith-core/iroh", "dep:iroh", "dep:url"] iroh = ["alknet-core/iroh", "dep:iroh", "dep:url"]
acme = ["wraith-core/acme", "dep:rustls-acme", "dep:rustls", "tls"] acme = ["alknet-core/acme", "dep:rustls-acme", "dep:rustls", "tls"]
[dependencies] [dependencies]
wraith-core = { path = "../wraith-core" } alknet-core = { path = "../alknet-core" }
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
anyhow = "1" anyhow = "1"

View File

@@ -1,10 +1,10 @@
//! # wraith //! # alknet
//! //!
//! CLI binary for [Wraith](https://git.alk.dev/alkdev/wraith), a self-hostable SSH-based tunnel //! CLI binary for [Alknet](https://git.alk.dev/alkdev/alknet), a self-hostable SSH-based tunnel
//! tool. Provides `wraith connect` (client) and `wraith serve` (server) subcommands with //! tool. Provides `alknet connect` (client) and `alknet serve` (server) subcommands with
//! pluggable transports (TCP, TLS, iroh). //! pluggable transports (TCP, TLS, iroh).
//! //!
//! > **Alpha software.** See `wraith-core` for library usage. //! > **Alpha software.** See `alknet-core` for library usage.
use std::net::SocketAddr; use std::net::SocketAddr;
use std::process; use std::process;
@@ -12,18 +12,18 @@ use std::sync::Arc;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use wraith_core::auth::keys::KeySource; use alknet_core::auth::keys::KeySource;
use wraith_core::client::{ConnectOptions, TransportMode}; use alknet_core::client::{ConnectOptions, TransportMode};
use wraith_core::server::{ServeOptions, ServeTransportMode, Server}; use alknet_core::server::{ServeOptions, ServeTransportMode, Server};
#[cfg(feature = "iroh")] #[cfg(feature = "iroh")]
use wraith_core::transport::IrohTransport; use alknet_core::transport::IrohTransport;
use wraith_core::transport::TcpTransport; use alknet_core::transport::TcpTransport;
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
use wraith_core::transport::TlsTransport; use alknet_core::transport::TlsTransport;
use wraith_core::transport::Transport; use alknet_core::transport::Transport;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "wraith", version, about = "Wraith SSH tunnel tool")] #[command(name = "alknet", version, about = "Alknet SSH tunnel tool")]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
@@ -32,13 +32,13 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
#[command( #[command(
about = "Connect to a wraith server and start a SOCKS5 proxy / port forwarding session" about = "Connect to an alknet server and start a SOCKS5 proxy / port forwarding session"
)] )]
Connect { Connect {
#[arg( #[arg(
long, long,
help = "TCP/TLS server address (required for tcp/tls transport)", help = "TCP/TLS server address (required for tcp/tls transport)",
env = "WRAITH_SERVER" env = "ALKNET_SERVER"
)] )]
server: Option<String>, server: Option<String>,
@@ -51,7 +51,7 @@ enum Commands {
#[arg(long, value_enum, default_value = "tcp", help = "Transport mode")] #[arg(long, value_enum, default_value = "tcp", help = "Transport mode")]
transport: TransportModeArg, transport: TransportModeArg,
#[arg(long, help = "SSH private key path", env = "WRAITH_IDENTITY")] #[arg(long, help = "SSH private key path", env = "ALKNET_IDENTITY")]
identity: Option<String>, identity: Option<String>,
#[arg(long, default_value = "127.0.0.1:1080", help = "SOCKS5 listen address")] #[arg(long, default_value = "127.0.0.1:1080", help = "SOCKS5 listen address")]
@@ -76,7 +76,7 @@ enum Commands {
insecure: bool, insecure: bool,
}, },
#[command(about = "Start the wraith server (accept SSH connections)")] #[command( about = "Start the alknet server (accept SSH connections)")]
Serve { Serve {
#[arg(long, help = "SSH host key path (required)")] #[arg(long, help = "SSH host key path (required)")]
key: String, key: String,
@@ -263,7 +263,7 @@ async fn run_connect(
insecure: bool, insecure: bool,
) -> Result<()> { ) -> Result<()> {
let identity_val = identity let identity_val = identity
.ok_or_else(|| anyhow!("--identity is required (or set WRAITH_IDENTITY env var)"))?; .ok_or_else(|| anyhow!("--identity is required (or set ALKNET_IDENTITY env var)"))?;
let key_source = KeySource::File(identity_val.into()); let key_source = KeySource::File(identity_val.into());
let transport_mode: TransportMode = transport.into(); let transport_mode: TransportMode = transport.into();
@@ -317,7 +317,7 @@ async fn run_connect(
#[cfg(not(feature = "tls"))] #[cfg(not(feature = "tls"))]
{ {
Err(anyhow!( Err(anyhow!(
"TLS transport is not available (wraith-core built without 'tls' feature)" "TLS transport is not available (alknet-core built without 'tls' feature)"
)) ))
} }
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
@@ -340,7 +340,7 @@ async fn run_connect(
#[cfg(not(feature = "iroh"))] #[cfg(not(feature = "iroh"))]
{ {
Err(anyhow!( Err(anyhow!(
"iroh transport is not available (wraith-core built without 'iroh' feature)" "iroh transport is not available (alknet-core built without 'iroh' feature)"
)) ))
} }
#[cfg(feature = "iroh")] #[cfg(feature = "iroh")]
@@ -375,7 +375,7 @@ async fn run_connect(
} }
async fn connect_and_run<T: Transport>(opts: ConnectOptions, transport: Arc<T>) -> Result<()> { async fn connect_and_run<T: Transport>(opts: ConnectOptions, transport: Arc<T>) -> Result<()> {
wraith_core::client::ClientSession::new(opts, transport) alknet_core::client::ClientSession::new(opts, transport)
.await .await
.map_err(|e| anyhow!("{e}"))? .map_err(|e| anyhow!("{e}"))?
.run() .run()
@@ -405,7 +405,7 @@ async fn run_serve(
#[cfg(not(feature = "acme"))] #[cfg(not(feature = "acme"))]
{ {
return Err(anyhow!( return Err(anyhow!(
"ACME support is not available (wraith built without 'acme' feature)" "ACME support is not available (alknet built without 'acme' feature)"
)); ));
} }
} }
@@ -454,7 +454,7 @@ async fn run_serve(
let addr: SocketAddr = listen let addr: SocketAddr = listen
.parse() .parse()
.map_err(|e| anyhow!("invalid listen address: {e}"))?; .map_err(|e| anyhow!("invalid listen address: {e}"))?;
let acceptor = wraith_core::transport::TcpAcceptor::bind(addr) let acceptor = alknet_core::transport::TcpAcceptor::bind(addr)
.await .await
.map_err(|e| anyhow!("bind failed: {e}"))?; .map_err(|e| anyhow!("bind failed: {e}"))?;
server.run(acceptor, None).await.map_err(|e| anyhow!("{e}")) server.run(acceptor, None).await.map_err(|e| anyhow!("{e}"))
@@ -463,7 +463,7 @@ async fn run_serve(
#[cfg(not(feature = "tls"))] #[cfg(not(feature = "tls"))]
{ {
Err(anyhow!( Err(anyhow!(
"TLS transport is not available (wraith-core built without 'tls' feature)" "TLS transport is not available (alknet-core built without 'tls' feature)"
)) ))
} }
#[cfg(feature = "acme")] #[cfg(feature = "acme")]
@@ -473,11 +473,11 @@ async fn run_serve(
.parse() .parse()
.map_err(|e| anyhow!("invalid listen address: {e}"))?; .map_err(|e| anyhow!("invalid listen address: {e}"))?;
let provider = Arc::new( let provider = Arc::new(
wraith_core::transport::AcmeCertProvider::domain(domain) alknet_core::transport::AcmeCertProvider::domain(domain)
.with_production_directory(), .with_production_directory(),
); );
let acceptor = let acceptor =
wraith_core::transport::AcmeTlsAcceptor::bind_acme(addr, provider) alknet_core::transport::AcmeTlsAcceptor::bind_acme(addr, provider)
.await .await
.map_err(|e| anyhow!("ACME bind failed: {e}"))?; .map_err(|e| anyhow!("ACME bind failed: {e}"))?;
return server.run(acceptor, None).await.map_err(|e| anyhow!("{e}")); return server.run(acceptor, None).await.map_err(|e| anyhow!("{e}"));
@@ -506,7 +506,7 @@ async fn run_serve(
let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut &key_data[..]) let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut &key_data[..])
.map_err(|e| anyhow!("failed to parse TLS private key: {e}"))? .map_err(|e| anyhow!("failed to parse TLS private key: {e}"))?
.ok_or_else(|| anyhow!("no private key found in {}", key_path))?; .ok_or_else(|| anyhow!("no private key found in {}", key_path))?;
let acceptor = wraith_core::transport::TlsAcceptor::bind(addr, certs, key, None) let acceptor = alknet_core::transport::TlsAcceptor::bind(addr, certs, key, None)
.await .await
.map_err(|e| anyhow!("TLS bind failed: {e}"))?; .map_err(|e| anyhow!("TLS bind failed: {e}"))?;
server.run(acceptor, None).await.map_err(|e| anyhow!("{e}")) server.run(acceptor, None).await.map_err(|e| anyhow!("{e}"))
@@ -516,7 +516,7 @@ async fn run_serve(
#[cfg(not(feature = "iroh"))] #[cfg(not(feature = "iroh"))]
{ {
Err(anyhow!( Err(anyhow!(
"iroh transport is not available (wraith-core built without 'iroh' feature)" "iroh transport is not available (alknet-core built without 'iroh' feature)"
)) ))
} }
#[cfg(feature = "iroh")] #[cfg(feature = "iroh")]
@@ -533,7 +533,7 @@ async fn run_serve(
Some(u) => Some(u.parse().map_err(|e| anyhow!("invalid proxy URL: {e}"))?), Some(u) => Some(u.parse().map_err(|e| anyhow!("invalid proxy URL: {e}"))?),
None => None, None => None,
}; };
let acceptor = wraith_core::transport::IrohAcceptor::bind(relay_url, proxy_url) let acceptor = alknet_core::transport::IrohAcceptor::bind(relay_url, proxy_url)
.await .await
.map_err(|e| anyhow!("iroh bind failed: {e}"))?; .map_err(|e| anyhow!("iroh bind failed: {e}"))?;
let endpoint_id = acceptor.endpoint_id(); let endpoint_id = acceptor.endpoint_id();

View File

@@ -3,7 +3,7 @@ status: draft
last_updated: 2026-06-04 last_updated: 2026-06-04
--- ---
# Wraith Architecture # Alknet Architecture
## Current State ## Current State

View File

@@ -15,7 +15,7 @@ shared across both auth paths. Identity resolution produces a transport-agnostic
## Why ## 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 auth. Non-SSH transports (WebTransport) cannot perform SSH key exchange — they
need a different auth presentation that shares the same key material. The need a different auth presentation that shares the same key material. The
unified auth layer ensures one key set, one identity, one rotation mechanism 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: 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: Server verification:
@@ -74,7 +74,7 @@ ADR-023.
### IdentityProvider Trait ### 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 storage. It resolves a key fingerprint or auth token to an `Identity` with
scopes and resources. scopes and resources.
@@ -103,7 +103,7 @@ default scope set. No database required.
`accounts` tables plus the ACL graph. Resolves fingerprint → account → `accounts` tables plus the ACL graph. Resolves fingerprint → account →
organization membership → effective scopes. Uses `ArcSwap` for hot reload. 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. depends on Honker, SQLite, or any specific database.
### AuthPolicy Structure ### AuthPolicy Structure
@@ -167,7 +167,7 @@ authorization decisions.
The wtransport library's `SessionRequest` provides: 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 ...`) - `headers()` — HTTP headers (for `Authorization: Bearer ...`)
- `origin()` — Browser origin (for CORS-like restrictions) - `origin()` — Browser origin (for CORS-like restrictions)
- `remote_address()` — Client UDP address - `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 - Auth tokens are Ed25519-signed with the same key pair used for SSH auth. No
separate key management for non-SSH transports. 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. storage. No database dependency at the core level.
- The SSH auth path is unchanged. `auth_publickey()` continues to work exactly - The SSH auth path is unchanged. `auth_publickey()` continues to work exactly
as it does today. Token auth is additive. as it does today. Token auth is additive.

View File

@@ -327,7 +327,7 @@ defined in `@alkdev/operations`. The TypeScript implementation provides:
The Rust implementation mirrors these types and behaviors. TypeScript consumers The Rust implementation mirrors these types and behaviors. TypeScript consumers
continue using `@alkdev/operations` over `@alkdev/pubsub` adapters (including 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. Both speak the same wire protocol and can interoperate.
The key principle: **the same `EventEnvelope` can flow from a Rust handler The key principle: **the same `EventEnvelope` can flow from a Rust handler

View File

@@ -7,11 +7,11 @@ last_updated: 2026-06-02
## What ## 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 ## 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 ## 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 │ │ │ │ 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: The client uses programmatic configuration — no `~/.ssh/config` parsing, no custom config files. Configuration comes from:
1. **CLI flags**: `--server`, `--identity`, `--transport`, etc. 1. **CLI flags**: `--server`, `--identity`, `--transport`, etc.
2. **Library API**: `ConnectOptions` and `ServeOptions` structs in `wraith-core`, constructable programmatically 2. **Library API**: `ConnectOptions` and `ServeOptions` structs in `alknet-core`, constructable programmatically
3. **Environment variables**: `WRAITH_SERVER`, `WRAITH_IDENTITY` as convenience defaults 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. 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: 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) - **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: 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 ```bash
# Basic connection (TCP, default port 22) # 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 # 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) # 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) # 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 # 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) # 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 # 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 # 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 # All options
wraith connect \ alknet connect \
--server <addr> \ # TCP/TLS server address (required for tcp/tls) --server <addr> \ # TCP/TLS server address (required for tcp/tls)
--peer <endpoint-id> \ # iroh endpoint ID, base58-encoded (required for iroh) --peer <endpoint-id> \ # iroh endpoint ID, base58-encoded (required for iroh)
--transport tcp|tls|iroh \ # Transport mode --transport tcp|tls|iroh \ # Transport mode
@@ -165,13 +165,13 @@ wraith connect \
## Constraints ## 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. - 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. - 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). - 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). - 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 ## Graceful Shutdown

View File

@@ -4,9 +4,9 @@
Accepted Accepted
## Context ## 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 ## 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()`. 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()`.

View File

@@ -4,18 +4,18 @@
Superseded by ADR-014 Superseded by ADR-014
## Context ## 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. 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 ## 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) 1. Creates a TUN device (requires root / CAP_NET_ADMIN)
2. Reads IP packets from it 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 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 ## Consequences
- **Positive**: Root-required code surface is tiny and auditable. - **Positive**: Root-required code surface is tiny and auditable.

View File

@@ -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**: Adding a new transport requires implementing the `Transport` trait, not modifying SSH code.
- **Positive**: Testing is straightforward — mock transports produce in-memory streams. - **Positive**: Testing is straightforward — mock transports produce in-memory streams.
- **Positive**: Security audit is clean — the SSH implementation has no network-facing code. - **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**: 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>`.) - **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>`.)

View File

@@ -24,14 +24,14 @@ SOCKS5 is the core because:
TUN forwards to SOCKS5 rather than directly to SSH because: TUN forwards to SOCKS5 rather than directly to SSH because:
- The SOCKS5 code already handles TCP connection establishment and bidirectional proxying - 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 - 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 - No root code in the core binary
## Consequences ## Consequences
- **Positive**: Core binary is root-free. TUN functionality is provided by the external `tun2proxy` tool (ADR-014). - **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**: 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. - **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. - **Negative**: SOCKS5 doesn't capture UDP (except DNS via SOCKS5h). TUN mode via tun2proxy handles this separately.
## References ## References

View File

@@ -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**: 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**: Reduces legal and privacy exposure for server operators.
- **Positive**: fail2ban can still work — it needs source IPs and auth failures, not destinations. - **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). - **Negative**: Debugging connectivity issues is harder without destination logs. Mitigated by client-side logging (the client knows what it's connecting to).
## References ## References

View File

@@ -4,7 +4,7 @@
Accepted Accepted
## Context ## 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. 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. 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.

View File

@@ -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. 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. 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 ## Decision
Support both ACME certificate provisioning paths: 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. 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 ## 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**: 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). - **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). - **Negative**: ACME adds complexity to the server binary (challenge responder, cert store, renewal timer).

View File

@@ -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. This matches iroh's own defaults — n0's relay is the standard starting point. Users who need production reliability self-host.
## Consequences ## 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. - **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**: 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. - **Negative**: Privacy-conscious users must remember to `--iroh-relay` to avoid n0. Mitigated by documentation.

View File

@@ -7,10 +7,10 @@ Accepted
Transport chaining allows combining iroh with an upstream proxy, e.g.: Transport chaining allows combining iroh with an upstream proxy, e.g.:
```bash ```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 - Nested tunnel topologies
- Environments where iroh needs to go through an existing proxy - Environments where iroh needs to go through an existing proxy
- Composing transports in flexible ways - Composing transports in flexible ways

View File

@@ -7,21 +7,21 @@ Accepted
The client and server both need configuration (host addresses, keys, transport options, etc.). There are several approaches: 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. 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.). 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 ## Decision
Option 3: Programmatic-first API. Configuration is provided via: Option 3: Programmatic-first API. Configuration is provided via:
- **CLI**: explicit flags (`--server`, `--identity`, `--transport`, etc.) - **CLI**: explicit flags (`--server`, `--identity`, `--transport`, etc.)
- **Library API**: `wraith_core::client::ConnectOptions` and `wraith_core::server::ServeOptions` structs, constructable programmatically - **Library API**: `alknet_core::client::ConnectOptions` and `alknet_core::server::ServeOptions` structs, constructable programmatically
- **Environment variables**: for a few convenience defaults (e.g., `WRAITH_SERVER`, `WRAITH_IDENTITY`) - **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.) - Avoids cross-platform path issues (`~` expansion, Windows `USERPROFILE`, etc.)
- Makes the library API clean and straightforward for programmatic consumers (NAPI wrapper, pubsub) - 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 - 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. 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.

View File

@@ -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. This ensures that even without fail2ban, the server rejects obviously abusive connections.
## Consequences ## 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**: Built-in rate limiting provides protection on platforms without fail2ban.
- **Positive**: No privacy-sensitive data in logs (no tunnel destinations). - **Positive**: No privacy-sensitive data in logs (no tunnel destinations).
- **Negative**: Slightly more code in the server for connection tracking per IP. - **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 ## References
- [server.md](../server.md) - [server.md](../server.md)

View File

@@ -4,7 +4,7 @@
Accepted Accepted
## Context ## 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: However, TUN implementation has significant complexities:
- Platform differences (Linux TUN, macOS utun, Windows wintun.dll) - 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. 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 ## 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) 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 `wraith connect` 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 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 ## Consequences
- **Positive**: Significantly reduces project scope and complexity. No TUN code to write, test, or maintain across platforms. - **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**: tun2proxy is already well-tested for this exact use case.
- **Positive**: Core binary remains unprivileged. No root code anywhere in the project. - **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**: 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**: 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. - **Negative**: No first-class Windows/macOS TUN story. tun2proxy handles these platforms but users need to install it separately.

View File

@@ -4,35 +4,35 @@
Accepted Accepted
## Context ## 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. 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 wraith server from Node.js. Used by hubs that want to accept wraith connections and route events. 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 ## Decision
The NAPI wrapper exposes both `connect()` and `serve()` from the start: The NAPI wrapper exposes both `connect()` and `serve()` from the start:
```typescript ```typescript
// @alkdev/wraith // @alkdev/alknet
function connect(options: WraithConnectOptions): Promise<Duplex>; function connect(options: AlknetConnectOptions): Promise<Duplex>;
function serve(options: WraithServeOptions): Promise<WraithServer>; function serve(options: AlknetServeOptions): Promise<AlknetServer>;
``` ```
- `connect()` returns a `Duplex` stream (as per ADR-007) - `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. 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 ## Consequences
- **Positive**: Pubsub can use both directions without running a separate binary for the server side. - **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**: 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]`. - **Positive**: Implementation is still minimal — `serve()` is just `alknet_core::server::run()` behind `#[napi]`.
- **Negative**: Slightly larger API surface (two functions + `WraithServer` type instead of just `connect()`). - **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 `WraithServer`. - **Negative**: Server-side NAPI needs to handle multiple concurrent connections, which adds complexity to `AlknetServer`.
## References ## References
- [napi-and-pubsub.md](../napi-and-pubsub.md) - [napi-and-pubsub.md](../napi-and-pubsub.md)

View File

@@ -4,7 +4,7 @@
Accepted Accepted
## Context ## 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. 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. The fake response uses `Server: nginx` headers to match the most common web server profile.
## Consequences ## 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**: Simple implementation — just peek at the first bytes and branch.
- **Positive**: Consistent with censorship circumvention best practices. - **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. - **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.

View File

@@ -4,22 +4,22 @@
Accepted Accepted
## Context ## 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: 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`). 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`. 3. **Custom channel type**: Define a new SSH channel type beyond `direct_tcpip` and `forwarded_tcpip`.
## Decision ## 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 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 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 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. 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 ## Consequences
- **Positive**: Simple implementation — just string matching in the server's `channel_open_direct_tcpip` handler. - **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**: Compatible with the NAPI wrapper's single-duplex-stream model.
- **Positive**: Port forwarding to a specific port is still available as an alternative. - **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**: The string `alknet-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**: Regular TCP destinations accidentally matching `alknet-control` would be misrouted. Mitigated by reserving the entire `alknet-` prefix namespace.
## References ## References
- [napi-and-pubsub.md](../napi-and-pubsub.md) - [napi-and-pubsub.md](../napi-and-pubsub.md)

View File

@@ -4,11 +4,11 @@
Accepted Accepted
## Context ## 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. 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.

View File

@@ -5,7 +5,7 @@ Accepted
## Context ## 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, 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 iroh) because SSH carries its own auth protocol. But WebTransport and other
HTTP-level transports cannot perform SSH key exchange — browsers speak HTTP/3, 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 same person connecting via SSH and WebTransport appears as two different
identities. identities.
The `IdentityProvider` trait is needed to decouple wraith-core from any The `IdentityProvider` trait is needed to decouple alknet-core from any
specific identity storage (config file vs. database). Without it, wraith-core specific identity storage (config file vs. database). Without it, alknet-core
would either hardcode config-file-based auth or take a database dependency — would either hardcode config-file-based auth or take a database dependency —
neither is acceptable for a library crate. 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` Server extracts the fingerprint, looks it up in the same `authorized_keys`
set, verifies the signature, and checks the timestamp window (default ±300s). 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 trait resolves a fingerprint or token to an `Identity`. Default implementation
loads from `DynamicConfig.auth` (no database). Hub implementation can back it loads from `DynamicConfig.auth` (no database). Hub implementation can back it
with `@alkdev/storage`. with `@alkdev/storage`.
@@ -60,7 +60,7 @@ the key material.
- **Positive**: One key set, one rotation, one `reloadAuth()` call. Adding a - **Positive**: One key set, one rotation, one `reloadAuth()` call. Adding a
key to `authorized_keys` immediately grants access via both SSH and key to `authorized_keys` immediately grants access via both SSH and
WebTransport. 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`. specific database. Default: config file. Hub: `@alkdev/storage`.
- **Positive**: Browser clients can authenticate using Ed25519 keys via - **Positive**: Browser clients can authenticate using Ed25519 keys via
SubtleCrypto (Chrome 105+, Firefox 130+, Safari 17+). Deno supports it SubtleCrypto (Chrome 105+, Firefox 130+, Safari 17+). Deno supports it

View File

@@ -5,7 +5,7 @@ Accepted
## Context ## 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 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 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 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 (`/services/list`, `/services/schema`). Spoke operations are prefixed
(`/dev1/fs/readFile`). (`/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 a transport for `EventEnvelope` frames with call protocol semantics, instead of
raw pubsub dispatch. raw pubsub dispatch.

View File

@@ -5,7 +5,7 @@ Accepted
## Context ## 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 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 `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 channel routing. The NAPI handler would need to intercept channel data outside

View File

@@ -7,14 +7,14 @@ last_updated: 2026-06-02
## What ## 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 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 wraith's SSH channel 2. **PubSub event target** (`@alkdev/pubsub` adapter) — An implementation of the `TypedEventTarget` interface that routes events over alknet's SSH channel
## Why ## 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. 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): The wrapper uses napi-rs (ADR-015) and exposes two functions (ADR-016):
```typescript ```typescript
// @alkdev/wraith (TypeScript side) // @alkdev/alknet (TypeScript side)
interface WraithConnectOptions { interface AlknetConnectOptions {
// TCP/TLS mode // TCP/TLS mode
server?: string; // e.g., "example.com:443" server?: string; // e.g., "example.com:443"
// iroh mode // iroh mode
@@ -45,7 +45,7 @@ interface WraithConnectOptions {
proxy?: string; // upstream SOCKS5/HTTP proxy URL proxy?: string; // upstream SOCKS5/HTTP proxy URL
} }
interface WraithServeOptions { interface AlknetServeOptions {
// Transport // Transport
transport: 'tcp' | 'tls' | 'iroh'; transport: 'tcp' | 'tls' | 'iroh';
// Auth // Auth
@@ -63,12 +63,12 @@ interface WraithServeOptions {
} }
// Returns a Duplex stream for the SSH channel // 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 // 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>; close(): Promise<void>;
onConnection(callback: (stream: Duplex, info: ConnectionInfo) => void): 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. 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. - **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) ### 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. 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`: This implements `TypedEventTarget` from `@alkdev/pubsub`:
```typescript ```typescript
// @alkdev/pubsub (new adapter: event-target-wraith.ts) // @alkdev/pubsub (new adapter: event-target-alknet.ts)
export interface WraithEventTargetOptions { export interface AlknetEventTargetOptions {
stream: Duplex; // from @alkdev/wraith.connect() or serve() stream: Duplex; // from @alkdev/alknet.connect() or serve()
} }
export interface WraithEventTarget<TEvent extends TypedEvent> export interface AlknetEventTarget<TEvent extends TypedEvent>
extends TypedEventTarget<TEvent> { extends TypedEventTarget<TEvent> {
close(): void; close(): void;
} }
export function createWraithEventTarget<TEvent extends TypedEvent>( export function createAlknetEventTarget<TEvent extends TypedEvent>(
options: WraithEventTargetOptions options: AlknetEventTargetOptions
): WraithEventTarget<TEvent>; ): AlknetEventTarget<TEvent>;
``` ```
Wire protocol (same as other pubsub adapters): Wire protocol (same as other pubsub adapters):
@@ -121,20 +121,20 @@ Wire protocol (same as other pubsub adapters):
### On the Server Side ### 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 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 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 ### 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` - **Worker connects to hub**: `alknet 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 - **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. 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 | | [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 | | [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 | | [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 |

View File

@@ -66,14 +66,14 @@ last_updated: 2026-06-04
- **Origin**: [tun-shim.md](tun-shim.md) - **Origin**: [tun-shim.md](tun-shim.md)
- **Status**: ~~resolved~~ - **Status**: ~~resolved~~
- **Priority**: ~~low~~ - **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) - **Cross-references**: [ADR-014](decisions/014-defer-tun-recommend-socks5-proxy.md)
### OQ-09: TCP reconstruction approach for TUN ### OQ-09: TCP reconstruction approach for TUN
- **Origin**: [tun-shim.md](tun-shim.md) - **Origin**: [tun-shim.md](tun-shim.md)
- **Status**: ~~resolved~~ - **Status**: ~~resolved~~
- **Priority**: ~~medium~~ - **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) - **Cross-references**: [ADR-014](decisions/014-defer-tun-recommend-socks5-proxy.md)
## NAPI / PubSub ## NAPI / PubSub
@@ -122,7 +122,7 @@ last_updated: 2026-06-04
- **Resolution**: (pending — needs R&D in WebTransport transport session) - **Resolution**: (pending — needs R&D in WebTransport transport session)
- **Cross-references**: [auth.md](auth.md), OQ-19 - **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) - **Origin**: [research/configuration.md](../research/configuration.md)
- **Status**: open - **Status**: open
- **Priority**: low - **Priority**: low
@@ -133,7 +133,7 @@ last_updated: 2026-06-04
- **Origin**: [research/configuration.md](../research/configuration.md) - **Origin**: [research/configuration.md](../research/configuration.md)
- **Status**: ~~resolved~~ - **Status**: ~~resolved~~
- **Priority**: ~~medium~~ - **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 - **Cross-references**: [ADR-023](decisions/023-unified-auth-shared-key-material.md), [auth.md](auth.md), OQ-15
## Auth ## Auth

View File

@@ -3,33 +3,33 @@ status: reviewed
last_updated: 2026-06-02 last_updated: 2026-06-02
--- ---
# Wraith Overview # Alknet Overview
## Purpose ## 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 - **Private tunneling** of services (Postgres, Redis, internal APIs) over SSH
- **Censorship circumvention** — SSH over TLS on port 443 looks like HTTPS to DPI - **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 - **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 - **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 ## Exports
### Binary: `wraith` ### Binary: `alknet`
A single binary with subcommands: A single binary with subcommands:
``` ```
wraith serve — Start the server (accepts SSH connections) alknet serve — Start the server (accepts SSH connections)
wraith connect — Start the client (opens SSH session, exposes SOCKS5/port-forwards) 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 - `Transport` trait — produces a duplex stream for SSH to run over
- `TcpTransport` — direct TCP connection - `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) 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) 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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |
## Open Questions ## Open Questions

View File

@@ -7,7 +7,7 @@ last_updated: 2026-06-02
## What ## 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 ## 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) │ │ │ │ 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)`: 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: **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` - Return `Accept` or `Reject`
**Channel handling (`channel_open_direct_tcpip`)**: **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) - 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 - Spawn a bidirectional proxy task between the SSH channel and the outbound TCP stream
- Return the channel for data flow - Return the channel for data flow
@@ -161,44 +161,44 @@ These provide abuse protection on platforms without fail2ban (macOS, Windows, BS
```bash ```bash
# Basic server (SSH on port 22) # 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) # With TLS (manual certs)
wraith serve --key ~/.ssh/ssh_host_ed25519_key \ alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--transport tls \ --transport tls \
--tls-cert /etc/ssl/cert.pem \ --tls-cert /etc/ssl/cert.pem \
--tls-key /etc/ssl/key.pem --tls-key /etc/ssl/key.pem
# With TLS (auto ACME, domain-based) # With TLS (auto ACME, domain-based)
wraith serve --key ~/.ssh/ssh_host_ed25519_key \ alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--transport tls \ --transport tls \
--acme-domain example.com --acme-domain example.com
# With TLS + stealth (fake nginx 404 to scanners) # 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 \ --transport tls \
--acme-domain example.com \ --acme-domain example.com \
--stealth --stealth
# With iroh transport (no public IP needed) # 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 --transport iroh
# With outbound proxy # 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 --proxy socks5://127.0.0.1:9050
# With certificate authority authentication # With certificate authority authentication
wraith serve --key ~/.ssh/ssh_host_ed25519_key \ alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--cert-authority /etc/wraith/ca.pub --cert-authority /etc/alknet/ca.pub
# With rate limiting # 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-connections-per-ip 5 \
--max-auth-attempts 3 --max-auth-attempts 3
# All options # All options
wraith serve \ alknet serve \
--key <path-or-buffer> \ # SSH host key (required) --key <path-or-buffer> \ # SSH host key (required)
--authorized-keys <path> \ # Authorized keys file --authorized-keys <path> \ # Authorized keys file
--cert-authority <path> \ # CA public key for cert-auth --cert-authority <path> \ # CA public key for cert-auth
@@ -218,7 +218,7 @@ wraith serve \
When running with `--transport iroh`, the server: 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 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 3. Accepts incoming connections on the endpoint
4. For each connection, accepts a bidirectional stream and passes it to `server::run_stream()` 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 ## Constraints
- The server does not log tunnel destinations (ADR-006). Auth events and connection events are logged for fail2ban integration (ADR-013). - 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). - 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. - 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. - 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 | | [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 | | [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 | | [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 | | [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |

View File

@@ -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): Transports can be nested. The CLI supports `--transport iroh --proxy socks5://...` natively (ADR-010):
```bash ```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. 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.

View File

@@ -5,21 +5,21 @@ last_updated: 2026-06-01
# TUN Shim (Deprecated) # 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 ## 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 ```bash
# Terminal 1: wraith SOCKS5 proxy (no root required) # Terminal 1: alknet SOCKS5 proxy (no root required)
wraith connect --server example.com --identity ~/.ssh/id_ed25519 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 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 ## References

View File

@@ -8,33 +8,33 @@ phase: exploration
## Problem ## 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: three specific failures:
1. **No hot reload of authentication credentials.** Adding or removing an 1. **No hot reload of authentication credentials.** Adding or removing an
authorized key requires restarting the server process. In a hub/spoke authorized key requires restarting the server process. In a hub/spoke
deployment where keys are managed via a database (see 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 restarted every time a key is added, revoked, or rotated. This is
operationally unacceptable for a production service. operationally unacceptable for a production service.
2. **No port forwarding access control.** Any authenticated client can open a 2. **No port forwarding access control.** Any authenticated client can open a
`direct-tcpip` channel to any destination. There is no policy governing `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 is a security gap — a compromised key grants unrestricted network access
through the tunnel. through the tunnel.
3. **No structured configuration beyond CLI flags.** ADR-011 chose 3. **No structured configuration beyond CLI flags.** ADR-011 chose
programmatic-first configuration for the alpha. This was correct — it programmatic-first configuration for the alpha. This was correct — it
avoided cross-platform path issues and kept the API surface small. But as 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 reproducible deployments, and the NAPI layer needs programmatic reload
capability that the current `ServeOptions` builder pattern doesn't support. capability that the current `ServeOptions` builder pattern doesn't support.
### What's Not The Problem ### What's Not The Problem
- This does not propose depending on Honker, SQLite, or any specific data - 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. data sources plug in from outside.
- This does not propose file-watching (potential attack vector, unnecessary - This does not propose file-watching (potential attack vector, unnecessary
complexity). CLI usage loads config once at startup. Programmatic usage complexity). CLI usage loads config once at startup. Programmatic usage
@@ -131,7 +131,7 @@ pub enum TargetPattern {
Host(String), Host(String),
Cidr(IpNetwork), Cidr(IpNetwork),
PortRange(String, Range<u16>), PortRange(String, Range<u16>),
WraithPrefix, AlknetPrefix,
} }
``` ```
@@ -169,14 +169,14 @@ max_connections_per_ip = 5
max_auth_attempts = 3 max_auth_attempts = 3
[server.tls] [server.tls]
cert = "/etc/wraith/tls/cert.pem" cert = "/etc/alknet/tls/cert.pem"
key = "/etc/wraith/tls/key.pem" key = "/etc/alknet/tls/key.pem"
[server.iroh] [server.iroh]
relay = "https://relay.alk.dev" relay = "https://relay.alk.dev"
[auth] [auth]
host_key = "/etc/wraith/ssh/host_key" host_key = "/etc/alknet/ssh/host_key"
[forwarding] [forwarding]
default = "deny" default = "deny"
@@ -186,7 +186,7 @@ target = "localhost:*"
action = "allow" action = "allow"
[[forwarding.rules]] [[forwarding.rules]]
target = "wraith-*" target = "alknet-*"
action = "allow" action = "allow"
[[forwarding.rules]] [[forwarding.rules]]
@@ -202,7 +202,7 @@ Rules are evaluated in order; first match wins.
The NAPI layer exposes the reload handle: The NAPI layer exposes the reload handle:
```typescript ```typescript
interface WraithServer { interface AlknetServer {
reloadAuth(auth: { authorizedKeys?: Buffer, certAuthority?: Buffer }): void; reloadAuth(auth: { authorizedKeys?: Buffer, certAuthority?: Buffer }): void;
reloadForwarding(policy: ForwardingPolicyConfig): void; reloadForwarding(policy: ForwardingPolicyConfig): void;
reloadAll(config: DynamicConfig): void; reloadAll(config: DynamicConfig): void;
@@ -214,7 +214,7 @@ interface ForwardingPolicyConfig {
} }
interface ForwardingRuleConfig { 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'; action: 'allow' | 'deny';
principals?: string[]; // default ["*"] 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()` | | Core Rust | `StaticConfig` struct | `ArcSwap<DynamicConfig>` | `ConfigReloadHandle::reload()` |
| NAPI | `serve()` options | Same `ArcSwap` | `server.reloadAuth()`, `server.reloadForwarding()` | | 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 command line, restarting is fine. The reload mechanism exists for programmatic
consumers that manage credentials in a database. consumers that manage credentials in a database.
@@ -337,8 +337,8 @@ listen = "0.0.0.0:443"
stealth = true stealth = true
[listeners.tls] [listeners.tls]
cert = "/etc/wraith/tls/cert.pem" cert = "/etc/alknet/tls/cert.pem"
key = "/etc/wraith/tls/key.pem" key = "/etc/alknet/tls/key.pem"
[[listeners]] [[listeners]]
transport = "tcp" 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 # WebTransport shares port 443 with TLS because QUIC is UDP, TLS is TCP
[listeners.webtransport] [listeners.webtransport]
cert = "/etc/wraith/tls/cert.pem" cert = "/etc/alknet/tls/cert.pem"
key = "/etc/wraith/tls/key.pem" key = "/etc/alknet/tls/key.pem"
``` ```
The `[[listeners]]` array-of-tables pattern means each listener is an 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. listeners.
### Transport Kind and WebTransport ### 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 this enum is straightforward — WebTransport connections are identifiable at
accept time. The handler behavior is the same (port forwarding only), but accept time. The handler behavior is the same (port forwarding only), but
the tag enables transport-specific logging and future policy differences 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 ## Proposed Solution
@@ -396,9 +396,9 @@ the tag enables transport-specific logging and future policy differences
2. Replace `Arc<ServerAuthConfig>` in `ServerHandler` with 2. Replace `Arc<ServerAuthConfig>` in `ServerHandler` with
`Arc<ArcSwap<DynamicConfig>>` `Arc<ArcSwap<DynamicConfig>>`
3. Add `ConfigReloadHandle` with `reload(DynamicConfig)` method 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 **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` 1. Add `ForwardingPolicy` to `DynamicConfig`
2. Add policy check to `channel_open_direct_tcpip` before proxy spawn 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 **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 3. Config file only covers static config + initial auth config path
4. Add `serde` derive to `StaticConfig` 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 **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 2. `--profile production` loads named profile
3. CLI flags override profile values 3. CLI flags override profile values
**Scope**: `wraith-cli` **Scope**: `alknet-cli`
**Risk**: Low — convenience layer only **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; 6. Add `WebTransport` to `TransportKind` enum (initially as a tag only;
actual WebTransport acceptor is a separate R&D phase) 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 **Risk**: Medium — changes the primary API surface of `serve()`. Backwards
compat via accepting both `transport: string` (single) and 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. Initially no — all transports get the same port-forwarding-only handler.
But WebTransport connections come from browsers, which have different trust But WebTransport connections come from browsers, which have different trust
assumptions. A future forwarding policy might restrict WebTransport clients 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 This is a policy question, not a transport question. The `TransportKind` tag
on the handler enables transport-aware policy rules in `ForwardingPolicy` on the handler enables transport-aware policy rules in `ForwardingPolicy`
without changing the handler. Defer to Phase 2 (forwarding policy design). without changing the handler. Defer to Phase 2 (forwarding policy design).

View File

@@ -19,7 +19,7 @@ No password auth. The client handler is simpler than the server — it just need
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/auth/client_auth.rs` exports `ClientAuthConfig` and client handler - [ ] `crates/alknet-core/src/auth/client_auth.rs` exports `ClientAuthConfig` and client handler
- [ ] `ClientAuthConfig` holds: `private_key: KeyPair`, optional `public_key: PublicKey` - [ ] `ClientAuthConfig` holds: `private_key: KeyPair`, optional `public_key: PublicKey`
- [ ] `ClientAuthConfig::from_key_source(source: KeySource) -> Result<Self>` — loads key via key-loading module - [ ] `ClientAuthConfig::from_key_source(source: KeySource) -> Result<Self>` — loads key via key-loading module
- [ ] Implements `russh::client::Handler` with `auth_publickey()` returning the public key - [ ] Implements `russh::client::Handler` with `auth_publickey()` returning the public key

View File

@@ -22,14 +22,14 @@ Use `thiserror` for structured error types propagated via `anyhow::Result` in th
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/error.rs` exports error types - [ ] `crates/alknet-core/src/error.rs` exports error types
- [ ] `TransportError` enum: `ConnectionFailed`, `HandshakeFailed`, `Timeout`, `ProxyFailed` - [ ] `TransportError` enum: `ConnectionFailed`, `HandshakeFailed`, `Timeout`, `ProxyFailed`
- [ ] `AuthError` enum: `KeyRejected`, `CertInvalid`, `CertExpired`, `CertPrincipalMismatch`, `NoMatchingKey` - [ ] `AuthError` enum: `KeyRejected`, `CertInvalid`, `CertExpired`, `CertPrincipalMismatch`, `NoMatchingKey`
- [ ] `ChannelError` enum: `TargetUnreachable`, `ProxyConnectFailed`, `ChannelClosed` - [ ] `ChannelError` enum: `TargetUnreachable`, `ProxyConnectFailed`, `ChannelClosed`
- [ ] `ConfigError` enum: `InvalidFlag`, `KeyFileNotFound`, `BindFailed`, `IncompatibleOptions` - [ ] `ConfigError` enum: `InvalidFlag`, `KeyFileNotFound`, `BindFailed`, `IncompatibleOptions`
- [ ] All error types implement `std::error::Error` via `thiserror`, `Display`, and `Debug` - [ ] All error types implement `std::error::Error` via `thiserror`, `Display`, and `Debug`
- [ ] Error types have `source` chaining where appropriate (e.g., `TransportError::HandshakeFailed { source: std::io::Error }`) - [ ] Error types have `source` chaining where appropriate (e.g., `TransportError::HandshakeFailed { source: std::io::Error }`)
- [ ] Re-exported from `crates/wraith-core/src/lib.rs` - [ ] Re-exported from `crates/alknet-core/src/lib.rs`
- [ ] Unit tests for Display output of each error variant - [ ] Unit tests for Display output of each error variant
## References ## References

View File

@@ -25,7 +25,7 @@ All keys must be in **OpenSSH key format** (not PEM/PKCS#1/PKCS#8). This module
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/auth/keys.rs` exports key loading functions - [ ] `crates/alknet-core/src/auth/keys.rs` exports key loading functions
- [ ] `KeySource` enum: `File(PathBuf)` and `Memory(Vec<u8>)` for unified key input handling - [ ] `KeySource` enum: `File(PathBuf)` and `Memory(Vec<u8>)` for unified key input handling
- [ ] `load_private_key(source: KeySource) -> Result<russh::key::KeyPair>` — loads OpenSSH private key from file or memory - [ ] `load_private_key(source: KeySource) -> Result<russh::key::KeyPair>` — loads OpenSSH private key from file or memory
- [ ] `load_public_keys(source: KeySource) -> Result<Vec<russh::key::PublicKey>>` — loads one or more public keys from authorized_keys format - [ ] `load_public_keys(source: KeySource) -> Result<Vec<russh::key::PublicKey>>` — loads one or more public keys from authorized_keys format

View File

@@ -22,7 +22,7 @@ No password authentication over SSH. This is the `russh::server::Handler::auth_p
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/auth/server_auth.rs` exports `ServerAuthConfig` and auth logic - [ ] `crates/alknet-core/src/auth/server_auth.rs` exports `ServerAuthConfig` and auth logic
- [ ] `ServerAuthConfig` holds: `authorized_keys: HashSet<PublicKey>`, `cert_authorities: Vec<CertAuthorityEntry>` - [ ] `ServerAuthConfig` holds: `authorized_keys: HashSet<PublicKey>`, `cert_authorities: Vec<CertAuthorityEntry>`
- [ ] `ServerAuthConfig::from_keys_and_ca()` constructor: loads authorized keys and cert-authority entries from provided key sources - [ ] `ServerAuthConfig::from_keys_and_ca()` constructor: loads authorized keys and cert-authority entries from provided key sources
- [ ] Auth check function: given a presented key/certificate, return `Accept` or `Reject` - [ ] Auth check function: given a presented key/certificate, return `Accept` or `Reject`

View File

@@ -1,6 +1,6 @@
--- ---
id: cli/connect-command id: cli/connect-command
name: Implement `wraith connect` CLI subcommand with clap name: Implement `alknet connect` CLI subcommand with clap
status: pending status: pending
depends_on: depends_on:
- client/connect-options - client/connect-options
@@ -12,15 +12,15 @@ level: implementation
## Description ## Description
Implement the `wraith connect` CLI subcommand using `clap` with derive macros. Translates `ConnectOptions` into CLI flags and runs the client session. All options from client.md CLI interface must be supported. Implement the `alknet connect` CLI subcommand using `clap` with derive macros. Translates `ConnectOptions` into CLI flags and runs the client session. All options from client.md CLI interface must be supported.
Environment variable defaults: `WRAITH_SERVER`, `WRAITH_IDENTITY` as convenience defaults per ADR-011. Environment variable defaults: `ALKNET_SERVER`, `ALKNET_IDENTITY` as convenience defaults per ADR-011.
`--proxy` with `--transport tcp` should warn or be a no-op (ADR-019: client proxy wraps transport, and TCP transport is already direct). `--proxy` with `--transport tcp` should warn or be a no-op (ADR-019: client proxy wraps transport, and TCP transport is already direct).
## Acceptance Criteria ## Acceptance Criteria
- [ ] `wraith connect` subcommand flags match client.md CLI interface: `--server`, `--peer`, `--transport`, `--identity`, `--socks5`, `--forward`, `--remote-forward`, `--proxy`, `--iroh-relay`, `--tls-server-name`, `--insecure` - [ ] `alknet connect` subcommand flags match client.md CLI interface: `--server`, `--peer`, `--transport`, `--identity`, `--socks5`, `--forward`, `--remote-forward`, `--proxy`, `--iroh-relay`, `--tls-server-name`, `--insecure`
- [ ] `--server` required for tcp/tls transport (validated at parse time or runtime) - [ ] `--server` required for tcp/tls transport (validated at parse time or runtime)
- [ ] `--peer` required for iroh transport (validated) - [ ] `--peer` required for iroh transport (validated)
- [ ] `--identity` required for all transports - [ ] `--identity` required for all transports
@@ -28,11 +28,11 @@ Environment variable defaults: `WRAITH_SERVER`, `WRAITH_IDENTITY` as convenience
- [ ] `--socks5` defaults to `127.0.0.1:1080` - [ ] `--socks5` defaults to `127.0.0.1:1080`
- [ ] `--forward` is repeatable (clap `multiple_occurrences`) - [ ] `--forward` is repeatable (clap `multiple_occurrences`)
- [ ] `--remote-forward` is repeatable - [ ] `--remote-forward` is repeatable
- [ ] Environment variable defaults: `WRAITH_SERVER` for `--server`, `WRAITH_IDENTITY` for `--identity` - [ ] Environment variable defaults: `ALKNET_SERVER` for `--server`, `ALKNET_IDENTITY` for `--identity`
- [ ] `--proxy` with `--transport tcp` prints warning (ADR-019: effectively no-op) - [ ] `--proxy` with `--transport tcp` prints warning (ADR-019: effectively no-op)
- [ ] CLI translates args into `ConnectOptions` and calls `ClientSession::new(opts).run().await` - [ ] CLI translates args into `ConnectOptions` and calls `ClientSession::new(opts).run().await`
- [ ] Errors reported to stderr with non-zero exit code - [ ] Errors reported to stderr with non-zero exit code
- [ ] `cargo run -p wraith -- connect --help` shows all flags with descriptions - [ ] `cargo run -p alknet -- connect --help` shows all flags with descriptions
## References ## References

View File

@@ -1,6 +1,6 @@
--- ---
id: cli/serve-command id: cli/serve-command
name: Implement `wraith serve` CLI subcommand with clap name: Implement `alknet serve` CLI subcommand with clap
status: completed status: completed
depends_on: depends_on:
- server/serve-loop - server/serve-loop
@@ -12,16 +12,16 @@ level: implementation
## Description ## Description
Implement the `wraith serve` CLI subcommand using `clap` with derive macros. This translates `ServeOptions` into CLI flags and runs the server. All options from server.md CLI interface must be supported. Implement the `alknet serve` CLI subcommand using `clap` with derive macros. This translates `ServeOptions` into CLI flags and runs the server. All options from server.md CLI interface must be supported.
Environment variable defaults: none mandated for serve, but consistent with programmatic-first API. Environment variable defaults: none mandated for serve, but consistent with programmatic-first API.
The binary is the `wraith` crate at `crates/wraith/src/main.rs`. The binary is the `alknet` crate at `crates/alknet/src/main.rs`.
## Acceptance Criteria ## Acceptance Criteria
- [x] `crates/wraith/src/main.rs` defines CLI with clap derive: `wraith` with `serve` and `connect` subcommands (connect stub for now) - [x] `crates/alknet/src/main.rs` defines CLI with clap derive: `alknet` with `serve` and `connect` subcommands (connect stub for now)
- [x] `wraith serve` subcommand flags match server.md CLI interface exactly: `--key`, `--authorized-keys`, `--cert-authority`, `--transport`, `--listen`, `--tls-cert`, `--tls-key`, `--acme-domain`, `--stealth`, `--proxy`, `--iroh-relay`, `--max-connections-per-ip`, `--max-auth-attempts` - [x] `alknet serve` subcommand flags match server.md CLI interface exactly: `--key`, `--authorized-keys`, `--cert-authority`, `--transport`, `--listen`, `--tls-cert`, `--tls-key`, `--acme-domain`, `--stealth`, `--proxy`, `--iroh-relay`, `--max-connections-per-ip`, `--max-auth-attempts`
- [x] `--key` is required (no default) - [x] `--key` is required (no default)
- [x] `--transport` defaults to `tcp` - [x] `--transport` defaults to `tcp`
- [x] `--listen` defaults to `0.0.0.0:22` - [x] `--listen` defaults to `0.0.0.0:22`
@@ -31,7 +31,7 @@ The binary is the `wraith` crate at `crates/wraith/src/main.rs`.
- [x] Key inputs accept file paths (strings); in-memory key data is a library/API concern, not CLI - [x] Key inputs accept file paths (strings); in-memory key data is a library/API concern, not CLI
- [x] CLI translates args into `ServeOptions` and calls `Server::new(opts).run().await` - [x] CLI translates args into `ServeOptions` and calls `Server::new(opts).run().await`
- [x] Errors reported to stderr with non-zero exit code - [x] Errors reported to stderr with non-zero exit code
- [x] `cargo run -p wraith -- serve --help` shows all flags with descriptions - [x] `cargo run -p alknet -- serve --help` shows all flags with descriptions
## References ## References
@@ -44,4 +44,4 @@ All 12 CLI flags implemented. ServeTransportModeArg ValueEnum maps to ServeTrans
## Summary ## Summary
Implemented wraith serve CLI subcommand with all server.md flags. Clap derive with ServeTransportModeArg, stealth validation, ACME feature gate, iroh endpoint ID printing. Build/clippy/test pass across all feature combinations. Implemented alknet serve CLI subcommand with all server.md flags. Clap derive with ServeTransportModeArg, stealth validation, ACME feature gate, iroh endpoint ID printing. Build/clippy/test pass across all feature combinations.

View File

@@ -32,7 +32,7 @@ Reconnection is always enabled. The backoff caps at 30 seconds and continues ind
## Acceptance Criteria ## Acceptance Criteria
- [x] `crates/wraith-core/src/client/channel_manager.rs` exports `ChannelManager` - [x] `crates/alknet-core/src/client/channel_manager.rs` exports `ChannelManager`
- [x] `ChannelManager` holds: `Arc<Transport>`, `Arc<ClientAuthConfig>`, `Arc<client::Handle<ClientHandler>>` (behind RwLock for reconnection) - [x] `ChannelManager` holds: `Arc<Transport>`, `Arc<ClientAuthConfig>`, `Arc<client::Handle<ClientHandler>>` (behind RwLock for reconnection)
- [x] `ChannelManager::new()` establishes initial transport connection, authenticates, returns manager - [x] `ChannelManager::new()` establishes initial transport connection, authenticates, returns manager
- [x] `open_direct_tcpip(host, port)` — opens SSH channel to target - [x] `open_direct_tcpip(host, port)` — opens SSH channel to target
@@ -61,4 +61,4 @@ Reconnection is always enabled. The backoff caps at 30 seconds and continues ind
## Summary ## Summary
Implemented `ChannelManager` in `crates/wraith-core/src/client/channel_manager.rs` with SSH session management, channel opens (`open_direct_tcpip`), port forward requests (`request_tcpip_forward`/`cancel_tcpip_forward`), and automatic reconnection with exponential backoff (1s→30s cap). Full reconnect per ADR-004 creates new transport stream + new SSH session. Port forwards are re-registered after successful reconnect. 8 unit tests covering backoff timing, forward tracking, transport failure, and reconnection detection. Implemented `ChannelManager` in `crates/alknet-core/src/client/channel_manager.rs` with SSH session management, channel opens (`open_direct_tcpip`), port forward requests (`request_tcpip_forward`/`cancel_tcpip_forward`), and automatic reconnection with exponential backoff (1s→30s cap). Full reconnect per ADR-004 creates new transport stream + new SSH session. Port forwards are re-registered after successful reconnect. 8 unit tests covering backoff timing, forward tracking, transport failure, and reconnection detection.

View File

@@ -34,7 +34,7 @@ Graceful shutdown (SIGTERM/SIGINT):
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/client/mod.rs` re-exports all client components - [ ] `crates/alknet-core/src/client/mod.rs` re-exports all client components
- [ ] `ConnectOptions` struct with fields matching client.md CLI interface: `server`, `peer`, `transport_mode`, `identity`, `socks5_addr`, `forwards`, `remote_forwards`, `proxy`, `iroh_relay`, `tls_server_name`, `insecure` - [ ] `ConnectOptions` struct with fields matching client.md CLI interface: `server`, `peer`, `transport_mode`, `identity`, `socks5_addr`, `forwards`, `remote_forwards`, `proxy`, `iroh_relay`, `tls_server_name`, `insecure`
- [ ] `ConnectOptions::identity` accepts `KeySource` (file or in-memory) - [ ] `ConnectOptions::identity` accepts `KeySource` (file or in-memory)
- [ ] `ClientSession::new(opts: ConnectOptions) -> Result<Self>` — creates transport, connects, authenticates - [ ] `ClientSession::new(opts: ConnectOptions) -> Result<Self>` — creates transport, connects, authenticates

View File

@@ -30,7 +30,7 @@ Both types are specified as repeatable `--forward` / `--remote-forward` CLI opti
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/client/forward.rs` exports `PortForwardSpec`, `LocalForwarder`, `RemoteForwarder` - [ ] `crates/alknet-core/src/client/forward.rs` exports `PortForwardSpec`, `LocalForwarder`, `RemoteForwarder`
- [ ] `PortForwardSpec` parses `-L` / `-R` spec strings: `local_addr:local_port:remote_host:remote_port` - [ ] `PortForwardSpec` parses `-L` / `-R` spec strings: `local_addr:local_port:remote_host:remote_port`
- [ ] `LocalForwarder` binds TcpListener, accepts connections, opens SSH direct-tcpip channel for each, proxies bidirectionally - [ ] `LocalForwarder` binds TcpListener, accepts connections, opens SSH direct-tcpip channel for each, proxies bidirectionally
- [ ] `RemoteForwarder` sends `tcpip_forward` request, handles `forwarded-tcpip` channel opens, connects to local target, proxies bidirectionally - [ ] `RemoteForwarder` sends `tcpip_forward` request, handles `forwarded-tcpip` channel opens, connects to local target, proxies bidirectionally

View File

@@ -25,14 +25,14 @@ Supports SOCKS5h (domain names resolved server-side) by default. This prevents D
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/socks5/mod.rs` exports `Socks5Server` - [ ] `crates/alknet-core/src/socks5/mod.rs` exports `Socks5Server`
- [ ] `Socks5Server` binds to configurable listen address (default `127.0.0.1:1080`) - [ ] `Socks5Server` binds to configurable listen address (default `127.0.0.1:1080`)
- [ ] SOCKS5 handshake: method negotiation (no-auth only), target address parsing (IPv4, IPv6, domain name) - [ ] SOCKS5 handshake: method negotiation (no-auth only), target address parsing (IPv4, IPv6, domain name)
- [ ] Domain name targets (SOCKS5h) sent unresolved to server — no local DNS resolution - [ ] Domain name targets (SOCKS5h) sent unresolved to server — no local DNS resolution
- [ ] For each SOCKS5 connection, opens SSH `direct_tcpip` channel and proxies bytes bidirectionally - [ ] For each SOCKS5 connection, opens SSH `direct_tcpip` channel and proxies bytes bidirectionally
- [ ] Connection errors (SSH session down, channel open failed) result in SOCKS5 error response to client - [ ] Connection errors (SSH session down, channel open failed) result in SOCKS5 error response to client
- [ ] No logging of SOCKS5 request targets (ADR-006) — only connection-level events logged - [ ] No logging of SOCKS5 request targets (ADR-006) — only connection-level events logged
- [ ] SOCKS5 server always enabled when `wraith connect` runs (per client.md constraint) - [ ] SOCKS5 server always enabled when `alknet connect` runs (per client.md constraint)
- [ ] Unit tests: SOCKS5 handshake parsing, address type handling, bidirectional proxy flow (with mock transport) - [ ] Unit tests: SOCKS5 handshake parsing, address type handling, bidirectional proxy flow (with mock transport)
## References ## References

View File

@@ -1,6 +1,6 @@
--- ---
id: meta/cli-layer id: meta/cli-layer
name: Complete CLI layer — wraith serve and wraith connect commands name: Complete CLI layer — alknet serve and alknet connect commands
status: completed status: completed
depends_on: depends_on:
- cli/serve-command - cli/serve-command
@@ -13,13 +13,13 @@ level: planning
## Description ## Description
Meta task that clusters CLI tasks. Once complete, the `wraith` binary has both `serve` and `connect` subcommands with all flags matching the architecture specs. Meta task that clusters CLI tasks. Once complete, the `alknet` binary has both `serve` and `connect` subcommands with all flags matching the architecture specs.
## Acceptance Criteria ## Acceptance Criteria
- [x] Both CLI tasks completed - [x] Both CLI tasks completed
- [x] `wraith serve --help` and `wraith connect --help` match architecture spec flag lists - [x] `alknet serve --help` and `alknet connect --help` match architecture spec flag lists
- [x] End-to-end: `wraith serve` + `wraith connect` establishes working SSH tunnel - [x] End-to-end: `alknet serve` + `alknet connect` establishes working SSH tunnel
## References ## References
@@ -27,4 +27,4 @@ Meta task that clusters CLI tasks. Once complete, the `wraith` binary has both `
## Summary ## Summary
CLI layer complete. Both `wraith serve` and `wraith connect` subcommands implemented with all architecture spec flags. CLI layer complete. Both `alknet serve` and `alknet connect` subcommands implemented with all architecture spec flags.

View File

@@ -14,13 +14,13 @@ level: planning
## Description ## Description
Meta task that clusters NAPI tasks. Once complete, the `@alkdev/wraith` Node.js native addon provides `connect()` and `serve()` returning duplex streams for TypeScript consumers. Meta task that clusters NAPI tasks. Once complete, the `@alkdev/alknet` Node.js native addon provides `connect()` and `serve()` returning duplex streams for TypeScript consumers.
## Acceptance Criteria ## Acceptance Criteria
- [x] All NAPI tasks completed - [x] All NAPI tasks completed
- [x] `connect()` returns Duplex stream, no SOCKS5, no port forwarding - [x] `connect()` returns Duplex stream, no SOCKS5, no port forwarding
- [x] `serve()` returns WraithServer with close() and onConnection events - [x] `serve()` returns AlknetServer with close() and onConnection events
- [x] Key material from Buffer (in-memory) and file paths both work - [x] Key material from Buffer (in-memory) and file paths both work
- [x] JS-to-Rust and Rust-to-JS error marshalling works correctly - [x] JS-to-Rust and Rust-to-JS error marshalling works correctly
@@ -30,4 +30,4 @@ Meta task that clusters NAPI tasks. Once complete, the `@alkdev/wraith` Node.js
## Summary ## Summary
NAPI layer complete. connect() returns WraithStream (read/write/close), serve() returns WraithServer with close()/onConnection(). Key material works from both file paths and in-memory Buffers. TCP transport fully supported; TLS/iroh return helpful errors in NAPI layer. NAPI layer complete. connect() returns AlknetStream (read/write/close), serve() returns AlknetServer with close()/onConnection(). Key material works from both file paths and in-memory Buffers. TCP transport fully supported; TLS/iroh return helpful errors in NAPI layer.

View File

@@ -17,7 +17,7 @@ level: planning
## Description ## Description
Meta task that clusters all server module tasks. Once complete, the server accepts SSH connections via any transport, authenticates clients, proxies channel traffic to TCP targets (directly or via proxy), handles stealth mode, rate limits connections, routes reserved `wraith-` destinations, and shuts down gracefully. Meta task that clusters all server module tasks. Once complete, the server accepts SSH connections via any transport, authenticates clients, proxies channel traffic to TCP targets (directly or via proxy), handles stealth mode, rate limits connections, routes reserved `alknet-` destinations, and shuts down gracefully.
## Acceptance Criteria ## Acceptance Criteria
@@ -27,7 +27,7 @@ Meta task that clusters all server module tasks. Once complete, the server accep
- [x] Channel proxying with direct, SOCKS5, and HTTP CONNECT outbound modes - [x] Channel proxying with direct, SOCKS5, and HTTP CONNECT outbound modes
- [x] Stealth mode detects SSH vs HTTP and returns fake nginx 404 - [x] Stealth mode detects SSH vs HTTP and returns fake nginx 404
- [x] Rate limiting and structured logging - [x] Rate limiting and structured logging
- [x] Control channel routing for `wraith-*` destinations - [x] Control channel routing for `alknet-*` destinations
- [x] Graceful shutdown - [x] Graceful shutdown
## References ## References
@@ -40,4 +40,4 @@ All server module tasks completed across Gens 4-7. Server layer is fully impleme
## Summary ## Summary
Server layer complete: handler (auth + channel dispatch), channel proxy (direct/SOCKS5/HTTP CONNECT), stealth mode (protocol multiplexing), rate limiting (per-IP connection limits), control channel (wraith-* destination routing), serve loop (accept loop + graceful shutdown). All 229 tests pass. Server layer complete: handler (auth + channel dispatch), channel proxy (direct/SOCKS5/HTTP CONNECT), stealth mode (protocol multiplexing), rate limiting (per-IP connection limits), control channel (alknet-* destination routing), serve loop (accept loop + graceful shutdown). All 229 tests pass.

View File

@@ -13,23 +13,23 @@ level: implementation
## Description ## Description
Implement the NAPI `connect()` function per ADR-007. This is fundamentally different from CLI `wraith connect`: Implement the NAPI `connect()` function per ADR-007. This is fundamentally different from CLI `alknet connect`:
- **NAPI `connect()`**: Opens a single SSH channel and returns it as a Node.js `Duplex` stream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly. - **NAPI `connect()`**: Opens a single SSH channel and returns it as a Node.js `Duplex` stream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly.
- **CLI `wraith connect`**: Full SSH client session with SOCKS5 server and port forwarding. - **CLI `alknet connect`**: Full SSH client session with SOCKS5 server and port forwarding.
The function accepts `WraithConnectOptions` and returns `Promise<Duplex>`. The NAPI layer handles transport selection, SSH authentication, and channel setup, then hands the caller a stream. The function accepts `AlknetConnectOptions` and returns `Promise<Duplex>`. The NAPI layer handles transport selection, SSH authentication, and channel setup, then hands the caller a stream.
## Acceptance Criteria ## Acceptance Criteria
- [ ] `#[napi]` function `connect(options: WraithConnectOptions) -> Result<DuplexStream>` in `crates/wraith-napi/src/connect.rs` - [ ] `#[napi]` function `connect(options: AlknetConnectOptions) -> Result<DuplexStream>` in `crates/alknet-napi/src/connect.rs`
- [ ] `WraithConnectOptions` struct with napi fields: `server`, `peer`, `transport`, `identity`, `tlsServerName`, `insecure`, `irohRelay`, `proxy` - [ ] `AlknetConnectOptions` struct with napi fields: `server`, `peer`, `transport`, `identity`, `tlsServerName`, `insecure`, `irohRelay`, `proxy`
- [ ] Transport creation from options (tcp, tls, iroh) — same logic as CLI but programmatic - [ ] Transport creation from options (tcp, tls, iroh) — same logic as CLI but programmatic
- [ ] SSH client connection: create transport stream, authenticate, open single `direct_tcpip` channel - [ ] SSH client connection: create transport stream, authenticate, open single `direct_tcpip` channel
- [ ] Channel returned as `napi::DuplexStream` for JavaScript consumption - [ ] Channel returned as `napi::DuplexStream` for JavaScript consumption
- [ ] Key material: `identity` field accepts file path (string) or `Buffer` (in-memory data) per ADR-011 - [ ] Key material: `identity` field accepts file path (string) or `Buffer` (in-memory data) per ADR-011
- [ ] Error marshalling: Rust errors become JavaScript exceptions with descriptive messages - [ ] Error marshalling: Rust errors become JavaScript exceptions with descriptive messages
- [ ] TypeScript type: `(options: WraithConnectOptions) => Promise<Duplex>` - [ ] TypeScript type: `(options: AlknetConnectOptions) => Promise<Duplex>`
- [ ] Integration test from JS: connect to a test server, write/receive bytes through stream - [ ] Integration test from JS: connect to a test server, write/receive bytes through stream
## References ## References

View File

@@ -1,6 +1,6 @@
--- ---
id: napi/project-setup id: napi/project-setup
name: Set up wraith-napi project with napi-rs build tooling and TypeScript types name: Set up alknet-napi project with napi-rs build tooling and TypeScript types
status: pending status: pending
depends_on: depends_on:
- setup/project-init - setup/project-init
@@ -12,7 +12,7 @@ level: implementation
## Description ## Description
Set up the napi-rs project for the `@alkdev/wraith` Node.js native addon. This includes the napi-rs build configuration, TypeScript type definitions, and the package structure. Set up the napi-rs project for the `@alkdev/alknet` Node.js native addon. This includes the napi-rs build configuration, TypeScript type definitions, and the package structure.
Per ADR-015 and ADR-016: napi-rs is the FFI bridge, and the wrapper exposes `connect()` and `serve()` functions. The NAPI layer is transport-agnostic — it doesn't know about pubsub's `EventEnvelope`. Per ADR-015 and ADR-016: napi-rs is the FFI bridge, and the wrapper exposes `connect()` and `serve()` functions. The NAPI layer is transport-agnostic — it doesn't know about pubsub's `EventEnvelope`.
@@ -20,11 +20,11 @@ The Cargo.toml skeleton was created in setup/project-init. This task configures
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-napi/` has `Cargo.toml` with `crate-type = ["cdylib"]`, `napi` and `napi-derive` dependencies - [ ] `crates/alknet-napi/` has `Cargo.toml` with `crate-type = ["cdylib"]`, `napi` and `napi-derive` dependencies
- [ ] `crates/wraith-napi/src/lib.rs` with napi module registration - [ ] `crates/alknet-napi/src/lib.rs` with napi module registration
- [ ] `packages/wraith-napi/` directory (or similar) with `package.json` named `@alkdev/wraith` - [ ] `packages/alknet-napi/` directory (or similar) with `package.json` named `@alkdev/alknet`
- [ ] `packages/wraith-napi/tsconfig.json` for TypeScript type generation - [ ] `packages/alknet-napi/tsconfig.json` for TypeScript type generation
- [ ] TypeScript type definitions for `WraithConnectOptions`, `WraithServeOptions`, `WraithServer`, `ConnectionInfo` matching napi-and-pubsub.md interfaces - [ ] TypeScript type definitions for `AlknetConnectOptions`, `AlknetServeOptions`, `AlknetServer`, `ConnectionInfo` matching napi-and-pubsub.md interfaces
- [ ] `napi.config.js` or `NapiRs.config` with correct cargo path, module name - [ ] `napi.config.js` or `NapiRs.config` with correct cargo path, module name
- [ ] Build command: `npm run build` builds the native addon - [ ] Build command: `npm run build` builds the native addon
- [ ] Feature flags: `iroh` feature optional; base package includes tcp + tls - [ ] Feature flags: `iroh` feature optional; base package includes tcp + tls

View File

@@ -13,15 +13,15 @@ level: implementation
## Description ## Description
Implement the NAPI `serve()` function per ADR-016. Returns a `WraithServer` object with a `close()` method and `onConnection` event emitter. Each incoming SSH connection produces a `Duplex` stream. Implement the NAPI `serve()` function per ADR-016. Returns a `AlknetServer` object with a `close()` method and `onConnection` event emitter. Each incoming SSH connection produces a `Duplex` stream.
The function accepts `WraithServeOptions` and returns `Promise<WraithServer>`. The NAPI layer handles transport binding, SSH server setup, and connection handling. The function accepts `AlknetServeOptions` and returns `Promise<AlknetServer>`. The NAPI layer handles transport binding, SSH server setup, and connection handling.
## Acceptance Criteria ## Acceptance Criteria
- [x] `#[napi]` function `serve(options: WraithServeOptions) -> Result<WraithServer>` in `crates/wraith-napi/src/serve.rs` - [x] `#[napi]` function `serve(options: AlknetServeOptions) -> Result<AlknetServer>` in `crates/alknet-napi/src/serve.rs`
- [x] `WraithServeOptions` struct with napi fields: `transport`, `hostKey`, `authorizedKeys`, `certAuthority`, `tlsCert`, `tlsKey`, `acmeDomain`, `listen`, `irohRelay` - [x] `AlknetServeOptions` struct with napi fields: `transport`, `hostKey`, `authorizedKeys`, `certAuthority`, `tlsCert`, `tlsKey`, `acmeDomain`, `listen`, `irohRelay`
- [x] `WraithServer` napi class with `close() -> Promise<void>` and `onConnection(callback)` event registration - [x] `AlknetServer` napi class with `close() -> Promise<void>` and `onConnection(callback)` event registration
- [x] Each incoming connection produces a `Duplex` stream via the `onConnection` callback - [x] Each incoming connection produces a `Duplex` stream via the `onConnection` callback
- [x] `ConnectionInfo` struct passed with each connection: `remoteAddr`, `transportKind` - [x] `ConnectionInfo` struct passed with each connection: `remoteAddr`, `transportKind`
- [x] Key material: `hostKey`, `authorizedKeys` accept file path (string) or `Buffer` (in-memory) - [x] Key material: `hostKey`, `authorizedKeys` accept file path (string) or `Buffer` (in-memory)
@@ -32,14 +32,14 @@ The function accepts `WraithServeOptions` and returns `Promise<WraithServer>`. T
## References ## References
- docs/architecture/napi-and-pubsub.md — NAPI serve() spec, WraithServer interface - docs/architecture/napi-and-pubsub.md — NAPI serve() spec, AlknetServer interface
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve() - docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
- docs/architecture/server.md — server configuration - docs/architecture/server.md — server configuration
## Notes ## Notes
TCP transport fully implemented. TLS/iroh transports return helpful "not yet supported" errors. WraithServerStream provides read/write/close. ConnectionInfo includes remoteAddr and transportKind. TCP transport fully implemented. TLS/iroh transports return helpful "not yet supported" errors. AlknetServerStream provides read/write/close. ConnectionInfo includes remoteAddr and transportKind.
## Summary ## Summary
Implemented NAPI serve() in crates/wraith-napi/src/serve.rs: WraithServeOptions, WraithServer with close()/onConnection(), WraithServerStream (Duplex read/write/close), ConnectionInfo. TCP transport works end-to-end. 241 tests pass, clippy clean. Implemented NAPI serve() in crates/alknet-napi/src/serve.rs: AlknetServeOptions, AlknetServer with close()/onConnection(), AlknetServerStream (Duplex read/write/close), ConnectionInfo. TCP transport works end-to-end. 241 tests pass, clippy clean.

View File

@@ -14,21 +14,21 @@ level: review
## Description ## Description
Final review of the complete wraith system. Verify CLI binary works end-to-end, NAPI wrapper provides correct JavaScript API, and both layers properly wrap the core library. Final review of the complete alknet system. Verify CLI binary works end-to-end, NAPI wrapper provides correct JavaScript API, and both layers properly wrap the core library.
## Acceptance Criteria ## Acceptance Criteria
- [x] `wraith serve` + `wraith connect` end-to-end: SSH tunnel established, SOCKS5 proxy routes traffic - [x] `alknet serve` + `alknet connect` end-to-end: SSH tunnel established, SOCKS5 proxy routes traffic
- [x] All CLI flags work: transport modes (tcp, tls, iroh), auth options, proxy, stealth, rate limits - [x] All CLI flags work: transport modes (tcp, tls, iroh), auth options, proxy, stealth, rate limits
- [x] Environment variables (`WRAITH_SERVER`, `WRAITH_IDENTITY`) work as defaults - [x] Environment variables (`ALKNET_SERVER`, `ALKNET_IDENTITY`) work as defaults
- [x] `--stealth` validates `--transport tls` requirement - [x] `--stealth` validates `--transport tls` requirement
- [x] NAPI `connect()` returns Duplex stream; data flows bidirectionally - [x] NAPI `connect()` returns Duplex stream; data flows bidirectionally
- [x] NAPI `serve()` accepts connections; `onConnection` emits Duplex streams - [x] NAPI `serve()` accepts connections; `onConnection` emits Duplex streams
- [x] NAPI key material from Buffer works (not just file paths) - [x] NAPI key material from Buffer works (not just file paths)
- [x] Feature flags: `tls`, `iroh`, `acme` correctly gate optional functionality - [x] Feature flags: `tls`, `iroh`, `acme` correctly gate optional functionality
- [x] Base build (`cargo build -p wraith-core` with no features) compiles and works - [x] Base build (`cargo build -p alknet-core` with no features) compiles and works
- [x] All tests pass: `cargo test --workspace` - [x] All tests pass: `cargo test --workspace`
- [x] NAPI tests pass: `cd crates/wraith-napi && npm test` - [x] NAPI tests pass: `cd crates/alknet-napi && npm test`
- [x] `cargo clippy --workspace` passes - [x] `cargo clippy --workspace` passes
- [x] No logging of tunnel destinations anywhere in the system - [x] No logging of tunnel destinations anywhere in the system
@@ -39,8 +39,8 @@ Final review of the complete wraith system. Verify CLI binary works end-to-end,
## Summary ## Summary
Final review complete. All acceptance criteria verified: Final review complete. All acceptance criteria verified:
- CLI binary: wraith serve/connect with all flags, env vars, stealth validation - CLI binary: alknet serve/connect with all flags, env vars, stealth validation
- NAPI: connect() returns WraithStream, serve() returns WraithServer with onConnection - NAPI: connect() returns AlknetStream, serve() returns AlknetServer with onConnection
- Feature flags: tls, iroh, acme correctly gate optional code; base build compiles - Feature flags: tls, iroh, acme correctly gate optional code; base build compiles
- ADR-006: no server-side logging of tunnel destinations - ADR-006: no server-side logging of tunnel destinations
- 241 tests pass, clippy clean with all features - 241 tests pass, clippy clean with all features

View File

@@ -28,7 +28,7 @@ Verify end-to-end SSH tunnel flow: client connects → SOCKS5 proxy works → po
- [x] Logging: structured `tracing::info!` events match ADR-013 format - [x] Logging: structured `tracing::info!` events match ADR-013 format
- [x] No logging of tunnel destinations (ADR-006) - [x] No logging of tunnel destinations (ADR-006)
- [x] Reconnection: transport failure → exponential backoff → reconnect → port forwards re-registered - [x] Reconnection: transport failure → exponential backoff → reconnect → port forwards re-registered
- [x] Reserved `wraith-` destinations routed to control channel, not TCP proxy - [x] Reserved `alknet-` destinations routed to control channel, not TCP proxy
- [x] Graceful shutdown works for both server and client - [x] Graceful shutdown works for both server and client
- [x] All tests pass: `cargo test --workspace` - [x] All tests pass: `cargo test --workspace`
- [x] `cargo clippy --workspace` passes - [x] `cargo clippy --workspace` passes
@@ -40,7 +40,7 @@ Verify end-to-end SSH tunnel flow: client connects → SOCKS5 proxy works → po
## Summary ## Summary
Server and client review passed with fixes. Key issues found and resolved: Server and client review passed with fixes. Key issues found and resolved:
- wired channel proxy into handler (was dropping all non-wraith channels) - wired channel proxy into handler (was dropping all non-alknet channels)
- added client reconnection with exponential backoff + remote forward re-registration - added client reconnection with exponential backoff + remote forward re-registration
- fixed ADR-006 violations (removed server-side destination logging) - fixed ADR-006 violations (removed server-side destination logging)
- 241 tests pass, clippy clean - 241 tests pass, clippy clean

View File

@@ -23,7 +23,7 @@ Supports three outbound proxy modes per server.md: Direct, SOCKS5 proxy, HTTP CO
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/server/channel_proxy.rs` exports channel proxy functions - [ ] `crates/alknet-core/src/server/channel_proxy.rs` exports channel proxy functions
- [ ] `ProxyConfig` enum: `Direct`, `Socks5 { addr: SocketAddr }`, `HttpConnect { addr: SocketAddr }` - [ ] `ProxyConfig` enum: `Direct`, `Socks5 { addr: SocketAddr }`, `HttpConnect { addr: SocketAddr }`
- [ ] `connect_outbound(target: SocketAddr, proxy: &ProxyConfig) -> Result<TcpStream>` — connects to target directly or via proxy - [ ] `connect_outbound(target: SocketAddr, proxy: &ProxyConfig) -> Result<TcpStream>` — connects to target directly or via proxy
- [ ] Direct mode: `TcpStream::connect(target)` - [ ] Direct mode: `TcpStream::connect(target)`

View File

@@ -1,6 +1,6 @@
--- ---
id: server/control-channel id: server/control-channel
name: Implement wraith-control reserved channel for pubsub event bus bridging (ADR-018) name: Implement alknet-control reserved channel for pubsub event bus bridging (ADR-018)
status: pending status: pending
depends_on: depends_on:
- server/handler - server/handler
@@ -13,26 +13,26 @@ level: implementation
## Description ## Description
Implement the control channel routing per ADR-018. When the server receives a `channel_open_direct_tcpip` request for `wraith-control:0`: Implement the control channel routing per ADR-018. When the server receives a `channel_open_direct_tcpip` request for `alknet-control:0`:
1. The handler detects the reserved `wraith-` prefix destination 1. The handler detects the reserved `alknet-` prefix destination
2. Instead of making a TCP connection, it bridges the SSH channel to an internal event bus handle 2. Instead of making a TCP connection, it bridges the SSH channel to an internal event bus handle
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel 3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
The entire `wraith-` prefix is reserved — no TCP connections should be attempted for `wraith-*` destinations. The control channel is optional; servers without pubsub configured should accept the channel and provide a configurable behavior (reject or provide a loopback pipe). The entire `alknet-` prefix is reserved — no TCP connections should be attempted for `alknet-*` destinations. The control channel is optional; servers without pubsub configured should accept the channel and provide a configurable behavior (reject or provide a loopback pipe).
At this stage, implement the routing logic and a `ControlChannel` trait that consumers can implement. The actual pubsub bridge implementation would be in a separate crate or behind a feature flag. At this stage, implement the routing logic and a `ControlChannel` trait that consumers can implement. The actual pubsub bridge implementation would be in a separate crate or behind a feature flag.
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/server/control_channel.rs` exports `ControlChannelHandler` trait and routing logic - [ ] `crates/alknet-core/src/server/control_channel.rs` exports `ControlChannelHandler` trait and routing logic
- [ ] `WRAITH_CONTROL_DESTINATION` constant defined as `"wraith-control"` (ADR-018) - [ ] `ALKNET_CONTROL_DESTINATION` constant defined as `"alknet-control"` (ADR-018)
- [ ] `WRAITH_PREFIX` constant defined as `"wraith-"` for namespace reservation - [ ] `ALKNET_PREFIX` constant defined as `"alknet-"` for namespace reservation
- [ ] `ControlChannelHandler` trait: `async fn handle_channel(stream: Box<dyn AsyncRead + AsyncWrite + Unpin + Send>)` - [ ] `ControlChannelHandler` trait: `async fn handle_channel(stream: Box<dyn AsyncRead + AsyncWrite + Unpin + Send>)`
- [ ] Server handler detects `wraith-*` prefix and routes to `ControlChannelHandler` instead of TCP proxy - [ ] Server handler detects `alknet-*` prefix and routes to `ControlChannelHandler` instead of TCP proxy
- [ ] If no `ControlChannelHandler` configured, reject the channel open request (SSH channel open failure) - [ ] If no `ControlChannelHandler` configured, reject the channel open request (SSH channel open failure)
- [ ] Non-reserved destinations continue through normal TCP proxy path - [ ] Non-reserved destinations continue through normal TCP proxy path
- [ ] Server constraint enforced: no TCP connections to `wraith-*` destinations - [ ] Server constraint enforced: no TCP connections to `alknet-*` destinations
- [ ] Unit tests: reserved destination detected, non-reserved passes through, prefix matching works - [ ] Unit tests: reserved destination detected, non-reserved passes through, prefix matching works
## References ## References

View File

@@ -16,17 +16,17 @@ level: implementation
Implement the core `ServerHandler` that implements `russh::server::Handler`. This is the heart of the server. Per server.md, it has two primary responsibilities: Implement the core `ServerHandler` that implements `russh::server::Handler`. This is the heart of the server. Per server.md, it has two primary responsibilities:
1. **`auth_publickey()`**: Delegated to `ServerAuthConfig` — checks key against authorized set or validates cert-authority 1. **`auth_publickey()`**: Delegated to `ServerAuthConfig` — checks key against authorized set or validates cert-authority
2. **`channel_open_direct_tcpip()`**: Routes the channel — either to a TCP target (directly or via proxy) or internally for reserved `wraith-*` destinations (ADR-018) 2. **`channel_open_direct_tcpip()`**: Routes the channel — either to a TCP target (directly or via proxy) or internally for reserved `alknet-*` destinations (ADR-018)
At this stage, implement the handler struct, auth delegation, and the channel dispatch skeleton (actual TCP connection and proxy logic in dependent tasks). At this stage, implement the handler struct, auth delegation, and the channel dispatch skeleton (actual TCP connection and proxy logic in dependent tasks).
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/server/handler.rs` exports `ServerHandler` - [ ] `crates/alknet-core/src/server/handler.rs` exports `ServerHandler`
- [ ] `ServerHandler` implements `russh::server::Handler` - [ ] `ServerHandler` implements `russh::server::Handler`
- [ ] `ServerHandler` holds: `Arc<ServerAuthConfig>`, `outbound_proxy: Option<ProxyConfig>`, `remote_addr: Option<SocketAddr>` - [ ] `ServerHandler` holds: `Arc<ServerAuthConfig>`, `outbound_proxy: Option<ProxyConfig>`, `remote_addr: Option<SocketAddr>`
- [ ] `auth_publickey()` delegates to `ServerAuthConfig` and returns `Accept` or `Reject` - [ ] `auth_publickey()` delegates to `ServerAuthConfig` and returns `Accept` or `Reject`
- [ ] `channel_open_direct_tcpip()` dispatches: if `host.starts_with("wraith-")`, route to internal handler (stub for control channel); otherwise, spawn TCP proxy task (stub that logs and returns error for now) - [ ] `channel_open_direct_tcpip()` dispatches: if `host.starts_with("alknet-")`, route to internal handler (stub for control channel); otherwise, spawn TCP proxy task (stub that logs and returns error for now)
- [ ] One `ServerHandler` instance per connection; state is not shared between connections (unless explicitly Arc'd) - [ ] One `ServerHandler` instance per connection; state is not shared between connections (unless explicitly Arc'd)
- [ ] Structured auth logging via `tracing::info!` with `remote_addr`, `key_fingerprint`, `result` (ADR-013) - [ ] Structured auth logging via `tracing::info!` with `remote_addr`, `key_fingerprint`, `result` (ADR-013)
- [ ] Unit tests: auth delegation works, reserved destination routing logic, unknown channel types rejected - [ ] Unit tests: auth delegation works, reserved destination routing logic, unknown channel types rejected
@@ -34,7 +34,7 @@ At this stage, implement the handler struct, auth delegation, and the channel di
## References ## References
- docs/architecture/server.md — Server Handler Behavior section, channel handling - docs/architecture/server.md — Server Handler Behavior section, channel handling
- docs/architecture/decisions/018-control-channel-for-pubsub.md — reserved `wraith-*` destinations - docs/architecture/decisions/018-control-channel-for-pubsub.md — reserved `alknet-*` destinations
- docs/architecture/decisions/013-fail2ban-friendly-logging.md — structured auth logging - docs/architecture/decisions/013-fail2ban-friendly-logging.md — structured auth logging
## Notes ## Notes

View File

@@ -21,7 +21,7 @@ No logging of tunnel destinations, DNS resolutions, or bytes transferred (ADR-00
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/server/rate_limit.rs` exports connection rate limiter - [ ] `crates/alknet-core/src/server/rate_limit.rs` exports connection rate limiter
- [ ] `ConnectionRateLimiter` tracks active connections per IP using `HashMap<IpAddr, usize>` - [ ] `ConnectionRateLimiter` tracks active connections per IP using `HashMap<IpAddr, usize>`
- [ ] `ConnectionRateLimiter::check(ip) -> bool` — returns `true` if connection allowed, `false` if over limit - [ ] `ConnectionRateLimiter::check(ip) -> bool` — returns `true` if connection allowed, `false` if over limit
- [ ] `ConnectionRateLimiter::on_connect(ip)` — increment counter - [ ] `ConnectionRateLimiter::on_connect(ip)` — increment counter

View File

@@ -26,7 +26,7 @@ Implement the server's main accept loop and configuration. This ties together th
## Acceptance Criteria ## Acceptance Criteria
- [x] `crates/wraith-core/src/server/mod.rs` re-exports all server components - [x] `crates/alknet-core/src/server/mod.rs` re-exports all server components
- [x] `ServeOptions` struct with fields matching server.md CLI interface: `key`, `authorized_keys`, `cert_authority`, `transport_mode`, `listen_addr`, `tls_cert`, `tls_key`, `acme_domain`, `stealth`, `proxy`, `iroh_relay`, `max_connections_per_ip`, `max_auth_attempts` - [x] `ServeOptions` struct with fields matching server.md CLI interface: `key`, `authorized_keys`, `cert_authority`, `transport_mode`, `listen_addr`, `tls_cert`, `tls_key`, `acme_domain`, `stealth`, `proxy`, `iroh_relay`, `max_connections_per_ip`, `max_auth_attempts`
- [x] `Server::new(opts: ServeOptions) -> Result<Server>` — creates server with bound acceptor, auth config, rate limiter - [x] `Server::new(opts: ServeOptions) -> Result<Server>` — creates server with bound acceptor, auth config, rate limiter
- [x] `Server::run()` — enters accept loop, for each connection: check rate limit → create handler → `run_stream()` - [x] `Server::run()` — enters accept loop, for each connection: check rate limit → create handler → `run_stream()`
@@ -56,7 +56,7 @@ Key design decisions:
## Summary ## Summary
Implemented server accept loop and configuration in `crates/wraith-core/src/server/serve.rs`: Implemented server accept loop and configuration in `crates/alknet-core/src/server/serve.rs`:
- `ServeOptions` struct with all CLI interface fields, builder pattern, KeySource support - `ServeOptions` struct with all CLI interface fields, builder pattern, KeySource support
- `Server::new()` creates server with russh config, auth config, rate limiter - `Server::new()` creates server with russh config, auth config, rate limiter
- `Server::run(acceptor, endpoint_info)` enters accept loop with rate limiting, stealth detection, russh::server::run_stream() - `Server::run(acceptor, endpoint_info)` enters accept loop with rate limiting, stealth detection, russh::server::run_stream()

View File

@@ -25,7 +25,7 @@ Stealth mode requires TLS transport. The CLI should reject or warn if `--stealth
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/server/stealth.rs` exports stealth mode protocol detection - [ ] `crates/alknet-core/src/server/stealth.rs` exports stealth mode protocol detection
- [ ] `detect_protocol(stream: TlsStream) -> ProtocolDetection` — peeks at first bytes to determine SSH vs HTTP - [ ] `detect_protocol(stream: TlsStream) -> ProtocolDetection` — peeks at first bytes to determine SSH vs HTTP
- [ ] `ProtocolDetection` enum: `Ssh`, `Http` (or `Unknown`) - [ ] `ProtocolDetection` enum: `Ssh`, `Http` (or `Unknown`)
- [ ] If SSH detected: pass stream to `russh::server::run_stream()` - [ ] If SSH detected: pass stream to `russh::server::run_stream()`

View File

@@ -1,6 +1,6 @@
--- ---
id: setup/project-init id: setup/project-init
name: Initialize Cargo workspace with wraith, wraith-core, and wraith-napi crates name: Initialize Cargo workspace with alknet, alknet-core, and alknet-napi crates
status: pending status: pending
depends_on: [] depends_on: []
scope: moderate scope: moderate
@@ -13,30 +13,30 @@ level: implementation
Set up the Rust workspace from scratch. The repo currently has only `docs/` and `.git/`. Initialize a Cargo workspace with three crate directories following the architecture spec: Set up the Rust workspace from scratch. The repo currently has only `docs/` and `.git/`. Initialize a Cargo workspace with three crate directories following the architecture spec:
- **`wraith-core`** — library crate with feature flags (`tls`, `iroh`, `acme`). All core logic lives here. - **`alknet-core`** — library crate with feature flags (`tls`, `iroh`, `acme`). All core logic lives here.
- **`wraith`** — binary crate depending on `wraith-core`. CLI entry point. - **`alknet`** — binary crate depending on `alknet-core`. CLI entry point.
- **`wraith-napi`** — napi-rs crate for the Node.js native addon (skeleton only at this stage). - **`alknet-napi`** — napi-rs crate for the Node.js native addon (skeleton only at this stage).
Per overview.md: `russh`, `tokio`, `clap`, `tracing`, `anyhow`/`thiserror` are core dependencies. `tokio-rustls`, `rustls`, `rustls-acme`, `iroh` are feature-gated. Per overview.md: `russh`, `tokio`, `clap`, `tracing`, `anyhow`/`thiserror` are core dependencies. `tokio-rustls`, `rustls`, `rustls-acme`, `iroh` are feature-gated.
## Acceptance Criteria ## Acceptance Criteria
- [ ] `Cargo.toml` workspace root with `[workspace]` members: `crates/wraith-core`, `crates/wraith`, `crates/wraith-napi` - [ ] `Cargo.toml` workspace root with `[workspace]` members: `crates/alknet-core`, `crates/alknet`, `crates/alknet-napi`
- [ ] `crates/wraith-core/Cargo.toml` with library crate, feature flags: `tls` (tokio-rustls + rustls), `iroh` (iroh), `acme` (rustls-acme, implies `tls`) - [ ] `crates/alknet-core/Cargo.toml` with library crate, feature flags: `tls` (tokio-rustls + rustls), `iroh` (iroh), `acme` (rustls-acme, implies `tls`)
- [ ] Core dependencies listed: `russh`, `tokio` (full), `tracing`, `anyhow`, `thiserror`, `tokio-util` - [ ] Core dependencies listed: `russh`, `tokio` (full), `tracing`, `anyhow`, `thiserror`, `tokio-util`
- [ ] `crates/wraith/Cargo.toml` with binary crate, depends on `wraith-core` with default features, `clap` with `derive` feature - [ ] `crates/alknet/Cargo.toml` with binary crate, depends on `alknet-core` with default features, `clap` with `derive` feature
- [ ] `crates/wraith-napi/Cargo.toml` with `cdylib` crate type, depends on `wraith-core`, `napi` and `napi-derive` - [ ] `crates/alknet-napi/Cargo.toml` with `cdylib` crate type, depends on `alknet-core`, `napi` and `napi-derive`
- [ ] `crates/wraith-core/src/lib.rs` with module skeleton: `pub mod transport; pub mod client; pub mod server; pub mod auth; pub mod socks5; pub mod error;` - [ ] `crates/alknet-core/src/lib.rs` with module skeleton: `pub mod transport; pub mod client; pub mod server; pub mod auth; pub mod socks5; pub mod error;`
- [ ] `crates/wraith/src/main.rs` with minimal `fn main()` skeleton - [ ] `crates/alknet/src/main.rs` with minimal `fn main()` skeleton
- [ ] `crates/wraith-napi/src/lib.rs` with `#[macro_use] extern crate napi_derive;` and empty skeleton - [ ] `crates/alknet-napi/src/lib.rs` with `#[macro_use] extern crate napi_derive;` and empty skeleton
- [ ] `.gitignore` covers `target/`, `node_modules/` - [ ] `.gitignore` covers `target/`, `node_modules/`
- [ ] `cargo check` succeeds for all workspace members - [ ] `cargo check` succeeds for all workspace members
- [ ] Feature flags resolve correctly: `cargo check -p wraith-core --features tls`, `--features iroh`, `--features acme` - [ ] Feature flags resolve correctly: `cargo check -p alknet-core --features tls`, `--features iroh`, `--features acme`
## References ## References
- docs/architecture/overview.md — package structure, dependencies, feature flags - docs/architecture/overview.md — package structure, dependencies, feature flags
- docs/architecture/napi-and-pubsub.md — wraith-napi crate purpose - docs/architecture/napi-and-pubsub.md — alknet-napi crate purpose
## Notes ## Notes

View File

@@ -18,8 +18,8 @@ The mock transport is critical — it lets us test SSH client/server flows witho
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/tests/` directory with empty integration test skeletons: `transport_tests.rs`, `client_tests.rs`, `server_tests.rs`, `auth_tests.rs` - [ ] `crates/alknet-core/tests/` directory with empty integration test skeletons: `transport_tests.rs`, `client_tests.rs`, `server_tests.rs`, `auth_tests.rs`
- [ ] `crates/wraith-core/src/testutil.rs` module (behind `#[cfg(test)]` or a `testutil` feature) exporting `MockTransport` and `MockStream` - [ ] `crates/alknet-core/src/testutil.rs` module (behind `#[cfg(test)]` or a `testutil` feature) exporting `MockTransport` and `MockStream`
- [ ] `MockStream` wraps `tokio::io::DuplexStream` implementing `AsyncRead + AsyncWrite + Unpin + Send` - [ ] `MockStream` wraps `tokio::io::DuplexStream` implementing `AsyncRead + AsyncWrite + Unpin + Send`
- [ ] `MockTransport` implements `Transport` trait (once defined) returning `MockStream` via `connect()` - [ ] `MockTransport` implements `Transport` trait (once defined) returning `MockStream` via `connect()`
- [ ] `MockTransportAcceptor` implements `TransportAcceptor` (once defined) returning paired `MockStream` via `accept()` - [ ] `MockTransportAcceptor` implements `TransportAcceptor` (once defined) returning paired `MockStream` via `accept()`

View File

@@ -23,7 +23,7 @@ This integrates with `TlsAcceptor` by providing ACME-resolved certificates inste
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/transport/acme.rs` (behind `#[cfg(feature = "acme")]`) - [ ] `crates/alknet-core/src/transport/acme.rs` (behind `#[cfg(feature = "acme")]`)
- [ ] Feature `acme` implies `tls` in Cargo.toml - [ ] Feature `acme` implies `tls` in Cargo.toml
- [ ] `AcmeCertProvider` struct accepts: domain (domain-based) or IP mode flag - [ ] `AcmeCertProvider` struct accepts: domain (domain-based) or IP mode flag
- [ ] Domain-based mode: uses `rustls-acme` with HTTP-01/TLS-ALPN-01 challenge responder - [ ] Domain-based mode: uses `rustls-acme` with HTTP-01/TLS-ALPN-01 challenge responder

View File

@@ -24,10 +24,10 @@ Feature-gated behind `iroh` feature flag.
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/transport/iroh.rs` (behind `#[cfg(feature = "iroh")]`) - [ ] `crates/alknet-core/src/transport/iroh.rs` (behind `#[cfg(feature = "iroh")]`)
- [ ] `IrohTransport` holds: target endpoint ID (base58-decoded to `NodeId`), relay URL, optional proxy URL - [ ] `IrohTransport` holds: target endpoint ID (base58-decoded to `NodeId`), relay URL, optional proxy URL
- [ ] `IrohTransport::connect()` calls `endpoint.connect(node_id, alpn)`, then `conn.open_bi()`, then `tokio::io::join(recv, send)` - [ ] `IrohTransport::connect()` calls `endpoint.connect(node_id, alpn)`, then `conn.open_bi()`, then `tokio::io::join(recv, send)`
- [ ] ALPN value is `b"wraith-ssh"` - [ ] ALPN value is `b"alknet-ssh"`
- [ ] `IrohTransport::describe()` returns e.g. `"iroh://<endpoint-id>"` - [ ] `IrohTransport::describe()` returns e.g. `"iroh://<endpoint-id>"`
- [ ] `IrohAcceptor` holds an `iroh::Endpoint` instance - [ ] `IrohAcceptor` holds an `iroh::Endpoint` instance
- [ ] `IrohAcceptor::bind()` creates endpoint with relay URL and optional proxy config - [ ] `IrohAcceptor::bind()` creates endpoint with relay URL and optional proxy config

View File

@@ -16,7 +16,7 @@ Implement the simplest transport: plain TCP. `TcpTransport` connects via `TcpStr
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/transport/tcp.rs` exports `TcpTransport` and `TcpAcceptor` - [ ] `crates/alknet-core/src/transport/tcp.rs` exports `TcpTransport` and `TcpAcceptor`
- [ ] `TcpTransport` holds a `SocketAddr` target address - [ ] `TcpTransport` holds a `SocketAddr` target address
- [ ] `TcpTransport::connect()` calls `TcpStream::connect(addr)` and returns the stream - [ ] `TcpTransport::connect()` calls `TcpStream::connect(addr)` and returns the stream
- [ ] `TcpTransport::describe()` returns e.g. `"tcp://1.2.3.4:22"` - [ ] `TcpTransport::describe()` returns e.g. `"tcp://1.2.3.4:22"`

View File

@@ -25,7 +25,7 @@ Feature-gated behind `tls` feature flag.
## Acceptance Criteria ## Acceptance Criteria
- [ ] `crates/wraith-core/src/transport/tls.rs` (behind `#[cfg(feature = "tls")]`) - [ ] `crates/alknet-core/src/transport/tls.rs` (behind `#[cfg(feature = "tls")]`)
- [ ] `TlsTransport` holds: target addr, optional `tls_server_name`, `insecure` flag, optional root cert for verification - [ ] `TlsTransport` holds: target addr, optional `tls_server_name`, `insecure` flag, optional root cert for verification
- [ ] `TlsTransport::connect()` does TCP connect then TLS client handshake via `tokio_rustls::TlsConnector` - [ ] `TlsTransport::connect()` does TCP connect then TLS client handshake via `tokio_rustls::TlsConnector`
- [ ] When `insecure`, accepts any certificate (dangerous, `webpki_roots::CertStore` bypass or custom verifier) - [ ] When `insecure`, accepts any certificate (dangerous, `webpki_roots::CertStore` bypass or custom verifier)

Some files were not shown because too many files have changed in this diff Show More