tunnix

June 6, 2026 · View on GitHub

An encrypted SOCKS5/HTTP proxy tunnel over HTTP/SSE.

tunnix routes your SOCKS5 and HTTP(S) proxy traffic through a plain HTTP connection, end-to-end encrypted with ChaCha20-Poly1305. It is designed for environments that serve HTTP but block direct TCP — Cloud Shell, Codespaces, Gitpod, or any host behind a reverse proxy.

crates.io License: MIT Ask DeepWiki

Features

  • End-to-end encryption — ChaCha20-Poly1305, Argon2id key derivation
  • HTTP/SSE transport — no WebSocket required; works wherever plain HTTP works
  • Dual-protocol listener — SOCKS5 and HTTP proxy on the same port, auto-detected
  • Connection multiplexing — many connections share one SSE stream
  • Custom header injection — for cookie-authenticated reverse proxies
  • Path prefix support — serve under a sub-path (/foo/bar/stream/...) to coexist with other apps on the same host
  • Remote exec (opt-in)tunnix remote-exec runs a command or interactive shell on the server over the tunnel; off by default, gated behind --allow-exec (Unix only)
  • Single binarytunnix server / tunnix client

Installation

Homebrew (macOS / Linux):

brew install aeroxy/tap/tunnix

Cargo:

cargo install tunnix

Or download a pre-built binary from the releases page.

Quick Start

# Server
tunnix server --listen 0.0.0.0:8080 --password "your-secret"

# Client
tunnix client \
  --server https://your-host.example.com \
  --password "your-secret" \
  --local-addr 127.0.0.1:7890

# Test
curl -x http://127.0.0.1:7890 https://ifconfig.me
curl --socks5 127.0.0.1:7890 https://ifconfig.me

Remote Exec (opt-in)

tunnix remote-exec runs a command — or an interactive shell — on the server, over the same encrypted tunnel. It is disabled by default and Unix-only. The server must be started with --allow-exec (or allow_exec = true in config) to authorize it.

# Server — must explicitly opt in
tunnix server --listen 0.0.0.0:8080 --password "your-secret" --allow-exec

# Client — interactive shell
tunnix remote-exec --server https://your-host.example.com --password "your-secret"

# Client — one-off command
tunnix remote-exec --server https://your-host.example.com --password "your-secret" -- ls -la /var/log

It allocates a PTY on the server, so interactive programs (vim, top, bash) work and your terminal size (SIGWINCH) is forwarded. The PTY runs in canonical mode — do not pipe binary data through it (line-buffering injects a trailing newline on EOF and drops control bytes); tunnel raw TCP for byte-exact transfers instead.

⚠️ --allow-exec grants remote code execution. Anyone holding the server password gets a shell on the host. The server prints a loud warning at startup when it's enabled. Only turn it on when you understand and accept that.

File Transfer (opt-in)

tunnix push and tunnix pull upload and download files or directories over the same encrypted tunnel. The stream is packed into a tar archive and zstd-compressed before it's encrypted by the transport (compress-then-encrypt). Transfers are disabled by default; the server must be started with --allow-transfer (or allow_transfer = true in config) to authorize them.

# Server — must explicitly opt in
tunnix server --listen 0.0.0.0:8080 --password "your-secret" --allow-transfer

# Upload a local file or directory to a destination directory on the server
tunnix push --server https://your-host.example.com --password "your-secret" ./localdir /remote/dir

# Download a remote file or directory into a local destination directory
tunnix pull --server https://your-host.example.com --password "your-secret" /remote/dir ./localdir

# Multiple sources in one transfer — last arg is the destination directory (like `cp`)
tunnix push -s https://your-host.example.com -p "your-secret" a.txt b.txt ./somedir /remote/dir
tunnix pull -s https://your-host.example.com -p "your-secret" /remote/a /remote/b ./localdir

# Tune compression (zstd level 1-22; default 3)
tunnix push -s https://your-host.example.com -p "your-secret" --level 19 ./bigdir /remote/dir

Directories transfer recursively with permissions preserved (like scp -r). You can pass several sources in one command — the last argument is always the destination directory, the rest are sources. Each source's basename becomes its archive root, so push ./foo /remote lands as /remote/foo/... (sources sharing a basename collide — the later one wins).

⚠️ --allow-transfer grants arbitrary file read/write. Anyone holding the server password can read or overwrite files on the host. The server prints a loud warning at startup when it's enabled.

Deployment Scenarios

Google Cloud Shell

Cloud Shell's Web Preview issues a temporary HTTPS URL for your HTTP server. tunnix runs inside Cloud Shell and the client connects using the preview URL with Cloud Shell's authorization cookies.

Server (inside Cloud Shell terminal):

tunnix server --listen 0.0.0.0:8080 --password "your-secret"

Open Web Preview on port 8080 to get the preview URL.

Get cookies: Browser DevTools → Network tab → any request to *.cloudshell.dev → copy the Cookie header.

Client (local machine):

tunnix client \
  --server "https://8080-cs-XXXX.cs-region.cloudshell.dev" \
  --password "your-secret" \
  --cookie "CloudShellAuthorization=Bearer ...; CloudShellPartitionedAuthorization=Bearer ..."

GitHub Codespaces

Codespaces exposes forwarded ports via a GitHub-authenticated HTTPS URL.

Server (inside Codespace terminal):

tunnix server --listen 0.0.0.0:8080 --password "your-secret"

