A fast, secure, reliable TCP tunnel for administrators to reach servers whose firewalls drop all inbound traffic. The admin (client) sends an authenticated knock that briefly opens the tunnel port to its current IP, connects out directly (no relay, no broker, no P2P), authenticates with a shared key, and forwards local ports to any service on the server — including SSH on 22 — across many servers at once.
Like NetBird's "no open ports" property, but without P2P and without WireGuard: Single Packet Authorization keeps the server unscannable, and the data path stays direct.
Status: in progress
- Stealth: Single Packet Authorization (SPA). The firewall default-DROPs everything; an authenticated knock — bound to that server's identity, so a capture is useless against the rest of the fleet — transiently opens the tunnel port to your current IP only.
- Crypto: Noise
Noise_IKpsk2_25519_ChaChaPoly_BLAKE2svianoise-rust— per-server static X25519 identities, ephemerals for forward secrecy, ChaCha20-Poly1305 AEAD, and a shared key (PSK) entered at start. The Noise framework WireGuard is built on, but not WireGuard (no kernel module, nowg, no UDP data path). - Transport: TCP, yamux multiplexing, a small self-healing per-server connection pool.
- Priorities: security → reliability → speed.
See ARCHITECTURE.md for the design and THREAT_MODEL.md
for the security model. Status: functional core (tunnel, mux, relay, pooled client,
SPA). The SPA path is implemented and type-checks for Linux but is not yet runtime-verified
on a live host, and the protocol has not had an external review — do not use in
production yet.
cargo build --release
# binaries: target/release/hyperion-server, target/release/hyperion-clientThe client includes L3 TUN mode by default (pulls in a userspace TUN/IP stack). For a
lean client without it: cargo build --release -p hyperion-vpn-client --no-default-features.
The fast path. Config and keys live in your user config dir (%APPDATA%\hyperion on
Windows, ~/.config/hyperion on Linux) — generated, not hand-written. Each server is
reached at a stable fake IP inside the client's TUN subnet (10.99.0.0/24), so there
are no local ports, no route scripts, and no Linux-only socket tricks — it works on the
Windows admin box.
1. Admin (Windows) — initialize. Prints your admin public key and the shared salt:
hyperion-client init2. Each Linux server — initialize with the admin key + salt from step 1, then start it.
up runs the daemon in the background; the firewall stays a separate, deliberate step:
hyperion-server init --salt <SALT> --admin-key <ADMIN_PUBKEY> --allow 22,5432
# prints the server's public key — copy it for step 3
hyperion-server print-firewall --tunnel-port 8443 | sudo nft -f - # review first (default-DROP)
sudo hyperion-server up3. Admin — add each server by its public key (auto-assigns a fake IP), then go up:
hyperion-client add-server web1 203.0.113.10:8443 <SERVER_PUBKEY>
hyperion-client up # run elevated; needs wintun.dll alongside the exe on Windows
hyperion-client status # shows the server → fake-IP table4. Use it — reach each server at its fake IP with any app:
ssh user@10.99.0.10
hyperion-client down # stop the tunnelBoth sides take up / down / status; --foreground runs in the current shell instead
of detaching. The SPA firewall is never applied automatically — apply and remove it
yourself (it is default-DROP and can lock you out).
Proves the tunnel end-to-end on one machine. Run a throwaway service to reach (here, an SSH server on 22, or any TCP service).
-
Generate keys.
hyperion-server keygen --out server.key # prints the server PUBLIC key hyperion-client keygen --out admin.key # prints the admin PUBLIC key
-
server.toml— note the admin public key in the allowlist, egress limited to the one service you want to reach:listen = "127.0.0.1:8443" [key] source = "passphrase" salt = "change-me-shared-salt" # same on client and server [identity] static_key_file = "server.key" admin_pubkeys = ["<ADMIN_PUBLIC_KEY>"] [egress] allow = [22] # deny-all by default; list each port (server dials its own loopback)
-
client.toml— forward a local port to the server's service:admin_static_key_file = "admin.key" [key] source = "passphrase" salt = "change-me-shared-salt" # MUST match the server [[server]] name = "srvA" addr = "127.0.0.1:8443" server_pubkey = "<SERVER_PUBLIC_KEY>" [[forward]] local = "127.0.0.1:2201" server = "srvA" remote_port = 22
-
Run (each prompts once, no-echo, for the shared passphrase):
hyperion-server run --config server.toml hyperion-client run --config client.toml
-
Use it. This simple (no-root) mode exposes each service on a local port you pick; for the real
<server-ip>:<port>with no local ports, use L3 TUN mode (below) instead.hyperion-client doctor --config client.toml # check knock + handshake first ssh -p 2201 user@127.0.0.1 # 2201 is the chosen local port → server's :22
Ad-hoc forwards without editing config: hyperion-client run -L 2201:srvA:22.
The SPA gate needs Linux, root (CAP_NET_RAW + CAP_NET_ADMIN), and nftables.
-
Install the base firewall ruleset (default-drop; only conntrack + loopback + knock-opened IPs may reach the tunnel port). Generate and review it, then apply on a host you have console/out-of-band access to:
hyperion-server print-firewall --tunnel-port 8443 | sudo nft -f -WARNING: this sets
policy dropon input. Ensure you have a recovery path before applying it remotely, or you can lock yourself out. -
Enable SPA in
server.toml:listen = "0.0.0.0:8443" [knock] enabled = true window_secs = 30 [firewall] table = "hyperion" set = "knock_allow" ttl_secs = 60
-
Enable knock in
client.toml(knocks each server before dialing):[knock] enabled = true
-
Run the server as root (or with the two capabilities). On a valid knock it adds the admin's source IP to the
knock_allowset with a 60 s timeout; the client connects within that window and the established tunnel survives via conntrack.
For many ports across many servers, this is the simple path: the client lists only
the servers (no [[forward]] blocks at all) and you reach any <server-ip>:<port>
directly. The server's egress allowlist still caps which ports. Built by default
(--no-default-features for the lean local-forward-only build).
- Needs root /
CAP_NET_ADMINon the client and a TUN driver (Linux; Windows via Wintun, but the routing/bypass below is Linux-only). Status: compiles for Linux + Windows; not yet runtime-verified.
client.toml for e.g. 4 servers (10 ports each) — just the servers:
admin_static_key_file = "admin.key"
[key]
source = "passphrase"
salt = "change-me-shared-salt"
[[server]]
name = "srvA"
addr = "203.0.113.10:8443"
server_pubkey = "<SRVA_PUBKEY>"
# …srvB / srvC / srvD the same — and NO [[forward]] blocksEach server.toml lists its ports once: [egress] allow = [22, 80, 443, 5432, …].
Run it (two steps — the client marks its own sockets with SO_MARK automatically):
# 1. start the tunnels + the hyperion0 TUN device
sudo hyperion-client tun --config client.toml
# 2. install routing: route each server IP via the tun, but keep the tunnel's own
# (marked) sockets on the physical route so it can't loop through itself
hyperion-client print-routes --config client.toml | sudo sh
# tear down later: hyperion-client print-routes --config client.toml --down | sudo shThen reach everything at its real address — no local ports, any app:
ssh user@203.0.113.10
curl http://203.0.113.11:8080
psql -h 203.0.113.12 -p 5432Notes: TCP only (UDP dropped). On Windows the TUN device works but SO_MARK + ip rule don't — that socket-bypass is a TODO, so run the TUN client on Linux (your
Windows box can still use the lean local-forward build).
Config defaults to the user config dir when --config is omitted (%APPDATA%\hyperion /
~/.config/hyperion); override the whole dir with the HYPERION_HOME env var.
hyperion-server
init --salt <s> --admin-key <pk> [--allow 22,5432] [--listen <a:p>] [--force]— generate the server key + config in the user config dir.up [--foreground]/down/status— start (background) / stop / inspect the daemon.run [--config <file>]— foreground: load config, (Linux) start the SPA sniffer, listen, relay.keygen [--out <file>]— generate a server static X25519 keypair.print-firewall --tunnel-port <p> [--table <t>] [--set <s>]— emit the base nft ruleset.
hyperion-client
init [--force]— generate the admin key + config; prints the admin pubkey + shared salt.add-server <name> <host:port> <server_pubkey>/rm-server <name>/ls— manage servers (each gets a stable fake IP); no hand-editing.up [--foreground]/down/status— start (background) / stop / inspect the fake-IP tunnel.run [--config <file>] [-L lport:server:rport ...]— foreground local-port forwards.keygen [--out <file>]— generate the admin static keypair.doctor [--config <file>]— knock + handshake against every server; no forwarding.tun --config <file> [--tun-addr <ip>] [--prefix <n>] [--mtu <n>]— L3 TUN mode (requires thetunbuild feature + root; see L3 TUN mode above).print-routes --config <file> [--dev <name>] [--mark <n>] [--table <n>] [--down]— emit theip route/ip rulecommands to route server IPs via the TUN (pipe tosh).
Set RUST_LOG=info (or debug) for logs.
Shared key ([key]) — source is one of:
passphrase—saltrequired; readspassphrase_envor prompts (no-echo); Argon2id.env— base64 PSK inenv_var.file— base64 PSK infile(server only).value— inline base64 PSK (discouraged).
The client [key] is the fleet default; a [[server]] may override with key_env /
key_value. The PSK and salt must be identical on both ends.
Identity — per-server static keypair (server keygen); the public key goes in the
client's server_pubkey. The admin keypair (client keygen); its public key goes in the
server's identity.admin_pubkeys. The server rejects any peer whose static key is not
allow-listed, independent of the (dynamic) source IP.
Egress (server) — allow is deny-all by default and is a list of port
numbers (e.g. allow = [22, 5432]). The server only ever dials its own loopback
(127.0.0.1:<port>), so no host is configured; this caps what a leaked PSK can reach.
- Key rotation: regenerate server/admin keypairs and rotate the PSK on a schedule and
on suspected compromise. Update
admin_pubkeys/server_pubkeyaccordingly. - Egress hygiene: keep the allowlist minimal (loopback services only, where possible).
- Knock window:
window_secsbounds replay/clock-skew; keep it tight (≈30 s) and run NTP on both ends. - Keepalive: tunnels use TCP keepalive (20 s idle) to keep conntrack warm; the client pool reconnects with backoff (and re-knocks) on any drop.
doctorsays connection refused / timed out: server not running, wrongaddr, or (with SPA) the knock didn't open the port — check the server log forknock accepted.- Knock not accepted: PSK/
saltmismatch, clock skew beyondwindow_secs, or the sniffer isn't running (Linux + root required; non-Linux logs a warning and skips SPA). - Locked out after
nft -f: the base ruleset is default-drop — recover via console and flush the table. - Slow/stalls under heavy loss: inherent TCP-over-TCP head-of-line blocking; raise the
per-server
pool_sizeand ensureTCP_NODELAYpaths (default). SeeTHREAT_MODEL.md. - Forward fails with denied/unreachable: the target isn't in the server
[egress]allowlist, or nothing is listening on it.
- Server: Linux + root (
CAP_NET_RAW+CAP_NET_ADMIN) for SPA (nftables + AF_PACKET). Without SPA the relay runs anywhere, but then the tunnel port must be reachable. - Client: cross-platform (Linux/macOS/Windows) — only sends a knock and opens TCP.
GPL-3.0-or-later.