Rusnel
Description
Rusnel is a fast TCP/UDP tunnel, transported over and encrypted using QUIC protocol. Single executable including both client and server. Written in Rust.
Quickstart
# on your public box
# server cert fingerprint: sha256:abcd...
# on your laptop — expose laptop:8080 as <server>:8080
Features
- Easy to use
- Single executable including both client and server.
- Uses QUIC protocol for fast and multiplexed communication.
- Encrypted connections using the QUIC protocol (TLS 1.3).
- Static forward tunneling (TCP, UDP)
- Static reverse tunneling (TCP, UDP)
- Dynamic tunneling (socks5, including UDP ASSOCIATE)
- Dynamic reverse tunneling (reverse socks5, including UDP ASSOCIATE)
- Layered peer authentication: insecure, fingerprint pinning, or full mTLS (see Authentication).
Install
Or build from source:
Pre-built binaries for Linux (x86_64 + aarch64, gnu and musl) and macOS (x86_64 + Apple Silicon) are attached to each GitHub release. Windows is not yet supported (the admin API is Unix-socket-only).
Docker
Multi-arch images (linux/amd64, linux/arm64) are published to GHCR:
Note that QUIC runs over UDP — -p 8080:8080/udp, not /tcp.
Usage
)
)
)
)
)
)
;
)
<SERVER> defines )
<remote>...
<remote>s
<local-host>:<local-port>:<remote-host>:<remote-port>/<protocol>
)
)
)
The client survives both transient network drops and full server restarts
out of the box: on disconnect it logs the close reason (e.g.
closed by peer: server received ^C (code 0)), backs off, and tries every
resolved address in parallel using RFC 8305 Happy Eyeballs so v4-only
servers reachable via a v6-preferring resolver still connect within
~250 ms. Server-side resources (including reverse-tunnel listeners) are
released the moment the QUIC connection drops.
Authentication
Both the server and the client require an explicit TLS-mode flag — there is no silent insecure default. Three modes:
| Mode | Server | Client |
|---|---|---|
| Insecure | --insecure |
--insecure |
| Fingerprint pin | --tls-self-signed (or --tls-cert/--tls-key) |
--tls-fingerprint sha256:... |
| Full mTLS | --tls-cert ... --tls-key ... --tls-ca ... |
--tls-ca ... --tls-cert ... --tls-key ... [--tls-server-name ...] |
Quickest path for a private/single-user setup — the server logs its fingerprint at startup, the client pins it:
# server cert fingerprint: sha256:abcd...
For full mTLS, generate a CA + server + client cert (no openssl required):
rusnel cert --help lists the underlying subcommands (ca, server,
client, fingerprint) for finer control.
Embedded credentials (drop-and-run binaries)
Rusnel can bake credentials and the default invocation into the binary
at build time via RUSNEL_EMBED_* env vars. The resulting binary
connects, authenticates, and starts forwarding the moment it's run —
no flags, no subcommand, no config file. CLI flags still override
embedded values, so --help and ad-hoc subcommands keep working.
RUSNEL_EMBED_CA=./pki/ca.pem \
RUSNEL_EMBED_CLIENT_CERT=./pki/client.pem \
RUSNEL_EMBED_CLIENT_KEY=./pki/client.key \
RUSNEL_EMBED_SERVER_NAME=1.2.3.4 \
RUSNEL_EMBED_ARGS='client 1.2.3.4:8080 R:2222:localhost:22' \
Recognised vars: RUSNEL_EMBED_ARGS, RUSNEL_EMBED_SERVER_ADDR,
RUSNEL_EMBED_FINGERPRINT, RUSNEL_EMBED_SERVER_NAME,
RUSNEL_EMBED_CA, RUSNEL_EMBED_SERVER_CERT, RUSNEL_EMBED_SERVER_KEY,
RUSNEL_EMBED_CLIENT_CERT, RUSNEL_EMBED_CLIENT_KEY. See
build.rs for the full mapping.
Configuration file
Both rusnel server and rusnel client accept --config <PATH>
pointing at a TOML file. A single file may contain a [server]
section, a [client] section, or both — only the section matching
the subcommand is read. Unknown keys are rejected so typos surface
immediately.
Precedence: CLI flag > config file > built-in default. Any flag you pass on the command line overrides whatever the file says.
The TLS-mode flags are special: passing any of --insecure,
--tls-self-signed, --tls-cert, --tls-key, --tls-ca (server)
or --insecure, --tls-fingerprint, --tls-ca, --tls-cert,
--tls-key (client) on the CLI causes all of the file's TLS-mode
keys to be ignored — you get exactly the mode you typed, with no
silent mixing.
A minimal server example:
[]
= "0.0.0.0"
= 8080
= true
= true
= true
= "json"
A minimal client example:
[]
= "tunnel.example.com:8080"
= ["R:2222:localhost:22", "1.1.1.1:53/udp"]
= "sha256:0123456789abcdef..."
= 60
Both positional arguments (<server> and <remote>...) can be
supplied either by the file or on the CLI; the CLI version wins when
both are present.
A fully-annotated example covering every supported key lives at
examples/rusnel.toml.
Logging
Rusnel uses tracing end-to-end with structured
fields and stable span hierarchy.
Verbosity (mutually exclusive):
For finer control set RUST_LOG directly — it overrides the flags:
RUST_LOG=rusnel=debug,quinn=info
RUST_LOG=rusnel::common::tcp=trace
Format: human-readable compact (default) or one JSON object per line for log aggregators:
Logs go to stderr (so forward stdio: tunnels can use stdout cleanly).
Timestamps are ISO-8601 UTC with millisecond precision; ANSI colours are
auto-detected from the stderr TTY.
Every event is wrapped in spans whose field names match the
rusnel ctl / admin-API ID schema (client_id, tunnel_id,
conn_id, …), so logs grep cleanly against API output. Every conn
emits a conn opened event on entry and a conn closed event on exit
carrying bytes_in, bytes_out, and dur_ms.
Example session (compact format, ANSI stripped):
2026-05-05T11:03:55.022Z INFO client: connected client_id=1 peer=127.0.0.1:62178
2026-05-05T11:03:55.027Z INFO client: session established count=1
2026-05-05T11:03:55.027Z INFO client: tunnel registered tunnel_id=1 dir="forward" spec=19999=>127.0.0.1:9999/tcp
2026-05-05T11:03:55.670Z INFO conn: conn opened conn_id=1 tunnel_id=1 peer="127.0.0.1:9999"
2026-05-05T11:03:55.671Z INFO conn: conn closed bytes_in=12 bytes_out=17 dur_ms=1
Server admin API & rusnel ctl
The server exposes a read-only admin HTTP API on a unix domain socket
by default at ~/.rusnel/admin.sock (mode 0600). Filesystem
permissions are the only auth — only the user that started the server
can connect. Pass --admin-socket <path> to override the path or
--no-admin-socket to disable.
Terminology: client, tunnel, conn
State is modelled in three layers, each with one meaning:
- A client is one connected
rusnel clientdaemon. Lives for the lifetime of the QUIC connection. - A tunnel is the remote declaration a client established with
the server (
R:5000=>socks,1080=>1.1.1.1:53/udp, …). Deduplicated per client by spec; lives as long as the client. - A conn is a single proxied network connection going through a tunnel — one accepted TCP connection, one per-source UDP flow, one SOCKS5 CONNECT, one SOCKS5 UDP target.
Query the API with rusnel ctl (or curl --unix-socket):
ctl defaults to the same socket path the server uses, so the
zero-flag pairing just works.
The available endpoints (GET only in this release):
| Path | Purpose |
|---|---|
/api/v1/server |
version, listen addr, uptime, client/tunnel/conn counts |
/api/v1/clients |
one row per connected client with rolled-up totals |
/api/v1/clients/:id |
client detail, with embedded tunnel summaries |
/api/v1/clients/:id/tunnels |
tunnels owned by one client |
/api/v1/clients/:id/conns |
active conns across all of one client's tunnels |
/api/v1/tunnels |
every tunnel across every client |
/api/v1/tunnels/:id |
tunnel detail with its active conns embedded |
/api/v1/tunnels/:id/conns |
just the active conns on one tunnel |
/api/v1/conns |
every active conn globally |
/api/v1/conns/:id |
one conn by id |
/api/v1/history?limit=N |
bounded ring buffer (256) of recent disconnects |
Write operations (kick client, kill conn), Prometheus /metrics, and
an embedded web UI are tracked as phase-2 follow-ups in
ROADMAP.md.
Performance
Rusnel (QUIC) vs Chisel (SSH-over-WebSocket) on loopback. Throughput is iperf3 over a tunneled TCP forward (100 MB × 5 runs + warmup, median); latency is the round-trip time of a 64 B echo across the tunnel.

End-to-end HTTP request times through the tunnel across payload sizes (median of 5 runs, error bars show min/max):

The benchmark harness also includes a wan profile that applies
tc qdisc netem delay 25ms to the loopback interface to approximate a
50 ms-RTT WAN. Reproduce everything with ./benchmark/run.sh
(requires Docker; needs --cap-add=NET_ADMIN for netem profiles, which
the script adds for you). See benchmark/ for tunables.
Roadmap
Planned protocol features (HTTP/3 facade, 0-RTT resumption, NAT
hole-punching), access-control work (server-side ACLs, OIDC client
auth), and admin-API phase 2 (kick / kill / Prometheus / web UI) are
tracked in ROADMAP.md. Contributions welcome.
License
Licensed under the Apache License 2.0.