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.
- 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-execruns a command or interactive shell on the server over the tunnel; off by default, gated behind--allow-exec(Unix only) - Single binary —
tunnix server/tunnix client
Homebrew (macOS / Linux):
brew install aeroxy/tap/tunnixCargo:
cargo install tunnixOr download a pre-built binary from the releases page.
# 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.metunnix 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/logIt 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-execgrants 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.
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/dirDirectories 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-transfergrants 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.
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 ..."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"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"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
/healthendpoint always responds regardless of prefix, so load-balancer probes continue to work.
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.
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.tomlWhen --config/-f is not given, tunnix looks for a config file in this order and uses the first that exists:
--config <path>/-f <path>— explicit path (must parse, or tunnix errors)./config.toml— in the current working directory~/.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.
cargo build --release
# Binary: target/release/tunnixCross-compile for Linux (requires cargo-zigbuild):
cargo zigbuild --release --target x86_64-unknown-linux-gnuOr use make:
make release # native
make release-linux # Linux x86_64
make release-all # bothLocal 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 (
CONNECTfor HTTPS, method for plain HTTP)
- 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.
proxies:
- name: tunnix
type: socks5 # or type: http
server: 127.0.0.1
port: 7890MIT