In the Ports panel, set port 8080 visibility to Public (or pass a GitHub token).

Client:

tunnix client \
  --server "https://your-codespace-name-8080.app.github.dev" \
  --password "your-secret"

Gitpod

Same pattern as Codespaces. Make the port public in the Gitpod ports UI.

Client:

tunnix client \
  --server "https://8080-your-workspace.ws-eu.gitpod.io" \
  --password "your-secret"

Behind nginx (path prefix)

When tunnix shares a host with other services, use path_prefix to scope all its routes under a sub-path. nginx handles TLS; tunnix binds to a local port.

config.toml (server):

[server]
listen = "127.0.0.1:9000"
password = "your-secret"
path_prefix = "/tunnix"

nginx snippet:

location /tunnix/ {
    proxy_pass http://127.0.0.1:9000;
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection "";
    proxy_set_header X-Accel-Buffering "no";
    proxy_read_timeout 3600s;
}

Client:

tunnix client --server "https://your-domain.com/tunnix" --password "your-secret"

The bare /health endpoint always responds regardless of prefix, so load-balancer probes continue to work.

Railway / Render / Fly.io

These platforms run long-lived processes and assign a public HTTPS URL. They set a $PORT environment variable.

Dockerfile (minimal):

FROM debian:bookworm-slim
COPY tunnix /usr/local/bin/tunnix
CMD tunnix server --listen "0.0.0.0:$PORT" --password "$TUNNIX_PASSWORD"

Set TUNNIX_PASSWORD as an environment secret in the platform dashboard.

Client:

tunnix client \
  --server "https://your-app.railway.app" \
  --password "your-secret"

Vercel is not recommended. Serverless functions have short execution timeouts (10–60 s depending on plan) that are incompatible with long-lived SSE streams. Use a container-based platform instead.

Configuration

Copy config.example.toml to config.toml and customize:

[server]
listen = "0.0.0.0:8080"
password = "your-secret"
# path_prefix = "/tunnix"   # optional; leave empty for root
# allow_exec = false        # opt-in remote shell (RCE) for `tunnix remote-exec`; Unix only
# allow_transfer = false    # opt-in file read/write for `tunnix push` / `tunnix pull`

[client]
server_url = "https://your-host.example.com"
password = "your-secret"
local_addr = "127.0.0.1:7890"

[client.headers]
# Cookie = "..."   # only needed for cookie-authenticated hosts

[logging]
level = "info"
# file = "./tunnix.log"

Run with a config file:

tunnix server --config config.toml
tunnix client --config config.toml

Config file resolution

When --config/-f is not given, tunnix looks for a config file in this order and uses the first that exists:

  1. --config <path> / -f <path> — explicit path (must parse, or tunnix errors)
  2. ./config.toml — in the current working directory
  3. ~/.config/tunnix/config.toml — global per-user default (honors $XDG_CONFIG_HOME; same path on macOS and Linux)

This applies to every subcommand, including push / pull and remote-exec — so you can keep your server_url and password in ~/.config/tunnix/config.toml and run tunnix push ./dir /remote/dir from anywhere without flags. If none of the three exist, tunnix falls back to built-in defaults.

CLI flags always override config file values. The password can also be supplied via the TUNNIX_PASSWORD environment variable.

Changes to config.toml are picked up automatically every few seconds — no restart needed. Hot-reloadable fields: password, headers, path_prefix, root_redirect, root_html, health_response. Fields that require a restart: listen, local_addr, server_url, logging.level. CLI overrides are never clobbered by file changes.

Building

cargo build --release
# Binary: target/release/tunnix

Cross-compile for Linux (requires cargo-zigbuild):

cargo zigbuild --release --target x86_64-unknown-linux-gnu

Or use make:

make release          # native
make release-linux    # Linux x86_64
make release-all      # both

Architecture

Local SOCKS5/HTTP client


  tunnix client
  ├── proxy.rs       — TCP listener; detects protocol (0x05=SOCKS5, letter=HTTP)
  ├── socks5.rs      — SOCKS5 handshake (RFC 1928, CONNECT only)
  ├── http_proxy.rs  — HTTP CONNECT + plain HTTP forwarding
  ├── relay.rs       — bidirectional relay; connection ID counter
  ├── exec.rs        — remote-exec client: raw terminal + PTY stream (Unix)
  └── tunnel.rs      — HTTP/SSE tunnel to server

          │  POST /[prefix]/send/{session}    encrypted binary body
          │  GET  /[prefix]/stream/{session}  SSE text/event-stream

  tunnix server
  └── server.rs      — hyper HTTP/1.1 server; session routing; prefix stripping

          │  raw TCP

  Target (e.g. api.example.com:443)

The client auto-detects the incoming protocol by peeking the first byte:

  • 0x05 → SOCKS5
  • ASCII letter → HTTP proxy (CONNECT for HTTPS, method for plain HTTP)

Security

  • Argon2id key derivation from the shared password
  • ChaCha20-Poly1305 AEAD, per-message random nonce
  • No plaintext payload logging
  • Use a strong, randomly generated password — it is the only credential
  • Remote exec is off by default. --allow-exec (server) grants a shell to anyone with the password — effectively full RCE on the host. Leave it disabled unless you explicitly need it.

Use with Clash / ClashX

proxies:
  - name: tunnix
    type: socks5      # or type: http
    server: 127.0.0.1
    port: 7890

License

MIT