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.
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-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
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-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.
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-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.
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
/healthendpoint 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:
--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.
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 (
CONNECTfor 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