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:
```
You are an implementation specialist for the @alkdev/wraith project.
You are an implementation specialist for the @alkdev/alknet project.
Your task: {{task}}
@@ -204,7 +204,7 @@ Your task: {{task}}
7. Push: git push origin $(git branch --show-current)
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
- No comments in code
- anyhow::Result for application errors, thiserror for library error types

160
Cargo.lock generated
View File

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

View File

@@ -1,8 +1,8 @@
[workspace]
members = [
"crates/wraith-core",
"crates/wraith",
"crates/wraith-napi",
"crates/alknet-core",
"crates/alknet",
"crates/alknet-napi",
]
resolver = "2"
@@ -10,4 +10,4 @@ resolver = "2"
version = "0.1.0"
edition = "2021"
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.
@@ -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
- **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
- **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
@@ -24,7 +24,7 @@ cargo build --release
The default build includes TLS and iroh transports. To build a minimal binary with just TCP:
```bash
cargo build --release --no-default-features -p wraith
cargo build --release --no-default-features -p alknet
```
### Server
@@ -34,17 +34,17 @@ cargo build --release --no-default-features -p wraith
ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N ""
# 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
# 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 \
--acme-domain example.com \
--stealth
# iroh (no public IP needed)
wraith serve --key ssh_host_ed25519_key \
alknet serve --key ssh_host_ed25519_key \
--transport iroh
```
@@ -52,21 +52,21 @@ wraith serve --key ssh_host_ed25519_key \
```bash
# 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
# Connect via TLS
wraith connect --server example.com:443 \
alknet connect --server example.com:443 \
--transport tls \
--identity ~/.ssh/id_ed25519
# Connect via iroh (peer-to-peer, no public IP)
wraith connect --peer <endpoint-id> \
alknet connect --peer <endpoint-id> \
--transport iroh \
--identity ~/.ssh/id_ed25519
# With port forwarding
wraith connect --server example.com:22 \
alknet connect --server example.com:22 \
--identity ~/.ssh/id_ed25519 \
--forward 5432:db.internal:5432 \
--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
```
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
| Crate | Description |
|-------|-------------|
| `wraith-core` | Core library: transport trait, SOCKS5 server, port forwarding, auth, server handler |
| `wraith` | CLI binary (`wraith connect` / `wraith serve`) |
| `wraith-napi` | Node.js native addon via napi-rs (`connect()` / `serve()`) |
| `alknet-core` | Core library: transport trait, SOCKS5 server, port forwarding, auth, server handler |
| `alknet` | CLI binary (`alknet connect` / `alknet serve`) |
| `alknet-napi` | Node.js native addon via napi-rs (`connect()` / `serve()`) |
## Feature flags
| Feature | Crate | Default | Description |
|---------|-------|---------|-------------|
| `tls` | `wraith-core`, `wraith` | yes | TLS transport (tokio-rustls) |
| `iroh` | `wraith-core`, `wraith` | yes | iroh QUIC P2P transport |
| `acme` | `wraith-core` | no | ACME/Let's Encrypt auto-cert provisioning |
| `testutil` | `wraith-core` | no | Test utilities (for internal use) |
| `tls` | `alknet-core`, `alknet` | yes | TLS transport (tokio-rustls) |
| `iroh` | `alknet-core`, `alknet` | yes | iroh QUIC P2P transport |
| `acme` | `alknet-core` | no | ACME/Let's Encrypt auto-cert provisioning |
| `testutil` | `alknet-core` | no | Test utilities (for internal use) |
## Transport modes
@@ -117,7 +117,7 @@ Key formats are OpenSSH throughout (private keys: `-----BEGIN OPENSSH PRIVATE KE
## 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
@@ -137,10 +137,10 @@ See [docs/architecture/](docs/architecture/) for full specifications and [ADR in
## 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
const { connect, serve } = require('wraith-napi');
const { connect, serve } = require('alknet-napi');
// Client: open a duplex stream through SSH
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
- **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
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

View File

@@ -1,13 +1,13 @@
[package]
name = "wraith-core"
name = "alknet-core"
version.workspace = true
edition.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
[lib]
name = "wraith_core"
name = "alknet_core"
[features]
default = []
@@ -36,9 +36,9 @@ async-trait = "0.1"
ipnetwork = "0.21.1"
[dev-dependencies]
wraith-core = { path = ".", features = ["testutil", "tls", "iroh"] }
alknet-core = { path = ".", features = ["testutil", "tls", "iroh"] }
tempfile = "3"
rcgen = "0.14"
rand_core = "0.6"
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.
/// Call `validate()` before passing to `ClientSession::new()`.
///
/// ```
/// use wraith_core::client::{ConnectOptions, TransportMode};
/// use wraith_core::auth::keys::KeySource;
/// use alknet_core::client::{ConnectOptions, TransportMode};
/// use alknet_core::auth::keys::KeySource;
///
/// let opts = ConnectOptions::new(KeySource::File("/path/to/key".into()))
/// .server("example.com:22")
@@ -312,7 +312,7 @@ impl<T: Transport> ClientSession<T> {
.await;
});
info!("wraith client running: SOCKS5 on {}", socks5_listen);
info!("alknet client running: SOCKS5 on {}", socks5_listen);
#[cfg(unix)]
let signal_done = {
@@ -439,7 +439,7 @@ impl<T: Transport> ClientSession<T> {
fn derive_username() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "wraith".to_string())
.unwrap_or_else(|_| "alknet".to_string())
}
async fn establish_session<T: Transport>(
@@ -567,7 +567,7 @@ mod tests {
.remote_forward("0.0.0.0:8080:127.0.0.1:3000")
.proxy("socks5://127.0.0.1:1080")
.iroh_relay("https://relay.example.com")
.tls_server_name("wraith.test")
.tls_server_name("alknet.test")
.insecure(true);
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.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.tls_server_name.as_deref(), Some("wraith.test"));
assert_eq!(opts.tls_server_name.as_deref(), Some("alknet.test"));
assert!(opts.insecure);
}

View File

@@ -1,4 +1,4 @@
//! Error types for wraith-core.
//! Error types for alknet-core.
//!
//! Layered error hierarchy:
//! - `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,
//! 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.
//!
//! > **Alpha software.** This crate depends on solid libraries (russh, tokio, rustls, iroh)
@@ -33,10 +33,10 @@
//!
//! ```no_run
//! use std::sync::Arc;
//! use wraith_core::transport::TcpTransport;
//! use wraith_core::client::{ClientSession, ConnectOptions, TransportMode};
//! use wraith_core::auth::keys::KeySource;
//! use wraith_core::Transport;
//! use alknet_core::transport::TcpTransport;
//! use alknet_core::client::{ClientSession, ConnectOptions, TransportMode};
//! use alknet_core::auth::keys::KeySource;
//! use alknet_core::Transport;
//!
//! #[tokio::main]
//! 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
//! TCP target. See ADR-018 for the design rationale.
@@ -9,11 +9,11 @@ use std::io;
use async_trait::async_trait;
use tokio::io::{AsyncRead, AsyncWrite};
pub const WRAITH_CONTROL_DESTINATION: &str = "wraith-control";
pub const WRAITH_PREFIX: &str = "wraith-";
pub const ALKNET_CONTROL_DESTINATION: &str = "alknet-control";
pub const ALKNET_PREFIX: &str = "alknet-";
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 {}
@@ -68,21 +68,21 @@ mod tests {
use tokio::io::duplex;
#[test]
fn wraith_control_destination_constant() {
assert_eq!(WRAITH_CONTROL_DESTINATION, "wraith-control");
fn alknet_control_destination_constant() {
assert_eq!(ALKNET_CONTROL_DESTINATION, "alknet-control");
}
#[test]
fn wraith_prefix_constant() {
assert_eq!(WRAITH_PREFIX, "wraith-");
fn alknet_prefix_constant() {
assert_eq!(ALKNET_PREFIX, "alknet-");
}
#[test]
fn reserved_destination_detected() {
assert!(is_reserved_destination("wraith-control"));
assert!(is_reserved_destination("wraith-status"));
assert!(is_reserved_destination("wraith-events"));
assert!(is_reserved_destination("wraith-"));
assert!(is_reserved_destination("alknet-control"));
assert!(is_reserved_destination("alknet-status"));
assert!(is_reserved_destination("alknet-events"));
assert!(is_reserved_destination("alknet-"));
}
#[test]
@@ -90,17 +90,17 @@ mod tests {
assert!(!is_reserved_destination("example.com"));
assert!(!is_reserved_destination("localhost"));
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("wrait-control"));
assert!(!is_reserved_destination("WRAITH-control"));
assert!(!is_reserved_destination("alkne-control"));
assert!(!is_reserved_destination("ALKNET-control"));
}
#[test]
fn prefix_matching_case_sensitive() {
assert!(!is_reserved_destination("Wraith-control"));
assert!(!is_reserved_destination("WRAITH-control"));
assert!(is_reserved_destination("wraith-Control"));
assert!(!is_reserved_destination("Alknet-control"));
assert!(!is_reserved_destination("ALKNET-control"));
assert!(is_reserved_destination("alknet-Control"));
}
#[test]
@@ -187,6 +187,6 @@ mod tests {
#[test]
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::server::control_channel::{
ControlChannelHandler, ControlChannelRouter, WRAITH_PREFIX,
ControlChannelHandler, ControlChannelRouter, ALKNET_PREFIX,
};
use crate::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
@@ -210,7 +210,7 @@ impl Handler for ServerHandler {
originator_port: u32,
_session: &mut Session,
) -> 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() {
return Ok(false);
}
@@ -576,18 +576,18 @@ mod tests {
}
#[test]
fn reserved_wraith_destination_routing() {
fn reserved_alknet_destination_routing() {
use crate::server::control_channel::is_reserved_destination;
assert!(is_reserved_destination("wraith-control"));
assert!(is_reserved_destination("wraith-status"));
assert!(is_reserved_destination("wraith-events"));
assert!(is_reserved_destination("alknet-control"));
assert!(is_reserved_destination("alknet-status"));
assert!(is_reserved_destination("alknet-events"));
assert!(!is_reserved_destination("example.com"));
assert!(!is_reserved_destination("localhost"));
assert!(!is_reserved_destination("wraith.example.com"));
assert!(!is_reserved_destination("alknet.example.com"));
}
#[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 handler = make_handler(auth_config, None, None);
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),
//! 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 control_channel;
@@ -16,8 +16,8 @@ pub mod stealth;
pub use channel_proxy::{connect_outbound, proxy_channel};
pub use control_channel::{
ControlChannelHandler, ControlChannelRouter, DuplexStream, WRAITH_CONTROL_DESTINATION,
WRAITH_PREFIX, is_reserved_destination,
ControlChannelHandler, ControlChannelRouter, DuplexStream, ALKNET_CONTROL_DESTINATION,
ALKNET_PREFIX, is_reserved_destination,
};
pub use handler::{ProxyConfig, ProxyMode, ServerHandler, TransportKind};
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.
/// Call `validate()` before passing to `Server::new()`.
///
/// ```
/// use wraith_core::server::{ServeOptions, ServeTransportMode};
/// use wraith_core::auth::keys::KeySource;
/// use alknet_core::server::{ServeOptions, ServeTransportMode};
/// use alknet_core::auth::keys::KeySource;
///
/// let opts = ServeOptions::new(KeySource::File("/path/to/host_key".into()))
/// .transport_mode(ServeTransportMode::Tcp)
@@ -221,7 +221,7 @@ struct ActiveSession {
join: tokio::task::JoinHandle<()>,
}
/// The wraith SSH server.
/// The alknet SSH server.
///
/// Accepts connections over any `TransportAcceptor`, authenticates via Ed25519 keys
/// or certificate authority, and proxies `direct-tcpip` channels to their targets.
@@ -331,13 +331,13 @@ impl Server {
if self.transport_mode == ServeTransportMode::Iroh {
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 {
info!("wraith server running: transport=iroh");
info!("alknet server running: transport=iroh");
}
} else {
info!(
"wraith server running: transport={} listen={}",
"alknet server running: transport={} listen={}",
self.transport_mode, self.listen_addr
);
}

View File

@@ -9,7 +9,7 @@ use tokio::io;
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/";
/// A client-side iroh QUIC P2P transport that connects to a remote iroh endpoint.
@@ -31,8 +31,8 @@ pub struct IrohTransport {
impl IrohTransport {
/// Create a new iroh transport with its own dedicated endpoint.
///
/// The endpoint is created with the `wraith-ssh` ALPN and the provided
/// relay URL. Use this when wraith is the only iroh service on this node.
/// The endpoint is created with the `alknet-ssh` ALPN and the provided
/// relay URL. Use this when alknet is the only iroh service on this node.
pub async fn new(
node_id: NodeId,
relay_url: Option<RelayUrl>,
@@ -54,9 +54,9 @@ impl IrohTransport {
/// 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
/// 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
/// per peer, multiplexed by ALPN.
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`
/// 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
/// 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()`.
pub struct IrohAcceptor {
endpoint: Endpoint,
@@ -112,9 +112,9 @@ pub struct 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(
relay_url: Option<RelayUrl>,
proxy_url: Option<url::Url>,
@@ -135,14 +135,14 @@ impl IrohAcceptor {
/// 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
/// 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
/// does not manage — the caller is responsible for bridging the
/// 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
/// internally.
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`)
//! 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() {
let addr: SocketAddr = "1.2.3.4:443".parse().unwrap();
let transport = TlsTransport::new(addr)
.with_server_name("wraith.test")
.with_server_name("alknet.test")
.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);
}
@@ -395,7 +395,7 @@ mod tests {
let mut client = transport.connect().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();
let mut buf = vec![0u8; msg.len()];
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]
async fn mock_transport_connect() {

View File

@@ -1,16 +1,16 @@
[package]
name = "wraith-napi"
name = "alknet-napi"
version.workspace = true
edition.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
[lib]
crate-type = ["cdylib"]
[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-derive = "3"
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.
//! 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::sync::Mutex;
use wraith_core::auth::client_auth::{ClientAuthConfig, ClientHandler};
use wraith_core::auth::keys::KeySource;
use wraith_core::transport::{IrohTransport, TcpTransport, TlsTransport, Transport};
use alknet_core::auth::client_auth::{ClientAuthConfig, ClientHandler};
use alknet_core::auth::keys::KeySource;
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;
#[napi(object)]
pub struct WraithConnectOptions {
pub struct AlknetConnectOptions {
pub server: Option<String>,
pub peer: Option<String>,
pub transport: String,
@@ -53,13 +53,13 @@ fn parse_addr(addr_str: &str) -> Result<SocketAddr> {
}
#[napi]
pub struct WraithStream {
pub struct AlknetStream {
read: Arc<Mutex<tokio::io::ReadHalf<russh::ChannelStream<client::Msg>>>>,
write: Arc<Mutex<tokio::io::WriteHalf<russh::ChannelStream<client::Msg>>>>,
}
#[napi]
impl WraithStream {
impl AlknetStream {
#[napi]
pub async fn read(&self, size: u32) -> Result<Buffer> {
let mut buf = vec![0u8; size as usize];
@@ -96,7 +96,7 @@ impl WraithStream {
}
#[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 auth_config = Arc::new(
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 handler = ClientHandler::from_config(&auth_config);
let username = "wraith".to_string();
let username = "alknet".to_string();
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 (read_half, write_half) = tokio::io::split(stream);
Ok(WraithStream {
Ok(AlknetStream {
read: Arc::new(Mutex::new(read_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.
//!
//! > **Alpha software.** The NAPI interface may change between versions.
@@ -8,7 +8,7 @@
//! # Quick example (Node.js)
//!
//! ```js
//! const { connect, serve } = require('wraith-napi');
//! const { connect, serve } = require('alknet-napi');
//!
//! // Client: open a duplex SSH stream
//! 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
//! `ThreadsafeFunction` callback. Supports TCP, TLS, and iroh transports.
@@ -14,14 +14,14 @@ use russh::Channel;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::Mutex;
use wraith_core::auth::keys::KeySource;
use wraith_core::auth::server_auth::ServerAuthConfig;
use wraith_core::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
use wraith_core::server::serve::{ServeOptions, ServeTransportMode, Server};
use wraith_core::transport::{TcpAcceptor, TransportAcceptor};
use alknet_core::auth::keys::KeySource;
use alknet_core::auth::server_auth::ServerAuthConfig;
use alknet_core::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
use alknet_core::server::serve::{ServeOptions, ServeTransportMode, Server};
use alknet_core::transport::{TcpAcceptor, TransportAcceptor};
#[napi(object)]
pub struct WraithServeOptions {
pub struct AlknetServeOptions {
pub transport: String,
pub host_key: Option<Either<String, Buffer>>,
pub authorized_keys: Option<Either<String, Buffer>>,
@@ -75,13 +75,13 @@ pub struct ConnectionInfo {
}
#[napi]
pub struct WraithServerStream {
pub struct AlknetServerStream {
read: Arc<Mutex<tokio::io::ReadHalf<russh::ChannelStream<server::Msg>>>>,
write: Arc<Mutex<tokio::io::WriteHalf<russh::ChannelStream<server::Msg>>>>,
}
#[napi]
impl WraithServerStream {
impl AlknetServerStream {
#[napi]
pub async fn read(&self, size: u32) -> napi::Result<Buffer> {
let mut buf = vec![0u8; size as usize];
@@ -208,7 +208,7 @@ impl russh::server::Handler for NapiServerHandler {
_originator_port: u32,
_session: &mut russh::server::Session,
) -> 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;
if let Some(ref tx) = *guard {
let _ = tx.send(channel);
@@ -385,7 +385,7 @@ impl russh::server::Handler for NapiServerHandler {
type ServerTsfn = ThreadsafeFunction<ConnectionEventWrapper, (), ConnectionEventWrapper>;
#[napi]
pub struct WraithServer {
pub struct AlknetServer {
shutdown_tx: tokio::sync::watch::Sender<bool>,
listen_addr: String,
endpoint_id: Option<String>,
@@ -393,7 +393,7 @@ pub struct WraithServer {
}
struct ConnectionEventWrapper {
stream: WraithServerStream,
stream: AlknetServerStream,
info: ConnectionInfo,
}
@@ -408,7 +408,7 @@ impl ToNapiValue for ConnectionEventWrapper {
"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();
napi::check_status!(
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 {}
#[napi]
impl WraithServer {
impl AlknetServer {
#[napi]
pub async fn close(&self) -> napi::Result<()> {
let _ = self.shutdown_tx.send(true);
@@ -470,7 +470,7 @@ impl WraithServer {
}
#[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 authorized_keys_source = resolve_optional_key_source(&options.authorized_keys);
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 =
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))
})?;
@@ -573,7 +573,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
.await;
});
Ok(WraithServer {
Ok(AlknetServer {
shutdown_tx,
listen_addr: actual_listen,
endpoint_id: None,
@@ -581,7 +581,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
})
}
ServeTransportMode::Tls => {
use wraith_core::transport::TlsAcceptor;
use alknet_core::transport::TlsAcceptor;
let addr = parse_addr(listen_addr_str)?;
@@ -654,7 +654,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
);
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))
})?;
@@ -684,7 +684,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
.await;
});
Ok(WraithServer {
Ok(AlknetServer {
shutdown_tx,
listen_addr: actual_listen,
endpoint_id: None,
@@ -692,7 +692,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
})
}
ServeTransportMode::Iroh => {
use wraith_core::transport::IrohAcceptor;
use alknet_core::transport::IrohAcceptor;
let relay_url: Option<iroh::RelayUrl> = match options.iroh_relay.as_deref() {
Some(u) => Some(u.parse().map_err(|e| {
@@ -736,7 +736,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
);
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))
})?;
@@ -766,7 +766,7 @@ pub async fn serve(options: WraithServeOptions) -> napi::Result<WraithServer> {
.await;
});
Ok(WraithServer {
Ok(AlknetServer {
shutdown_tx,
listen_addr: String::new(),
endpoint_id: Some(iroh_endpoint_id),
@@ -836,7 +836,7 @@ async fn run_accept_loop<A>(
Some(ch) => {
let channel_stream = ch.into_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)),
write: Arc::new(Mutex::new(write_half)),
};

View File

@@ -1,23 +1,23 @@
[package]
name = "wraith"
name = "alknet"
version.workspace = true
edition.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
[[bin]]
name = "wraith"
name = "alknet"
path = "src/main.rs"
[features]
default = ["tls", "iroh"]
tls = ["wraith-core/tls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
iroh = ["wraith-core/iroh", "dep:iroh", "dep:url"]
acme = ["wraith-core/acme", "dep:rustls-acme", "dep:rustls", "tls"]
tls = ["alknet-core/tls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
iroh = ["alknet-core/iroh", "dep:iroh", "dep:url"]
acme = ["alknet-core/acme", "dep:rustls-acme", "dep:rustls", "tls"]
[dependencies]
wraith-core = { path = "../wraith-core" }
alknet-core = { path = "../alknet-core" }
clap = { version = "4", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] }
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
//! tool. Provides `wraith connect` (client) and `wraith serve` (server) subcommands with
//! CLI binary for [Alknet](https://git.alk.dev/alkdev/alknet), a self-hostable SSH-based tunnel
//! tool. Provides `alknet connect` (client) and `alknet serve` (server) subcommands with
//! 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::process;
@@ -12,18 +12,18 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand, ValueEnum};
use wraith_core::auth::keys::KeySource;
use wraith_core::client::{ConnectOptions, TransportMode};
use wraith_core::server::{ServeOptions, ServeTransportMode, Server};
use alknet_core::auth::keys::KeySource;
use alknet_core::client::{ConnectOptions, TransportMode};
use alknet_core::server::{ServeOptions, ServeTransportMode, Server};
#[cfg(feature = "iroh")]
use wraith_core::transport::IrohTransport;
use wraith_core::transport::TcpTransport;
use alknet_core::transport::IrohTransport;
use alknet_core::transport::TcpTransport;
#[cfg(feature = "tls")]
use wraith_core::transport::TlsTransport;
use wraith_core::transport::Transport;
use alknet_core::transport::TlsTransport;
use alknet_core::transport::Transport;
#[derive(Parser)]
#[command(name = "wraith", version, about = "Wraith SSH tunnel tool")]
#[command(name = "alknet", version, about = "Alknet SSH tunnel tool")]
struct Cli {
#[command(subcommand)]
command: Commands,
@@ -32,13 +32,13 @@ struct Cli {
#[derive(Subcommand)]
enum Commands {
#[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 {
#[arg(
long,
help = "TCP/TLS server address (required for tcp/tls transport)",
env = "WRAITH_SERVER"
env = "ALKNET_SERVER"
)]
server: Option<String>,
@@ -51,7 +51,7 @@ enum Commands {
#[arg(long, value_enum, default_value = "tcp", help = "Transport mode")]
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>,
#[arg(long, default_value = "127.0.0.1:1080", help = "SOCKS5 listen address")]
@@ -76,7 +76,7 @@ enum Commands {
insecure: bool,
},
#[command(about = "Start the wraith server (accept SSH connections)")]
#[command( about = "Start the alknet server (accept SSH connections)")]
Serve {
#[arg(long, help = "SSH host key path (required)")]
key: String,
@@ -263,7 +263,7 @@ async fn run_connect(
insecure: bool,
) -> Result<()> {
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 transport_mode: TransportMode = transport.into();
@@ -317,7 +317,7 @@ async fn run_connect(
#[cfg(not(feature = "tls"))]
{
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")]
@@ -340,7 +340,7 @@ async fn run_connect(
#[cfg(not(feature = "iroh"))]
{
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")]
@@ -375,7 +375,7 @@ async fn run_connect(
}
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
.map_err(|e| anyhow!("{e}"))?
.run()
@@ -405,7 +405,7 @@ async fn run_serve(
#[cfg(not(feature = "acme"))]
{
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
.parse()
.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
.map_err(|e| anyhow!("bind failed: {e}"))?;
server.run(acceptor, None).await.map_err(|e| anyhow!("{e}"))
@@ -463,7 +463,7 @@ async fn run_serve(
#[cfg(not(feature = "tls"))]
{
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")]
@@ -473,11 +473,11 @@ async fn run_serve(
.parse()
.map_err(|e| anyhow!("invalid listen address: {e}"))?;
let provider = Arc::new(
wraith_core::transport::AcmeCertProvider::domain(domain)
alknet_core::transport::AcmeCertProvider::domain(domain)
.with_production_directory(),
);
let acceptor =
wraith_core::transport::AcmeTlsAcceptor::bind_acme(addr, provider)
alknet_core::transport::AcmeTlsAcceptor::bind_acme(addr, provider)
.await
.map_err(|e| anyhow!("ACME bind failed: {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[..])
.map_err(|e| anyhow!("failed to parse TLS private key: {e}"))?
.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
.map_err(|e| anyhow!("TLS bind failed: {e}"))?;
server.run(acceptor, None).await.map_err(|e| anyhow!("{e}"))
@@ -516,7 +516,7 @@ async fn run_serve(
#[cfg(not(feature = "iroh"))]
{
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")]
@@ -533,7 +533,7 @@ async fn run_serve(
Some(u) => Some(u.parse().map_err(|e| anyhow!("invalid proxy URL: {e}"))?),
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
.map_err(|e| anyhow!("iroh bind failed: {e}"))?;
let endpoint_id = acceptor.endpoint_id();

View File

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

View File

@@ -15,7 +15,7 @@ shared across both auth paths. Identity resolution produces a transport-agnostic
## Why
Wraith currently authenticates connections exclusively through SSH public key
Alknet currently authenticates connections exclusively through SSH public key
auth. Non-SSH transports (WebTransport) cannot perform SSH key exchange — they
need a different auth presentation that shares the same key material. The
unified auth layer ensures one key set, one identity, one rotation mechanism
@@ -48,7 +48,7 @@ AuthToken = base64url(key_id || timestamp || signature)
Wire format when passed in a WebTransport CONNECT request:
```
CONNECT https://server:443/wraith?token=<AuthToken>
CONNECT https://server:443/alknet?token=<AuthToken>
```
Server verification:
@@ -74,7 +74,7 @@ ADR-023.
### IdentityProvider Trait
The `IdentityProvider` trait decouples wraith-core from any specific identity
The `IdentityProvider` trait decouples alknet-core from any specific identity
storage. It resolves a key fingerprint or auth token to an `Identity` with
scopes and resources.
@@ -103,7 +103,7 @@ default scope set. No database required.
`accounts` tables plus the ACL graph. Resolves fingerprint → account →
organization membership → effective scopes. Uses `ArcSwap` for hot reload.
The trait is the contract. The backing store is pluggable. Wraith-core never
The trait is the contract. The backing store is pluggable. Alknet-core never
depends on Honker, SQLite, or any specific database.
### AuthPolicy Structure
@@ -167,7 +167,7 @@ authorization decisions.
The wtransport library's `SessionRequest` provides:
- `path()` — URL path (e.g., `/wraith?token=...`)
- `path()` — URL path (e.g., `/alknet?token=...`)
- `headers()` — HTTP headers (for `Authorization: Bearer ...`)
- `origin()` — Browser origin (for CORS-like restrictions)
- `remote_address()` — Client UDP address
@@ -204,7 +204,7 @@ dependencies needed.
- Auth tokens are Ed25519-signed with the same key pair used for SSH auth. No
separate key management for non-SSH transports.
- `IdentityProvider` is the only interface between wraith-core and identity
- `IdentityProvider` is the only interface between alknet-core and identity
storage. No database dependency at the core level.
- The SSH auth path is unchanged. `auth_publickey()` continues to work exactly
as it does today. Token auth is additive.

View File

@@ -327,7 +327,7 @@ defined in `@alkdev/operations`. The TypeScript implementation provides:
The Rust implementation mirrors these types and behaviors. TypeScript consumers
continue using `@alkdev/operations` over `@alkdev/pubsub` adapters (including
the `event-target-wraith` adapter). Rust consumers use core's registry directly.
the `event-target-alknet` adapter). Rust consumers use core's registry directly.
Both speak the same wire protocol and can interoperate.
The key principle: **the same `EventEnvelope` can flow from a Rust handler

View File

@@ -7,11 +7,11 @@ last_updated: 2026-06-02
## What
The wraith client establishes an SSH session to a server (via pluggable transport) and exposes a local SOCKS5 proxy for routing traffic through that session. Port forwarding (`-L` / `-R` style) covers specific service access like Postgres or Redis.
The alknet client establishes an SSH session to a server (via pluggable transport) and exposes a local SOCKS5 proxy for routing traffic through that session. Port forwarding (`-L` / `-R` style) covers specific service access like Postgres or Redis.
## Why
Users need a way to route traffic through the SSH tunnel. SOCKS5 is the primary interface — it's standard, well-supported by browsers and CLI tools, and needs no privileges. Port forwarding covers specific service access. For VPN-like "route all traffic" behavior, users run `tun2proxy` alongside wraith (ADR-014).
Users need a way to route traffic through the SSH tunnel. SOCKS5 is the primary interface — it's standard, well-supported by browsers and CLI tools, and needs no privileges. Port forwarding covers specific service access. For VPN-like "route all traffic" behavior, users run `tun2proxy` alongside alknet (ADR-014).
## Architecture
@@ -19,7 +19,7 @@ Users need a way to route traffic through the SSH tunnel. SOCKS5 is the primary
```
┌────────────────────────────────────────────────────────┐
wraith connect │
alknet connect │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SOCKS5 │ │ Port │ │ Remote │ │
@@ -101,8 +101,8 @@ The channel manager orchestrates reconnection: it creates a new transport stream
The client uses programmatic configuration — no `~/.ssh/config` parsing, no custom config files. Configuration comes from:
1. **CLI flags**: `--server`, `--identity`, `--transport`, etc.
2. **Library API**: `ConnectOptions` and `ServeOptions` structs in `wraith-core`, constructable programmatically
3. **Environment variables**: `WRAITH_SERVER`, `WRAITH_IDENTITY` as convenience defaults
2. **Library API**: `ConnectOptions` and `ServeOptions` structs in `alknet-core`, constructable programmatically
3. **Environment variables**: `ALKNET_SERVER`, `ALKNET_IDENTITY` as convenience defaults
This approach avoids cross-platform path issues (`~` expansion, Windows `USERPROFILE`) and makes the library API clean for programmatic consumers like the NAPI wrapper. Keys can be provided as file paths or in-memory data.
@@ -110,7 +110,7 @@ This approach avoids cross-platform path issues (`~` expansion, Windows `USERPRO
Key inputs (`--identity`, `--authorized-keys`, `--cert-authority`, `--key`) accept either:
- **File path**: A filesystem path to a key file (e.g., `~/.ssh/id_ed25519`, `/etc/wraith/ca.pub`)
- **File path**: A filesystem path to a key file (e.g., `~/.ssh/id_ed25519`, `/etc/alknet/ca.pub`)
- **In-memory data**: Raw key bytes provided programmatically via the library API or NAPI wrapper (as `Vec<u8>` in Rust, `Buffer` in Node.js)
The accepted format is **OpenSSH key format** (the format used by `ssh-keygen` and OpenSSH's `~/.ssh/` files). This includes:
@@ -125,31 +125,31 @@ PEM-encoded keys (PKCS#1, PKCS#8) are not supported. Use OpenSSH format keys thr
```bash
# Basic connection (TCP, default port 22)
wraith connect --server example.com --identity ~/.ssh/id_ed25519
alknet connect --server example.com --identity ~/.ssh/id_ed25519
# With TLS
wraith connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519
alknet connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519
# With TLS + insecure (self-signed certs)
wraith connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519 --insecure
alknet connect --server example.com:443 --transport tls --identity ~/.ssh/id_ed25519 --insecure
# With iroh (no public IP needed)
wraith connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519
alknet connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519
# With iroh + custom relay
wraith connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519 --iroh-relay https://relay.example.com
alknet connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519 --iroh-relay https://relay.example.com
# With iroh + proxy (transport chaining)
wraith connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519 --proxy socks5://127.0.0.1:1080
alknet connect --peer <endpoint-id> --transport iroh --identity ~/.ssh/id_ed25519 --proxy socks5://127.0.0.1:1080
# SOCKS5 on custom port
wraith connect --server example.com --socks5 127.0.0.1:1080 --identity ~/.ssh/id_ed25519
alknet connect --server example.com --socks5 127.0.0.1:1080 --identity ~/.ssh/id_ed25519
# With port forwards
wraith connect --server example.com --forward 5432:db.internal:5432 --forward 6379:redis.internal:6379
alknet connect --server example.com --forward 5432:db.internal:5432 --forward 6379:redis.internal:6379
# All options
wraith connect \
alknet connect \
--server <addr> \ # TCP/TLS server address (required for tcp/tls)
--peer <endpoint-id> \ # iroh endpoint ID, base58-encoded (required for iroh)
--transport tcp|tls|iroh \ # Transport mode
@@ -165,13 +165,13 @@ wraith connect \
## Constraints
- SOCKS5 is always enabled when `wraith connect` runs (it's the primary interface). Port forwards are optional.
- SOCKS5 is always enabled when `alknet connect` runs (it's the primary interface). Port forwards are optional.
- The client does not log tunnel destinations. The SOCKS5 server connects and proxies — no logging of SOCKS5 request targets.
- Authentication is Ed25519 public key or OpenSSH certificate (ADR-012). No password authentication over SSH.
- Only one SSH session per `wraith connect` process. Multiple sessions = multiple processes (or a future multiplexer).
- Only one SSH session per `alknet connect` process. Multiple sessions = multiple processes (or a future multiplexer).
- No `~/.ssh/config` parsing. Configuration is programmatic via CLI flags, env vars, or library API structs (ADR-011).
- VPN-like "route all traffic" behavior is provided by running `tun2proxy --proxy socks5://127.0.0.1:1080` alongside the client, not by a built-in TUN interface (ADR-014).
- The CLI `wraith connect` command manages a full SSH session with SOCKS5 and port forwarding. The NAPI `connect()` function is a different operation — it opens a single SSH channel as a Duplex stream for programmatic use, with no SOCKS5 server or port forwarding. See napi-and-pubsub.md for details.
- The CLI `alknet connect` command manages a full SSH session with SOCKS5 and port forwarding. The NAPI `connect()` function is a different operation — it opens a single SSH channel as a Duplex stream for programmatic use, with no SOCKS5 server or port forwarding. See napi-and-pubsub.md for details.
## Graceful Shutdown

View File

@@ -4,9 +4,9 @@
Accepted
## Context
Wraith needs to support multiple transport modes (TCP, TLS, iroh) for SSH sessions. Each mode has different connection establishment logic but produces the same result: a bidirectional byte stream. Without an abstraction, each transport would need its own SSH connection code path.
Alknet needs to support multiple transport modes (TCP, TLS, iroh) for SSH sessions. Each mode has different connection establishment logic but produces the same result: a bidirectional byte stream. Without an abstraction, each transport would need its own SSH connection code path.
russh's `client::connect_stream()` and `server::run_stream()` both accept `AsyncRead + AsyncWrite + Unpin + Send`, meaning SSH is already transport-agnostic at the API level. The design question is whether to enshrine this in wraith's own type system or handle each transport case-by-case.
russh's `client::connect_stream()` and `server::run_stream()` both accept `AsyncRead + AsyncWrite + Unpin + Send`, meaning SSH is already transport-agnostic at the API level. The design question is whether to enshrine this in alknet's own type system or handle each transport case-by-case.
## Decision
Define a `Transport` trait that produces `AsyncRead + AsyncWrite + Unpin + Send` streams. Each transport (TCP, TLS, iroh) implements this trait. The SSH layer calls `transport.connect()` and passes the result to `russh::client::connect_stream()`.

View File

@@ -4,18 +4,18 @@
Superseded by ADR-014
## Context
TUN interface creation requires root privileges or `CAP_NET_ADMIN` on Linux, Administrator on Windows, or platform-specific VPN APIs on macOS/iOS/Android. If the core wraith binary required these privileges, the attack surface of root-required code would include the entire SSH implementation, key handling, and transport negotiation.
TUN interface creation requires root privileges or `CAP_NET_ADMIN` on Linux, Administrator on Windows, or platform-specific VPN APIs on macOS/iOS/Android. If the core alknet binary required these privileges, the attack surface of root-required code would include the entire SSH implementation, key handling, and transport negotiation.
The primary use cases (SOCKS5 proxy, port forwarding) need no privileges at all. Only the "route all traffic through TUN" use case needs root.
## Decision
The TUN functionality is a separate `wraith-tun` binary that:
The TUN functionality is a separate `alknet-tun` binary that:
1. Creates a TUN device (requires root / CAP_NET_ADMIN)
2. Reads IP packets from it
3. Forwards each connection to the core wraith's SOCKS5 port (127.0.0.1:1080)
3. Forwards each connection to the core alknet's SOCKS5 port (127.0.0.1:1080)
4. Proxies bytes between TUN packets and SOCKS5 connections
The core `wraith connect` binary never needs root. The `wraith-tun` binary is ~200-500 lines and does nothing except TUN ↔ SOCKS5 forwarding.
The core `alknet connect` binary never needs root. The `alknet-tun` binary is ~200-500 lines and does nothing except TUN ↔ SOCKS5 forwarding.
## Consequences
- **Positive**: Root-required code surface is tiny and auditable.

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**: Testing is straightforward — mock transports produce in-memory streams.
- **Positive**: Security audit is clean — the SSH implementation has no network-facing code.
- **Positive**: The transport can be layered. Iroh connecting through a SOCKS5 proxy (which itself tunnels through wraith) is just a transport that calls out to a SOCKS5 library before establishing the QUIC connection.
- **Positive**: The transport can be layered. Iroh connecting through a SOCKS5 proxy (which itself tunnels through alknet) is just a transport that calls out to a SOCKS5 library before establishing the QUIC connection.
- **Negative**: SSH keepalive and reconnection must be handled at the transport level. If the transport stream dies, the SSH session dies. Reconnection means establishing a new transport + new SSH session. There's no "SSH reconnects over the same transport" — you get a new session.
- **Negative**: Multiple SSH sessions over the same iroh connection require the iroh `Endpoint` (not stream) to be shared between sessions. The transport trait produces one stream per `connect()` call. The iroh `Endpoint` must be created externally and shared. (The `IrohTransport` struct holds an `Arc<Endpoint>`.)

View File

@@ -24,14 +24,14 @@ SOCKS5 is the core because:
TUN forwards to SOCKS5 rather than directly to SSH because:
- The SOCKS5 code already handles TCP connection establishment and bidirectional proxying
- TUN's job is just IP packet → SOCKS5 connection, not IP packet → SSH channel
- The `wraith-tun` binary stays minimal (~200-500 lines)
- The `alknet-tun` binary stays minimal (~200-500 lines)
- No root code in the core binary
## Consequences
- **Positive**: Core binary is root-free. TUN functionality is provided by the external `tun2proxy` tool (ADR-014).
- **Positive**: SOCKS5 is testable without TUN — just `curl` against it.
- **Positive**: The TUN approach is validated by tun2proxy, a well-tested existing tool. No custom TUN code to maintain.
- **Negative**: VPN-like behavior requires running `tun2proxy` alongside `wraith connect` — two processes instead of one integrated binary.
- **Negative**: VPN-like behavior requires running `tun2proxy` alongside `alknet connect` — two processes instead of one integrated binary.
- **Negative**: SOCKS5 doesn't capture UDP (except DNS via SOCKS5h). TUN mode via tun2proxy handles this separately.
## References

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**: Reduces legal and privacy exposure for server operators.
- **Positive**: fail2ban can still work — it needs source IPs and auth failures, not destinations.
- **Negative**: Server operators cannot audit what destinations clients are accessing. If an operator needs this for compliance, they must implement it outside wraith (e.g., network-level logging at the target host).
- **Negative**: Server operators cannot audit what destinations clients are accessing. If an operator needs this for compliance, they must implement it outside alknet (e.g., network-level logging at the target host).
- **Negative**: Debugging connectivity issues is harder without destination logs. Mitigated by client-side logging (the client knows what it's connecting to).
## References

View File

@@ -4,7 +4,7 @@
Accepted
## Context
The NAPI wrapper for wraith could expose different granularity levels:
The NAPI wrapper for alknet could expose different granularity levels:
1. **Full SSH API**: Expose channel multiplexing, `open_direct_tcpip`, `tcpip_forward`, session management. The TypeScript layer would manage channels.
2. **Single duplex stream**: The NAPI wrapper establishes one SSH channel and returns it as a Node.js `Duplex` stream. TypeScript multiplexing (if needed) happens at the pubsub layer.

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.
2. **IP-based**: Short-lived certificates via TLS-ALPN-01 challenge on port 443. No domain needed, but cert is short-lived (days, not months). Simpler for quick setups but requires the ACME client to run continuously.
Both flows are important for wraith's usability. Without ACME, TLS mode requires manual cert setup — a significant barrier for users who want "SSH over port 443" for censorship resistance.
Both flows are important for alknet's usability. Without ACME, TLS mode requires manual cert setup — a significant barrier for users who want "SSH over port 443" for censorship resistance.
## Decision
Support both ACME certificate provisioning paths:
@@ -21,10 +21,10 @@ Support both ACME certificate provisioning paths:
3. **Manual certs** (`--tls-cert` / `--tls-key`): Always supported for users with existing certificates or specific PKI setups.
The implementation should use the `rustls-acme` crate (or similar pure-Rust ACME client) to avoid an external certbot dependency. This keeps wraith self-contained as a single binary.
The implementation should use the `rustls-acme` crate (or similar pure-Rust ACME client) to avoid an external certbot dependency. This keeps alknet self-contained as a single binary.
## Consequences
- **Positive**: Users can run `wraith serve --transport tls --acme-domain example.com` and get working TLS with zero manual cert management.
- **Positive**: Users can run `alknet serve --transport tls --acme-domain example.com` and get working TLS with zero manual cert management.
- **Positive**: IP-based ACME covers the quick-setup case without requiring a domain.
- **Positive**: Consistent with our production infrastructure (certbot + Let's Encrypt is already our standard).
- **Negative**: ACME adds complexity to the server binary (challenge responder, cert store, renewal timer).

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.
## Consequences
- **Positive**: Zero-config iroh transport for testing and development. `wraith serve --transport iroh` just works.
- **Positive**: Zero-config iroh transport for testing and development. `alknet serve --transport iroh` just works.
- **Positive**: Self-hosting is a single flag override, not a complex setup requirement.
- **Negative**: Default depends on n0's infrastructure. If n0's relay is down, default iroh connections fail (but this is the same experience as every iroh user).
- **Negative**: Privacy-conscious users must remember to `--iroh-relay` to avoid n0. Mitigated by documentation.

View File

@@ -7,10 +7,10 @@ Accepted
Transport chaining allows combining iroh with an upstream proxy, e.g.:
```bash
wraith connect --transport iroh --proxy socks5://127.0.0.1:1080
alknet connect --transport iroh --proxy socks5://127.0.0.1:1080
```
This routes iroh's outbound TCP connections through a SOCKS5 proxy, which could itself be another wraith instance. This is important for:
This routes iroh's outbound TCP connections through a SOCKS5 proxy, which could itself be another alknet instance. This is important for:
- Nested tunnel topologies
- Environments where iroh needs to go through an existing proxy
- Composing transports in flexible ways

View File

@@ -7,21 +7,21 @@ Accepted
The client and server both need configuration (host addresses, keys, transport options, etc.). There are several approaches:
1. **Read `~/.ssh/config`**: Parse OpenSSH config for default host/key/port. Reduces CLI verbosity for frequent connections.
2. **Custom config file**: Wraith-specific config file (TOML/YAML) with host definitions.
2. **Custom config file**: Alknet-specific config file (TOML/YAML) with host definitions.
3. **Programmatic API only**: Configuration comes from CLI flags or the library API. No file parsing. `~/.ssh/` path conventions are cross-platform trouble (`~` expansion, Windows paths, etc.).
4. **Hybrid**: `--config` flag pointing to a wraith-specific config file, but no OpenSSH config parsing.
4. **Hybrid**: `--config` flag pointing to a alknet-specific config file, but no OpenSSH config parsing.
## Decision
Option 3: Programmatic-first API. Configuration is provided via:
- **CLI**: explicit flags (`--server`, `--identity`, `--transport`, etc.)
- **Library API**: `wraith_core::client::ConnectOptions` and `wraith_core::server::ServeOptions` structs, constructable programmatically
- **Environment variables**: for a few convenience defaults (e.g., `WRAITH_SERVER`, `WRAITH_IDENTITY`)
- **Library API**: `alknet_core::client::ConnectOptions` and `alknet_core::server::ServeOptions` structs, constructable programmatically
- **Environment variables**: for a few convenience defaults (e.g., `ALKNET_SERVER`, `ALKNET_IDENTITY`)
No `~/.ssh/config` parsing, no wraith-specific config files. This approach:
No `~/.ssh/config` parsing, no alknet-specific config files. This approach:
- Avoids cross-platform path issues (`~` expansion, Windows `USERPROFILE`, etc.)
- Makes the library API clean and straightforward for programmatic consumers (NAPI wrapper, pubsub)
- Keeps the CLI simple and explicit — no hidden behavior from config files
- Matches the design principle that the library crate (`wraith-core`) is the primary interface
- Matches the design principle that the library crate (`alknet-core`) is the primary interface
If users want config-file behavior in the future, it can be added as a separate layer that populates the options structs. But the core doesn't need to know about files.

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.
## Consequences
- **Positive**: fail2ban can parse wraith logs the same way it parses SSH and nginx logs on our production systems.
- **Positive**: fail2ban can parse alknet logs the same way it parses SSH and nginx logs on our production systems.
- **Positive**: Built-in rate limiting provides protection on platforms without fail2ban.
- **Positive**: No privacy-sensitive data in logs (no tunnel destinations).
- **Negative**: Slightly more code in the server for connection tracking per IP.
- **Negative**: Users with custom fail2ban filters need to write regex for wraith's log format (documented examples provided).
- **Negative**: Users with custom fail2ban filters need to write regex for alknet's log format (documented examples provided).
## References
- [server.md](../server.md)

View File

@@ -4,7 +4,7 @@
Accepted
## Context
The original plan included a TUN shim (`wraith-tun`) as Phase 3 — a separate root-requiring process that creates a TUN device and forwards IP packets through wraith's SOCKS5 port. This would provide VPN-like "route all traffic" behavior.
The original plan included a TUN shim (`alknet-tun`) as Phase 3 — a separate root-requiring process that creates a TUN device and forwards IP packets through alknet's SOCKS5 port. This would provide VPN-like "route all traffic" behavior.
However, TUN implementation has significant complexities:
- Platform differences (Linux TUN, macOS utun, Windows wintun.dll)
@@ -16,21 +16,21 @@ However, TUN implementation has significant complexities:
The core SOCKS5 interface already works for the vast majority of use cases. For users who truly need VPN-like "route all traffic" behavior, `tun2proxy` is an existing, well-tested tool that does exactly this: creates a TUN device and routes traffic through a SOCKS5 proxy.
## Decision
Defer TUN implementation entirely. Remove `wraith-tun` from the architecture. Instead:
Defer TUN implementation entirely. Remove `alknet-tun` from the architecture. Instead:
1. **Core interface**: wraith's local SOCKS5 proxy (always available, no root required)
2. **VPN-like behavior**: Users who need it run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside `wraith connect`
1. **Core interface**: alknet's local SOCKS5 proxy (always available, no root required)
2. **VPN-like behavior**: Users who need it run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside `alknet connect`
3. **Documentation**: Recommend tun2proxy in the README/wiki for "route all traffic" use cases
This removes TUN from the project scope while still providing a path to VPN-like behavior. If demand justifies it later, `wraith-tun` can be added as a thin wrapper around tun2proxy's pattern.
This removes TUN from the project scope while still providing a path to VPN-like behavior. If demand justifies it later, `alknet-tun` can be added as a thin wrapper around tun2proxy's pattern.
The `tun` feature flag and `wraith-tun` binary are removed from the architecture. The `tun-rs` dependency is removed.
The `tun` feature flag and `alknet-tun` binary are removed from the architecture. The `tun-rs` dependency is removed.
## Consequences
- **Positive**: Significantly reduces project scope and complexity. No TUN code to write, test, or maintain across platforms.
- **Positive**: tun2proxy is already well-tested for this exact use case.
- **Positive**: Core binary remains unprivileged. No root code anywhere in the project.
- **Positive**: Cleaner architecture — wraith only does SSH tunneling + SOCKS5. tun2proxy does TUN.
- **Positive**: Cleaner architecture — alknet only does SSH tunneling + SOCKS5. tun2proxy does TUN.
- **Negative**: Users need two tools instead of one for VPN-like behavior. Mitigated by documentation.
- **Negative**: tun2proxy is an external dependency in practice, though it's widely available in package managers.
- **Negative**: No first-class Windows/macOS TUN story. tun2proxy handles these platforms but users need to install it separately.

View File

@@ -4,35 +4,35 @@
Accepted
## Context
The NAPI wrapper needs to provide TypeScript/Node.js consumers with access to wraith's functionality. The primary use case is `@alkdev/pubsub`'s event target system, which needs both directions:
The NAPI wrapper needs to provide TypeScript/Node.js consumers with access to alknet's functionality. The primary use case is `@alkdev/pubsub`'s event target system, which needs both directions:
1. **connect()**: Establish a client connection to a wraith server. Used by workers/spokes that need to tunnel events through a wraith server.
2. **serve()**: Start a wraith server from Node.js. Used by hubs that want to accept wraith connections and route events.
1. **connect()**: Establish a client connection to a alknet server. Used by workers/spokes that need to tunnel events through a alknet server.
2. **serve()**: Start a alknet server from Node.js. Used by hubs that want to accept alknet connections and route events.
The previous decision (ADR-007) was to expose only `connect()` for MVP, deferring `serve()`. However, the pubsub integration requires both: a spoke needs `connect()` to reach a hub, and a hub could use `serve()` to accept connections without running a separate `wraith serve` process.
The previous decision (ADR-007) was to expose only `connect()` for MVP, deferring `serve()`. However, the pubsub integration requires both: a spoke needs `connect()` to reach a hub, and a hub could use `serve()` to accept connections without running a separate `alknet serve` process.
More importantly, both `connect()` and `serve()` are fundamental operations of the wraith library. Since the NAPI wrapper is a thin layer over `wraith-core`, exposing both is straightforward — they're just Rust functions behind `#[napi]` attributes.
More importantly, both `connect()` and `serve()` are fundamental operations of the alknet library. Since the NAPI wrapper is a thin layer over `alknet-core`, exposing both is straightforward — they're just Rust functions behind `#[napi]` attributes.
## Decision
The NAPI wrapper exposes both `connect()` and `serve()` from the start:
```typescript
// @alkdev/wraith
function connect(options: WraithConnectOptions): Promise<Duplex>;
function serve(options: WraithServeOptions): Promise<WraithServer>;
// @alkdev/alknet
function connect(options: AlknetConnectOptions): Promise<Duplex>;
function serve(options: AlknetServeOptions): Promise<AlknetServer>;
```
- `connect()` returns a `Duplex` stream (as per ADR-007)
- `serve()` returns a `WraithServer` object with a `close()` method and events for new connections
- `serve()` returns a `AlknetServer` object with a `close()` method and events for new connections
The NAPI layer is transport-agnostic — it doesn't know about pubsub's `EventEnvelope`. The pubsub event target adapter wraps the `Duplex` stream to implement `TypedEventTarget`. This separation ensures the NAPI wrapper is reusable for any stream-based protocol, not just pubsub.
## Consequences
- **Positive**: Pubsub can use both directions without running a separate binary for the server side.
- **Positive**: The NAPI wrapper becomes a complete bridge — any Node.js process can be either a client or server.
- **Positive**: Implementation is still minimal — `serve()` is just `wraith_core::server::run()` behind `#[napi]`.
- **Negative**: Slightly larger API surface (two functions + `WraithServer` type instead of just `connect()`).
- **Negative**: Server-side NAPI needs to handle multiple concurrent connections, which adds complexity to `WraithServer`.
- **Positive**: Implementation is still minimal — `serve()` is just `alknet_core::server::run()` behind `#[napi]`.
- **Negative**: Slightly larger API surface (two functions + `AlknetServer` type instead of just `connect()`).
- **Negative**: Server-side NAPI needs to handle multiple concurrent connections, which adds complexity to `AlknetServer`.
## References
- [napi-and-pubsub.md](../napi-and-pubsub.md)

View File

@@ -4,7 +4,7 @@
Accepted
## Context
When running a wraith server with TLS transport on port 443, the server should be indistinguishable from a regular HTTPS web server to port scanners and deep packet inspection (DPI) systems. This is important for censorship circumvention — if SSH traffic on port 443 is detectable, it can be blocked.
When running a alknet server with TLS transport on port 443, the server should be indistinguishable from a regular HTTPS web server to port scanners and deep packet inspection (DPI) systems. This is important for censorship circumvention — if SSH traffic on port 443 is detectable, it can be blocked.
After the TLS handshake completes, the server sees a raw byte stream. SSH protocol identification starts with `SSH-2.0-`, while HTTP starts with HTTP method verbs (GET, POST, etc.). The server can inspect the first bytes to determine the protocol.
@@ -20,7 +20,7 @@ This makes the server appear as an nginx web server returning 404 errors to all
The fake response uses `Server: nginx` headers to match the most common web server profile.
## Consequences
- **Positive**: TLS+wraith servers on port 443 are indistinguishable from ordinary HTTPS sites to automated scanners.
- **Positive**: TLS+alknet servers on port 443 are indistinguishable from ordinary HTTPS sites to automated scanners.
- **Positive**: Simple implementation — just peek at the first bytes and branch.
- **Positive**: Consistent with censorship circumvention best practices.
- **Negative**: Legitimate HTTPS traffic to the same port gets a 404. If the same IP needs to serve real web content, use a reverse proxy (nginx/haproxy) in front that routes by SNI or path.

View File

@@ -4,22 +4,22 @@
Accepted
## Context
The NAPI wrapper and pubsub integration need a way to use wraith's SSH channel as a data plane for event routing. When a `wraith connect` client opens an SSH session to a server, the `direct_tcpip` channel type is used to reach specific TCP targets (host:port).
The NAPI wrapper and pubsub integration need a way to use alknet's SSH channel as a data plane for event routing. When a `alknet connect` client opens an SSH session to a server, the `direct_tcpip` channel type is used to reach specific TCP targets (host:port).
For the pubsub use case, the client needs a dedicated bidirectional stream to the server's event bus — not a TCP connection to a random host. There are several approaches:
1. **Special destination**: Use `direct_tcpip` with a reserved destination (e.g., `wraith-control:0`) that the server recognizes and routes internally instead of connecting to a TCP target.
1. **Special destination**: Use `direct_tcpip` with a reserved destination (e.g., `alknet-control:0`) that the server recognizes and routes internally instead of connecting to a TCP target.
2. **Port forwarding**: The server runs a pubsub hub on a specific port (e.g., 9736) and the client uses normal port forwarding (`-L 9736:hub:9736`).
3. **Custom channel type**: Define a new SSH channel type beyond `direct_tcpip` and `forwarded_tcpip`.
## Decision
Use approach 1: a reserved `direct_tcpip` destination string. When the server receives a `channel_open_direct_tcpip` request for `wraith-control:0`:
Use approach 1: a reserved `direct_tcpip` destination string. When the server receives a `channel_open_direct_tcpip` request for `alknet-control:0`:
1. The `channel_open_direct_tcpip` handler detects the special target via string matching
2. Instead of connecting to a TCP target, it bridges the channel to the local pubsub event bus
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
The destination string `wraith-control` is reserved. Regular TCP targets are hostnames or IP addresses, so there is no collision risk.
The destination string `alknet-control` is reserved. Regular TCP targets are hostnames or IP addresses, so there is no collision risk.
Approach 2 (port forwarding to a specific port) is still supported as an alternative — the client can use `--forward 9736:localhost:9736` if the server runs a pubsub hub on that port. But the control channel approach is simpler and doesn't require a separate listening port.
@@ -27,11 +27,11 @@ Approach 3 (custom channel type) was rejected because russh's `direct_tcpip` han
## Consequences
- **Positive**: Simple implementation — just string matching in the server's `channel_open_direct_tcpip` handler.
- **Positive**: No separate port or service needs to run on the server. The control channel is built into wraith.
- **Positive**: No separate port or service needs to run on the server. The control channel is built into alknet.
- **Positive**: Compatible with the NAPI wrapper's single-duplex-stream model.
- **Positive**: Port forwarding to a specific port is still available as an alternative.
- **Negative**: The string `wraith-control` is a magic constant. It should be defined as a constant in the crate.
- **Negative**: Regular TCP destinations accidentally matching `wraith-control` would be misrouted. Mitigated by reserving the entire `wraith-` prefix namespace.
- **Negative**: The string `alknet-control` is a magic constant. It should be defined as a constant in the crate.
- **Negative**: Regular TCP destinations accidentally matching `alknet-control` would be misrouted. Mitigated by reserving the entire `alknet-` prefix namespace.
## References
- [napi-and-pubsub.md](../napi-and-pubsub.md)

View File

@@ -4,11 +4,11 @@
Accepted
## Context
The `--proxy` CLI flag appears on both `wraith connect` (client) and `wraith serve` (server), but the two sides proxy fundamentally different things:
The `--proxy` CLI flag appears on both `alknet connect` (client) and `alknet serve` (server), but the two sides proxy fundamentally different things:
- **Client**: `--proxy` routes the *transport connection* through the proxy. For example, `wraith connect --transport iroh --proxy socks5://127.0.0.1:1080` means the iroh endpoint's outbound TCP connections go through the specified SOCKS5 proxy before reaching the iroh relay. The proxy wraps the transport layer.
- **Client**: `--proxy` routes the *transport connection* through the proxy. For example, `alknet connect --transport iroh --proxy socks5://127.0.0.1:1080` means the iroh endpoint's outbound TCP connections go through the specified SOCKS5 proxy before reaching the iroh relay. The proxy wraps the transport layer.
- **Server**: `--proxy` routes *outbound target connections* through the proxy. For example, `wraith serve --proxy socks5://127.0.0.1:9050` means when an SSH client opens a `direct_tcpip` channel to `db.internal:5432`, the server connects to that target through the specified proxy. The proxy wraps the data-plane connections.
- **Server**: `--proxy` routes *outbound target connections* through the proxy. For example, `alknet serve --proxy socks5://127.0.0.1:9050` means when an SSH client opens a `direct_tcpip` channel to `db.internal:5432`, the server connects to that target through the specified proxy. The proxy wraps the data-plane connections.
Using the same flag name for both is intentional — from the user's perspective, both mean "route traffic through a proxy." But the layer at which the proxy operates differs, and this needs to be explicit so implementers don't confuse the two.

View File

@@ -5,7 +5,7 @@ Accepted
## Context
Wraith currently authenticates connections exclusively through SSH public key
Alknet currently authenticates connections exclusively through SSH public key
auth in the SSH handshake. This works for SSH-over-any-transport (TCP, TLS,
iroh) because SSH carries its own auth protocol. But WebTransport and other
HTTP-level transports cannot perform SSH key exchange — browsers speak HTTP/3,
@@ -17,8 +17,8 @@ identity system (API keys, JWTs, session tokens). This creates two problems:
same person connecting via SSH and WebTransport appears as two different
identities.
The `IdentityProvider` trait is needed to decouple wraith-core from any
specific identity storage (config file vs. database). Without it, wraith-core
The `IdentityProvider` trait is needed to decouple alknet-core from any
specific identity storage (config file vs. database). Without it, alknet-core
would either hardcode config-file-based auth or take a database dependency —
neither is acceptable for a library crate.
@@ -42,7 +42,7 @@ AuthToken = base64url(key_id || timestamp || signature)
Server extracts the fingerprint, looks it up in the same `authorized_keys`
set, verifies the signature, and checks the timestamp window (default ±300s).
**`IdentityProvider` trait**: Decouples wraith-core from identity storage. The
**`IdentityProvider` trait**: Decouples alknet-core from identity storage. The
trait resolves a fingerprint or token to an `Identity`. Default implementation
loads from `DynamicConfig.auth` (no database). Hub implementation can back it
with `@alkdev/storage`.
@@ -60,7 +60,7 @@ the key material.
- **Positive**: One key set, one rotation, one `reloadAuth()` call. Adding a
key to `authorized_keys` immediately grants access via both SSH and
WebTransport.
- **Positive**: `IdentityProvider` trait makes wraith-core independent of any
- **Positive**: `IdentityProvider` trait makes alknet-core independent of any
specific database. Default: config file. Hub: `@alkdev/storage`.
- **Positive**: Browser clients can authenticate using Ed25519 keys via
SubtleCrypto (Chrome 105+, Firefox 130+, Safari 17+). Deno supports it

View File

@@ -5,7 +5,7 @@ Accepted
## Context
The wraith control channel (ADR-018) routes from client → server's event bus.
The alknet control channel (ADR-018) routes from client → server's event bus.
This is unidirectional: clients can send events to the server, but the server
cannot call operations on the client. In the hub/spoke model, spokes (dev env
containers) connect to a hub and expose operations (fs, bash, search) that the
@@ -35,7 +35,7 @@ Core-provided operations use short paths without a spoke prefix
(`/services/list`, `/services/schema`). Spoke operations are prefixed
(`/dev1/fs/readFile`).
This generalizes ADR-018's control channel: the `wraith-*` destination becomes
This generalizes ADR-018's control channel: the `alknet-*` destination becomes
a transport for `EventEnvelope` frames with call protocol semantics, instead of
raw pubsub dispatch.

View File

@@ -5,7 +5,7 @@ Accepted
## Context
The current control channel (ADR-018) is hardcoded: `wraith-control:0` bridges
The current control channel (ADR-018) is hardcoded: `alknet-control:0` bridges
to the local pubsub event bus. If NAPI wants to expose `fs.readFile` or
`bash.exec` as callable operations, it has no way to register these with core's
channel routing. The NAPI handler would need to intercept channel data outside

View File

@@ -7,14 +7,14 @@ last_updated: 2026-06-02
## What
Two integration layers that enable TypeScript/JavaScript consumers to use wraith as a transport:
Two integration layers that enable TypeScript/JavaScript consumers to use alknet as a transport:
1. **NAPI wrapper** (`@alkdev/wraith`) — A Node.js native addon (via napi-rs) exposing `connect()` and `serve()` that return duplex streams
2. **PubSub event target** (`@alkdev/pubsub` adapter) — An implementation of the `TypedEventTarget` interface that routes events over wraith's SSH channel
1. **NAPI wrapper** (`@alkdev/alknet`) — A Node.js native addon (via napi-rs) exposing `connect()` and `serve()` that return duplex streams
2. **PubSub event target** (`@alkdev/pubsub` adapter) — An implementation of the `TypedEventTarget` interface that routes events over alknet's SSH channel
## Why
The wraith Rust binary serves CLI users. But the broader ecosystem (pubsub, operations, agent spokes) is TypeScript-first. These integration layers let TypeScript code use wraith's transport without reimplementing SSH.
The alknet Rust binary serves CLI users. But the broader ecosystem (pubsub, operations, agent spokes) is TypeScript-first. These integration layers let TypeScript code use alknet's transport without reimplementing SSH.
The NAPI surface is intentionally minimal — it exposes transport connections as duplex streams, not the full SSH protocol. The pubsub adapter wraps those streams with `EventEnvelope` serialization.
@@ -25,9 +25,9 @@ The NAPI surface is intentionally minimal — it exposes transport connections a
The wrapper uses napi-rs (ADR-015) and exposes two functions (ADR-016):
```typescript
// @alkdev/wraith (TypeScript side)
// @alkdev/alknet (TypeScript side)
interface WraithConnectOptions {
interface AlknetConnectOptions {
// TCP/TLS mode
server?: string; // e.g., "example.com:443"
// iroh mode
@@ -45,7 +45,7 @@ interface WraithConnectOptions {
proxy?: string; // upstream SOCKS5/HTTP proxy URL
}
interface WraithServeOptions {
interface AlknetServeOptions {
// Transport
transport: 'tcp' | 'tls' | 'iroh';
// Auth
@@ -63,12 +63,12 @@ interface WraithServeOptions {
}
// Returns a Duplex stream for the SSH channel
function connect(options: WraithConnectOptions): Promise<Duplex>;
function connect(options: AlknetConnectOptions): Promise<Duplex>;
// Returns a server object with close() and connection events
function serve(options: WraithServeOptions): Promise<WraithServer>;
function serve(options: AlknetServeOptions): Promise<AlknetServer>;
interface WraithServer {
interface AlknetServer {
close(): Promise<void>;
onConnection(callback: (stream: Duplex, info: ConnectionInfo) => void): void;
}
@@ -76,18 +76,18 @@ interface WraithServer {
The NAPI layer is **transport-agnostic** — it doesn't know about pubsub's `EventEnvelope`. The pubsub adapter wraps the `Duplex` stream to implement `TypedEventTarget`. This separation ensures the NAPI wrapper is reusable for any stream-based protocol, not tied specifically to pubsub.
### NAPI `connect()` vs CLI `wraith connect`
### NAPI `connect()` vs CLI `alknet connect`
The NAPI `connect()` function and the CLI `wraith connect` command are fundamentally different operations despite sharing the same name:
The NAPI `connect()` function and the CLI `alknet connect` command are fundamentally different operations despite sharing the same name:
- **CLI `wraith connect`**: Starts a full SSH client session with a local SOCKS5 server and optional port forwards. It manages multiple SSH channels over a single session — the user routes traffic through it via SOCKS5 or forwarded ports.
- **CLI `alknet connect`**: Starts a full SSH client session with a local SOCKS5 server and optional port forwards. It manages multiple SSH channels over a single session — the user routes traffic through it via SOCKS5 or forwarded ports.
- **NAPI `connect()`**: Opens a single SSH channel and returns it as a `Duplex` stream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly. This is designed for the pubsub/programmatic use case where a single bidirectional byte stream is needed.
For SOCKS5 proxy functionality, use the CLI binary (`wraith connect`). The NAPI wrapper is for programmatic consumers that need a raw stream.
For SOCKS5 proxy functionality, use the CLI binary (`alknet connect`). The NAPI wrapper is for programmatic consumers that need a raw stream.
### Programmatic Configuration (ADR-011)
Both `connect()` and `serve()` accept options as plain objects. No file paths are mandatory — keys can be provided as `Buffer` data directly, making programmatic usage straightforward. Environment variables (`WRAITH_SERVER`, `WRAITH_IDENTITY`) provide convenience defaults.
Both `connect()` and `serve()` accept options as plain objects. No file paths are mandatory — keys can be provided as `Buffer` data directly, making programmatic usage straightforward. Environment variables (`ALKNET_SERVER`, `ALKNET_IDENTITY`) provide convenience defaults.
Key material provided as `Buffer` must be in **OpenSSH key format** (the format used by `ssh-keygen`). Private keys: OpenSSH format (`-----BEGIN OPENSSH PRIVATE KEY-----`). Public keys: OpenSSH format (`ssh-ed25519 AAAA...`). PEM-encoded keys (PKCS#1, PKCS#8) are not supported.
@@ -96,20 +96,20 @@ Key material provided as `Buffer` must be in **OpenSSH key format** (the format
This implements `TypedEventTarget` from `@alkdev/pubsub`:
```typescript
// @alkdev/pubsub (new adapter: event-target-wraith.ts)
// @alkdev/pubsub (new adapter: event-target-alknet.ts)
export interface WraithEventTargetOptions {
stream: Duplex; // from @alkdev/wraith.connect() or serve()
export interface AlknetEventTargetOptions {
stream: Duplex; // from @alkdev/alknet.connect() or serve()
}
export interface WraithEventTarget<TEvent extends TypedEvent>
export interface AlknetEventTarget<TEvent extends TypedEvent>
extends TypedEventTarget<TEvent> {
close(): void;
}
export function createWraithEventTarget<TEvent extends TypedEvent>(
options: WraithEventTargetOptions
): WraithEventTarget<TEvent>;
export function createAlknetEventTarget<TEvent extends TypedEvent>(
options: AlknetEventTargetOptions
): AlknetEventTarget<TEvent>;
```
Wire protocol (same as other pubsub adapters):
@@ -121,20 +121,20 @@ Wire protocol (same as other pubsub adapters):
### On the Server Side
The wraith server uses a reserved `direct_tcpip` destination (`wraith-control:0`) for the pubsub control channel (ADR-018). When a client connects to this destination:
The alknet server uses a reserved `direct_tcpip` destination (`alknet-control:0`) for the pubsub control channel (ADR-018). When a client connects to this destination:
1. The server's `channel_open_direct_ip` handler detects the reserved `wraith-control` target
1. The server's `channel_open_direct_ip` handler detects the reserved `alknet-control` target
2. Instead of opening a TCP connection, it bridges the channel to its local pubsub event bus
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
Users who prefer not to use the control channel can alternatively run a pubsub hub on a specific port and use standard port forwarding: `wraith connect --forward 9736:hub:9736`. This is a deployment choice, not a separate implementation — wraith's port forwarding works normally for any TCP service.
Users who prefer not to use the control channel can alternatively run a pubsub hub on a specific port and use standard port forwarding: `alknet connect --forward 9736:hub:9736`. This is a deployment choice, not a separate implementation — alknet's port forwarding works normally for any TCP service.
### Direction Agnostic
Because wraith supports both local and remote port forwarding, the event target works in either direction:
Because alknet supports both local and remote port forwarding, the event target works in either direction:
- **Worker connects to hub**: `wraith connect --forward 9736:hub:9736` then create WebSocket event target pointing at `ws://localhost:9736`
- **Hub connects to worker**: `wraith connect --remote-forward 9736:worker:9736` — same result, opposite initiator
- **Worker connects to hub**: `alknet connect --forward 9736:hub:9736` then create WebSocket event target pointing at `ws://localhost:9736`
- **Hub connects to worker**: `alknet connect --remote-forward 9736:worker:9736` — same result, opposite initiator
The pubsub adapter doesn't care which side initiated the SSH session. It just needs a byte stream.
@@ -157,4 +157,4 @@ None — all resolved.
| [011](decisions/011-no-ssh-config-programmatic-api.md) | Programmatic-first API | No file-based config; options are structs or env vars |
| [015](decisions/015-napi-rs-for-ffi-bridge.md) | napi-rs for FFI | Standard Node.js native addon tooling |
| [016](decisions/016-napi-expose-connect-and-serve.md) | Both connect() and serve() | NAPI exposes client and server sides from the start |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel for pubsub | Reserved `wraith-control` destination for event bus |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel for pubsub | Reserved `alknet-control` destination for event bus |

View File

@@ -66,14 +66,14 @@ last_updated: 2026-06-04
- **Origin**: [tun-shim.md](tun-shim.md)
- **Status**: ~~resolved~~
- **Priority**: ~~low~~
- **Resolution**: ADR-014 — TUN is deferred entirely from the wraith project. For VPN-like behavior, users run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside wraith. This eliminates all TUN-related scope questions (Windows, TCP reconstruction, etc.).
- **Resolution**: ADR-014 — TUN is deferred entirely from the alknet project. For VPN-like behavior, users run `tun2proxy --proxy socks5://127.0.0.1:1080` alongside alknet. This eliminates all TUN-related scope questions (Windows, TCP reconstruction, etc.).
- **Cross-references**: [ADR-014](decisions/014-defer-tun-recommend-socks5-proxy.md)
### OQ-09: TCP reconstruction approach for TUN
- **Origin**: [tun-shim.md](tun-shim.md)
- **Status**: ~~resolved~~
- **Priority**: ~~medium~~
- **Resolution**: ADR-014 — TUN is deferred from wraith. tun2proxy (external tool) handles this if users need VPN-like behavior.
- **Resolution**: ADR-014 — TUN is deferred from alknet. tun2proxy (external tool) handles this if users need VPN-like behavior.
- **Cross-references**: [ADR-014](decisions/014-defer-tun-recommend-socks5-proxy.md)
## NAPI / PubSub
@@ -122,7 +122,7 @@ last_updated: 2026-06-04
- **Resolution**: (pending — needs R&D in WebTransport transport session)
- **Cross-references**: [auth.md](auth.md), OQ-19
### OQ-16: Transport-specific forwarding policy (e.g., WebTransport clients restricted to wraith-* channels)
### OQ-16: Transport-specific forwarding policy (e.g., WebTransport clients restricted to alknet-* channels)
- **Origin**: [research/configuration.md](../research/configuration.md)
- **Status**: open
- **Priority**: low
@@ -133,7 +133,7 @@ last_updated: 2026-06-04
- **Origin**: [research/configuration.md](../research/configuration.md)
- **Status**: ~~resolved~~
- **Priority**: ~~medium~~
- **Resolution**: ADR-023 — Unified auth with shared key material. SSH transports use SSH pubkey auth. Non-SSH transports (WebTransport) use Ed25519-signed timestamp tokens. Both verify against the same `authorized_keys` set. The presentation differs per transport, but the identity is unified. `AuthPolicy` holds both `SshAuthConfig` and `TokenAuthConfig`, with `TokenKeySource::Shared` as the default (same keys for both paths). `IdentityProvider` trait decouples wraith-core from identity storage.
- **Resolution**: ADR-023 — Unified auth with shared key material. SSH transports use SSH pubkey auth. Non-SSH transports (WebTransport) use Ed25519-signed timestamp tokens. Both verify against the same `authorized_keys` set. The presentation differs per transport, but the identity is unified. `AuthPolicy` holds both `SshAuthConfig` and `TokenAuthConfig`, with `TokenKeySource::Shared` as the default (same keys for both paths). `IdentityProvider` trait decouples alknet-core from identity storage.
- **Cross-references**: [ADR-023](decisions/023-unified-auth-shared-key-material.md), [auth.md](auth.md), OQ-15
## Auth

View File

@@ -3,33 +3,33 @@ status: reviewed
last_updated: 2026-06-02
---
# Wraith Overview
# Alknet Overview
## Purpose
Wraith is a self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol. It enables:
Alknet is a self-hostable SSH-based tunnel tool that provides VPN-like functionality without being a VPN protocol. It enables:
- **Private tunneling** of services (Postgres, Redis, internal APIs) over SSH
- **Censorship circumvention** — SSH over TLS on port 443 looks like HTTPS to DPI
- **NAT traversal** — iroh transport allows peer-to-peer connections without public IPs or port forwarding
- **Service mesh connectivity** — a lightweight transport layer for the pubsub/operations event system
The core insight: SSH tunnels work because SSH is fundamental infrastructure. Blocking it breaks the internet. Wraith makes SSH tunneling accessible through a simple CLI with pluggable transports.
The core insight: SSH tunnels work because SSH is fundamental infrastructure. Blocking it breaks the internet. Alknet makes SSH tunneling accessible through a simple CLI with pluggable transports.
## Exports
### Binary: `wraith`
### Binary: `alknet`
A single binary with subcommands:
```
wraith serve — Start the server (accepts SSH connections)
wraith connect — Start the client (opens SSH session, exposes SOCKS5/port-forwards)
alknet serve — Start the server (accepts SSH connections)
alknet connect — Start the client (opens SSH session, exposes SOCKS5/port-forwards)
```
### Library: `wraith-core`
### Library: `alknet-core`
The `wraith-core` crate exports the pluggable components for embedding or programmatic use:
The `alknet-core` crate exports the pluggable components for embedding or programmatic use:
- `Transport` trait — produces a duplex stream for SSH to run over
- `TcpTransport` — direct TCP connection
@@ -60,7 +60,7 @@ The `wraith-core` crate exports the pluggable components for embedding or progra
1. **SSH runs over transport, not alongside** — The transport layer produces a single `AsyncRead+AsyncWrite+Unpin+Send` stream. SSH runs over that stream via `russh::client::connect_stream()` / `russh::server::run_stream()`. The SSH layer never knows what transport it's on. (ADR-001, ADR-004)
2. **SOCKS5 is the primary client interface** — Port forwarding is built on top of SOCKS5-like channel management. For VPN-like "route all traffic" behavior, users run `tun2proxy` alongside wraith's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014)
2. **SOCKS5 is the primary client interface** — Port forwarding is built on top of SOCKS5-like channel management. For VPN-like "route all traffic" behavior, users run `tun2proxy` alongside alknet's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014)
3. **No logging of tunnel destinations** — The server logs auth attempts and connections (for fail2ban) but does not log `channel_open_direct_tcpip` destinations, DNS lookups, or bytes transferred. (ADR-006, ADR-013)
@@ -91,11 +91,11 @@ The `wraith-core` crate exports the pluggable components for embedding or progra
| [011](decisions/011-no-ssh-config-programmatic-api.md) | Programmatic-first | No file-based config; options are structs, env vars, CLI flags |
| [012](decisions/012-auth-ed25519-and-cert-authority.md) | Key + cert-authority | Ed25519 keys + OpenSSH CA; no password auth |
| [013](decisions/013-fail2ban-friendly-logging.md) | Fail2ban-friendly | Structured auth logs + built-in rate limiting |
| [014](decisions/014-defer-tun-recommend-socks5-proxy.md) | Defer TUN | Use tun2proxy for VPN-like behavior; no wraith-tun binary |
| [014](decisions/014-defer-tun-recommend-socks5-proxy.md) | Defer TUN | Use tun2proxy for VPN-like behavior; no alknet-tun binary |
| [015](decisions/015-napi-rs-for-ffi-bridge.md) | napi-rs | Standard Node.js native addon tooling |
| [016](decisions/016-napi-expose-connect-and-serve.md) | connect + serve | NAPI exposes both client and server from the start |
| [017](decisions/017-stealth-mode-protocol-multiplexing.md) | Stealth mode | Protocol multiplexing on port 443 |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `wraith-control` destination for pubsub |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `alknet-control` destination for pubsub |
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |
## Open Questions

View File

@@ -7,7 +7,7 @@ last_updated: 2026-06-02
## What
The wraith server accepts SSH connections (via pluggable transport) and handles `channel_open_direct_tcpip` requests by connecting to the requested target — either directly or through an outbound proxy.
The alknet server accepts SSH connections (via pluggable transport) and handles `channel_open_direct_tcpip` requests by connecting to the requested target — either directly or through an outbound proxy.
## Why
@@ -19,7 +19,7 @@ The server is the tunnel endpoint. It receives SSH channels requesting TCP conne
```
┌──────────────────────────────────────────────────┐
wraith serve │
alknet serve │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ SSH Server (russh) │ │
@@ -85,7 +85,7 @@ ACME support is feature-gated behind the `acme` feature flag to keep the base bi
When a client opens a `channel_open_direct_tcpip(host, port, originator_addr, originator_port)`:
**Reserved destination** — If `host` starts with `wraith-` (e.g., `wraith-control`), the server routes the channel internally instead of connecting to a TCP target. The primary reserved destination is `wraith-control:0`, which bridges the channel to the local pubsub event bus (ADR-018).
**Reserved destination** — If `host` starts with `alknet-` (e.g., `alknet-control`), the server routes the channel internally instead of connecting to a TCP target. The primary reserved destination is `alknet-control:0`, which bridges the channel to the local pubsub event bus (ADR-018).
**Regular destination** — For all other targets:
@@ -128,7 +128,7 @@ The server handler implements `russh::server::Handler` with two primary responsi
- Return `Accept` or `Reject`
**Channel handling (`channel_open_direct_tcpip`)**:
- If the destination host starts with `wraith-`, route internally (control channel, ADR-018)
- If the destination host starts with `alknet-`, route internally (control channel, ADR-018)
- Otherwise, connect to `host:port` (directly or via the configured outbound proxy)
- Spawn a bidirectional proxy task between the SSH channel and the outbound TCP stream
- Return the channel for data flow
@@ -161,44 +161,44 @@ These provide abuse protection on platforms without fail2ban (macOS, Windows, BS
```bash
# Basic server (SSH on port 22)
wraith serve --key ~/.ssh/ssh_host_ed25519_key
alknet serve --key ~/.ssh/ssh_host_ed25519_key
# With TLS (manual certs)
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--transport tls \
--tls-cert /etc/ssl/cert.pem \
--tls-key /etc/ssl/key.pem
# With TLS (auto ACME, domain-based)
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--transport tls \
--acme-domain example.com
# With TLS + stealth (fake nginx 404 to scanners)
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--transport tls \
--acme-domain example.com \
--stealth
# With iroh transport (no public IP needed)
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--transport iroh
# With outbound proxy
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--proxy socks5://127.0.0.1:9050
# With certificate authority authentication
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
--cert-authority /etc/wraith/ca.pub
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--cert-authority /etc/alknet/ca.pub
# With rate limiting
wraith serve --key ~/.ssh/ssh_host_ed25519_key \
alknet serve --key ~/.ssh/ssh_host_ed25519_key \
--max-connections-per-ip 5 \
--max-auth-attempts 3
# All options
wraith serve \
alknet serve \
--key <path-or-buffer> \ # SSH host key (required)
--authorized-keys <path> \ # Authorized keys file
--cert-authority <path> \ # CA public key for cert-auth
@@ -218,7 +218,7 @@ wraith serve \
When running with `--transport iroh`, the server:
1. Creates an iroh endpoint with ALPN value `b"wraith-ssh"`
1. Creates an iroh endpoint with ALPN value `b"alknet-ssh"`
2. Prints its endpoint ID (base58-encoded Ed25519 public key) — this is what clients use as the `--peer` value
3. Accepts incoming connections on the endpoint
4. For each connection, accepts a bidirectional stream and passes it to `server::run_stream()`
@@ -228,7 +228,7 @@ No listening port is needed. The server connects outbound to the iroh relay (def
## Constraints
- The server does not log tunnel destinations (ADR-006). Auth events and connection events are logged for fail2ban integration (ADR-013).
- Destination strings beginning with `wraith-` are reserved for internal use (ADR-018). The server must not attempt TCP connections to `wraith-*` destinations — these are intercepted for control channel routing.
- Destination strings beginning with `alknet-` are reserved for internal use (ADR-018). The server must not attempt TCP connections to `alknet-*` destinations — these are intercepted for control channel routing.
- One `ServerHandler` instance per connection. Handler state is not shared between connections (unless explicitly configured via `Arc` shared state for things like connection limits).
- The server binds to a single transport at a time. Running multiple transports (e.g., TCP + iroh) simultaneously requires separate processes or a future multiplexing feature.
- ACME support requires the `acme` feature flag. Without it, only manual TLS certs are supported.
@@ -271,5 +271,5 @@ None — all resolved.
| [012](decisions/012-auth-ed25519-and-cert-authority.md) | Key + cert-authority auth | No password auth; support OpenSSH cert-authority |
| [013](decisions/013-fail2ban-friendly-logging.md) | Fail2ban-friendly logging | Structured auth logs + built-in rate limiting |
| [017](decisions/017-stealth-mode-protocol-multiplexing.md) | Stealth mode | Protocol multiplexing on port 443 |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `wraith-control` destination for pubsub |
| [018](decisions/018-control-channel-for-pubsub.md) | Control channel | Reserved `alknet-control` destination for pubsub |
| [019](decisions/019-proxy-dual-semantics.md) | Proxy dual semantics | `--proxy` routes transport on client, data on server |

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):
```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.

View File

@@ -5,21 +5,21 @@ last_updated: 2026-06-01
# TUN Shim (Deprecated)
> **Note**: TUN functionality has been deferred from the wraith project. For VPN-like "route all traffic" behavior, use `tun2proxy` alongside wraith's SOCKS5 proxy. See ADR-014 for the rationale.
> **Note**: TUN functionality has been deferred from the alknet project. For VPN-like "route all traffic" behavior, use `tun2proxy` alongside alknet's SOCKS5 proxy. See ADR-014 for the rationale.
## What Changed
The `wraith-tun` separate process and all TUN-related code is out of scope. The recommended approach for VPN-like behavior is:
The `alknet-tun` separate process and all TUN-related code is out of scope. The recommended approach for VPN-like behavior is:
```bash
# Terminal 1: wraith SOCKS5 proxy (no root required)
wraith connect --server example.com --identity ~/.ssh/id_ed25519
# Terminal 1: alknet SOCKS5 proxy (no root required)
alknet connect --server example.com --identity ~/.ssh/id_ed25519
# Terminal 2: tun2proxy routes all traffic through wraith's SOCKS5
# Terminal 2: tun2proxy routes all traffic through alknet's SOCKS5
sudo tun2proxy --proxy socks5://127.0.0.1:1080
```
This keeps the core wraith binary free of TUN complexity and leverages an existing, well-tested tool for TUN-to-SOCKS5 bridging.
This keeps the core alknet binary free of TUN complexity and leverages an existing, well-tested tool for TUN-to-SOCKS5 bridging.
## References

View File

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

View File

@@ -19,7 +19,7 @@ No password auth. The client handler is simpler than the server — it just need
## 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::from_key_source(source: KeySource) -> Result<Self>` — loads key via key-loading module
- [ ] 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
- [ ] `crates/wraith-core/src/error.rs` exports error types
- [ ] `crates/alknet-core/src/error.rs` exports error types
- [ ] `TransportError` enum: `ConnectionFailed`, `HandshakeFailed`, `Timeout`, `ProxyFailed`
- [ ] `AuthError` enum: `KeyRejected`, `CertInvalid`, `CertExpired`, `CertPrincipalMismatch`, `NoMatchingKey`
- [ ] `ChannelError` enum: `TargetUnreachable`, `ProxyConnectFailed`, `ChannelClosed`
- [ ] `ConfigError` enum: `InvalidFlag`, `KeyFileNotFound`, `BindFailed`, `IncompatibleOptions`
- [ ] 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 }`)
- [ ] 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
## 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
- [ ] `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
- [ ] `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

View File

@@ -22,7 +22,7 @@ No password authentication over SSH. This is the `russh::server::Handler::auth_p
## 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::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`

View File

@@ -1,6 +1,6 @@
---
id: cli/connect-command
name: Implement `wraith connect` CLI subcommand with clap
name: Implement `alknet connect` CLI subcommand with clap
status: pending
depends_on:
- client/connect-options
@@ -12,15 +12,15 @@ level: implementation
## 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).
## 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)
- [ ] `--peer` required for iroh transport (validated)
- [ ] `--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`
- [ ] `--forward` is repeatable (clap `multiple_occurrences`)
- [ ] `--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)
- [ ] CLI translates args into `ConnectOptions` and calls `ClientSession::new(opts).run().await`
- [ ] 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

View File

@@ -1,6 +1,6 @@
---
id: cli/serve-command
name: Implement `wraith serve` CLI subcommand with clap
name: Implement `alknet serve` CLI subcommand with clap
status: completed
depends_on:
- server/serve-loop
@@ -12,16 +12,16 @@ level: implementation
## 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.
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
- [x] `crates/wraith/src/main.rs` defines CLI with clap derive: `wraith` 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] `crates/alknet/src/main.rs` defines CLI with clap derive: `alknet` with `serve` and `connect` subcommands (connect stub for now)
- [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] `--transport` defaults to `tcp`
- [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] CLI translates args into `ServeOptions` and calls `Server::new(opts).run().await`
- [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
@@ -44,4 +44,4 @@ All 12 CLI flags implemented. ServeTransportModeArg ValueEnum maps to ServeTrans
## 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
- [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::new()` establishes initial transport connection, authenticates, returns manager
- [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
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
- [ ] `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::identity` accepts `KeySource` (file or in-memory)
- [ ] `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
- [ ] `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`
- [ ] `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

View File

@@ -25,14 +25,14 @@ Supports SOCKS5h (domain names resolved server-side) by default. This prevents D
## 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`)
- [ ] 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
- [ ] 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
- [ ] 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)
## References

View File

@@ -1,6 +1,6 @@
---
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
depends_on:
- cli/serve-command
@@ -13,13 +13,13 @@ level: planning
## 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
- [x] Both CLI tasks completed
- [x] `wraith serve --help` and `wraith connect --help` match architecture spec flag lists
- [x] End-to-end: `wraith serve` + `wraith connect` establishes working SSH tunnel
- [x] `alknet serve --help` and `alknet connect --help` match architecture spec flag lists
- [x] End-to-end: `alknet serve` + `alknet connect` establishes working SSH tunnel
## References
@@ -27,4 +27,4 @@ Meta task that clusters CLI tasks. Once complete, the `wraith` binary has both `
## 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
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
- [x] All NAPI tasks completed
- [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] 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
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
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
@@ -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] Stealth mode detects SSH vs HTTP and returns fake nginx 404
- [x] Rate limiting and structured logging
- [x] Control channel routing for `wraith-*` destinations
- [x] Control channel routing for `alknet-*` destinations
- [x] Graceful shutdown
## References
@@ -40,4 +40,4 @@ All server module tasks completed across Gens 4-7. Server layer is fully impleme
## 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
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.
- **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
- [ ] `#[napi]` function `connect(options: WraithConnectOptions) -> Result<DuplexStream>` in `crates/wraith-napi/src/connect.rs`
- [ ] `WraithConnectOptions` struct with napi fields: `server`, `peer`, `transport`, `identity`, `tlsServerName`, `insecure`, `irohRelay`, `proxy`
- [ ] `#[napi]` function `connect(options: AlknetConnectOptions) -> Result<DuplexStream>` in `crates/alknet-napi/src/connect.rs`
- [ ] `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
- [ ] SSH client connection: create transport stream, authenticate, open single `direct_tcpip` channel
- [ ] Channel returned as `napi::DuplexStream` for JavaScript consumption
- [ ] 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
- [ ] 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
## References

View File

@@ -1,6 +1,6 @@
---
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
depends_on:
- setup/project-init
@@ -12,7 +12,7 @@ level: implementation
## 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`.
@@ -20,11 +20,11 @@ The Cargo.toml skeleton was created in setup/project-init. This task configures
## Acceptance Criteria
- [ ] `crates/wraith-napi/` has `Cargo.toml` with `crate-type = ["cdylib"]`, `napi` and `napi-derive` dependencies
- [ ] `crates/wraith-napi/src/lib.rs` with napi module registration
- [ ] `packages/wraith-napi/` directory (or similar) with `package.json` named `@alkdev/wraith`
- [ ] `packages/wraith-napi/tsconfig.json` for TypeScript type generation
- [ ] TypeScript type definitions for `WraithConnectOptions`, `WraithServeOptions`, `WraithServer`, `ConnectionInfo` matching napi-and-pubsub.md interfaces
- [ ] `crates/alknet-napi/` has `Cargo.toml` with `crate-type = ["cdylib"]`, `napi` and `napi-derive` dependencies
- [ ] `crates/alknet-napi/src/lib.rs` with napi module registration
- [ ] `packages/alknet-napi/` directory (or similar) with `package.json` named `@alkdev/alknet`
- [ ] `packages/alknet-napi/tsconfig.json` for TypeScript type generation
- [ ] 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
- [ ] Build command: `npm run build` builds the native addon
- [ ] Feature flags: `iroh` feature optional; base package includes tcp + tls

View File

@@ -13,15 +13,15 @@ level: implementation
## 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
- [x] `#[napi]` function `serve(options: WraithServeOptions) -> Result<WraithServer>` in `crates/wraith-napi/src/serve.rs`
- [x] `WraithServeOptions` 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] `#[napi]` function `serve(options: AlknetServeOptions) -> Result<AlknetServer>` in `crates/alknet-napi/src/serve.rs`
- [x] `AlknetServeOptions` struct with napi fields: `transport`, `hostKey`, `authorizedKeys`, `certAuthority`, `tlsCert`, `tlsKey`, `acmeDomain`, `listen`, `irohRelay`
- [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] `ConnectionInfo` struct passed with each connection: `remoteAddr`, `transportKind`
- [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
- 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/server.md — server configuration
## 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
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
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
- [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] 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] NAPI `connect()` returns Duplex stream; data flows bidirectionally
- [x] NAPI `serve()` accepts connections; `onConnection` emits Duplex streams
- [x] NAPI key material from Buffer works (not just file paths)
- [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] 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] 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
Final review complete. All acceptance criteria verified:
- CLI binary: wraith serve/connect with all flags, env vars, stealth validation
- NAPI: connect() returns WraithStream, serve() returns WraithServer with onConnection
- CLI binary: alknet serve/connect with all flags, env vars, stealth validation
- NAPI: connect() returns AlknetStream, serve() returns AlknetServer with onConnection
- Feature flags: tls, iroh, acme correctly gate optional code; base build compiles
- ADR-006: no server-side logging of tunnel destinations
- 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] No logging of tunnel destinations (ADR-006)
- [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] All tests pass: `cargo test --workspace`
- [x] `cargo clippy --workspace` passes
@@ -40,7 +40,7 @@ Verify end-to-end SSH tunnel flow: client connects → SOCKS5 proxy works → po
## Summary
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
- fixed ADR-006 violations (removed server-side destination logging)
- 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
- [ ] `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 }`
- [ ] `connect_outbound(target: SocketAddr, proxy: &ProxyConfig) -> Result<TcpStream>` — connects to target directly or via proxy
- [ ] Direct mode: `TcpStream::connect(target)`

View File

@@ -1,6 +1,6 @@
---
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
depends_on:
- server/handler
@@ -13,26 +13,26 @@ level: implementation
## 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
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.
## Acceptance Criteria
- [ ] `crates/wraith-core/src/server/control_channel.rs` exports `ControlChannelHandler` trait and routing logic
- [ ] `WRAITH_CONTROL_DESTINATION` constant defined as `"wraith-control"` (ADR-018)
- [ ] `WRAITH_PREFIX` constant defined as `"wraith-"` for namespace reservation
- [ ] `crates/alknet-core/src/server/control_channel.rs` exports `ControlChannelHandler` trait and routing logic
- [ ] `ALKNET_CONTROL_DESTINATION` constant defined as `"alknet-control"` (ADR-018)
- [ ] `ALKNET_PREFIX` constant defined as `"alknet-"` for namespace reservation
- [ ] `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)
- [ ] 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
## 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:
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).
## 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` holds: `Arc<ServerAuthConfig>`, `outbound_proxy: Option<ProxyConfig>`, `remote_addr: Option<SocketAddr>`
- [ ] `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)
- [ ] 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
@@ -34,7 +34,7 @@ At this stage, implement the handler struct, auth delegation, and the channel di
## References
- 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
## Notes

View File

@@ -21,7 +21,7 @@ No logging of tunnel destinations, DNS resolutions, or bytes transferred (ADR-00
## 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::check(ip) -> bool` — returns `true` if connection allowed, `false` if over limit
- [ ] `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
- [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] `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()`
@@ -56,7 +56,7 @@ Key design decisions:
## 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
- `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()

View File

@@ -25,7 +25,7 @@ Stealth mode requires TLS transport. The CLI should reject or warn if `--stealth
## 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
- [ ] `ProtocolDetection` enum: `Ssh`, `Http` (or `Unknown`)
- [ ] If SSH detected: pass stream to `russh::server::run_stream()`

View File

@@ -1,6 +1,6 @@
---
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
depends_on: []
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:
- **`wraith-core`** — library crate with feature flags (`tls`, `iroh`, `acme`). All core logic lives here.
- **`wraith`** — binary crate depending on `wraith-core`. CLI entry point.
- **`wraith-napi`** — napi-rs crate for the Node.js native addon (skeleton only at this stage).
- **`alknet-core`** — library crate with feature flags (`tls`, `iroh`, `acme`). All core logic lives here.
- **`alknet`** — binary crate depending on `alknet-core`. CLI entry point.
- **`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.
## Acceptance Criteria
- [ ] `Cargo.toml` workspace root with `[workspace]` members: `crates/wraith-core`, `crates/wraith`, `crates/wraith-napi`
- [ ] `crates/wraith-core/Cargo.toml` with library crate, feature flags: `tls` (tokio-rustls + rustls), `iroh` (iroh), `acme` (rustls-acme, implies `tls`)
- [ ] `Cargo.toml` workspace root with `[workspace]` members: `crates/alknet-core`, `crates/alknet`, `crates/alknet-napi`
- [ ] `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`
- [ ] `crates/wraith/Cargo.toml` with binary crate, depends on `wraith-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/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/wraith/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/Cargo.toml` with binary crate, depends on `alknet-core` with default features, `clap` with `derive` feature
- [ ] `crates/alknet-napi/Cargo.toml` with `cdylib` crate type, depends on `alknet-core`, `napi` and `napi-derive`
- [ ] `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/alknet/src/main.rs` with minimal `fn main()` skeleton
- [ ] `crates/alknet-napi/src/lib.rs` with `#[macro_use] extern crate napi_derive;` and empty skeleton
- [ ] `.gitignore` covers `target/`, `node_modules/`
- [ ] `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
- 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

View File

@@ -18,8 +18,8 @@ The mock transport is critical — it lets us test SSH client/server flows witho
## 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/wraith-core/src/testutil.rs` module (behind `#[cfg(test)]` or a `testutil` feature) exporting `MockTransport` and `MockStream`
- [ ] `crates/alknet-core/tests/` directory with empty integration test skeletons: `transport_tests.rs`, `client_tests.rs`, `server_tests.rs`, `auth_tests.rs`
- [ ] `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`
- [ ] `MockTransport` implements `Transport` trait (once defined) returning `MockStream` via `connect()`
- [ ] `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
- [ ] `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
- [ ] `AcmeCertProvider` struct accepts: domain (domain-based) or IP mode flag
- [ ] 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
- [ ] `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::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>"`
- [ ] `IrohAcceptor` holds an `iroh::Endpoint` instance
- [ ] `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
- [ ] `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::connect()` calls `TcpStream::connect(addr)` and returns the stream
- [ ] `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
- [ ] `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::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)

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