A lightweight reverse tunnel, written in Rust, for exposing a local service to the internet. It uses QUIC for the control/data channel between client and server, and a raw TCP + httparse gateway on the server side that routes incoming HTTP traffic by Host header — either to an active QUIC tunnel (dynamic) or to a fixed backend address (static).
Status: early / experimental. The core tunneling path works, but hardening (TLS verification, rate limiting, input limits) is still in progress — see Security Notice below before exposing this to untrusted traffic.
There are two binaries:
woho-server— runs on your VPS / public server. It listens on two ports:- a QUIC port, where
wohoclients authenticate and register a subdomain; - an HTTP port, where public traffic arrives and gets routed by
Hostheader.
- a QUIC port, where
woho— runs next to the local app you want to expose (e.g. a dev server onlocalhost:3000). It authenticates towoho-serverover QUIC and keeps the connection open so the server can push incoming requests through it.
flowchart LR
Internet -->|HTTP request, Host: dev.example.com| Gateway[woho-server: HTTP Gateway]
Gateway -->|subdomain registered?| Registry[(Session Registry)]
Registry -->|yes: dynamic route| QUICConn[QUIC Connection]
Registry -->|no: check static_routes| Static[Static upstream, e.g. 127.0.0.1:9000]
QUICConn <-->|bidirectional stream| Client[woho client]
Client <--> LocalApp[Your local app, e.g. 127.0.0.1:3000]
Dynamic routes are created on the fly: when a woho client authenticates successfully, the server registers its Connection under the assigned subdomain. Every subsequent HTTP request whose Host matches that subdomain opens a new bidirectional QUIC stream to that client, which the client forwards to your local port.
Static routes are fixed mappings defined in the server config (subdomain -> upstream), useful for backends that live permanently on the same machine as woho-server and don't need a tunnel at all.
cargo build --release
# binaries end up in target/release/woho and target/release/woho-serverGenerate (or bring your own) TLS certificate for the QUIC listener:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout key.pem -out cert.pem -days 365 -subj "/CN=localhost"Create woho-server.toml:
[server]
listen_quic = "0.0.0.0:4433"
listen_http = "0.0.0.0:8080"
domain_suffix = "yourdomain.com"
[tls_config]
cert_path = "cert.pem"
key_path = "key.pem"
# Fixed mappings — always available, no client/tunnel required
[[static_routes]]
subdomain = "status"
upstream = "127.0.0.1:9090"
[[static_routes]]
subdomain = "internal-api"
upstream = "127.0.0.1:9000"
# Each user can only claim subdomains listed in allowed_subdomains
[[users]]
username = "alice"
password = "change-me"
allowed_subdomains = ["staging", "preview"]
[[users]]
username = "bob"
password = "change-me-too"
allowed_subdomains = ["bob-sandbox"]Run it:
./woho-server --config <CONFIG_FIEL>woho --address <SERVER_IP>:<PORT> --export-port <LOCAL_PORT> --username <USERNAME> --password <PASSWORD>Your local app on 127.0.0.1:3000 is now reachable at http://dev.example.com (routed through the server's HTTP gateway on port 8080).
Wormhole Tunnel Client
Usage: woho [OPTIONS] --address <ADDRESS> --export-port <EXPORT_PORT> --username <USERNAME> --password <PASSWORD>
Options:
-a, --address <ADDRESS> Address of the woho-server (host:port)
-e, --export-port <EXPORT_PORT> Local port to expose to the internet
-u, --username <USERNAME> Username configured on the server
-p, --password <PASSWORD> Password configured on the server
-s, --subdomain <SUBDOMAIN> Requested subdomain (must be in your allowed list)
-f, --force Force-close an existing session on the same
subdomain and take it over
-h, --help Print help
Note:
--addresscurrently expects anip:portpair, not a hostname — resolve DNS yourself before passing it in.
| Section | Field | Description |
|---|---|---|
[server] |
listen_quic |
Address the QUIC control/data endpoint binds to |
[server] |
listen_http |
Address the HTTP gateway binds to |
[server] |
domain_suffix |
Suffix appended to a user's subdomain to form the public domain |
[tls_config] |
cert_path / key_path |
TLS certificate and key used for the QUIC endpoint |
[[static_routes]] |
subdomain / upstream |
Fixed subdomain → local address mapping, bypassing QUIC entirely |
[[users]] |
username / password / allowed_subdomains |
Credentials and which subdomain(s) each user may request |
This project has not yet been hardened for production use. In particular, as of now:
- The client does not verify the server's TLS certificate (certificate verification is disabled). Anyone able to intercept traffic between client and server can read or tamper with tunneled data.
- The HTTP gateway has no request size limit or read timeout on the initial header parse, and does not validate that the
Hostheader actually belongs to the configureddomain_suffix. - There is no rate limiting on connections or requests.
- Passwords are stored in plaintext in the server config.
Until these are addressed, run woho-server's HTTP gateway behind a reverse proxy (e.g. Caddy, Nginx, or Traefik) that terminates public TLS, enforces timeouts/body size limits, and ideally does its own authentication or IP allow-listing in front of it. Don't expose the raw HTTP gateway directly to untrusted traffic yet.
- No automatic reconnect on the client if the QUIC connection drops
- No graceful shutdown (open tunnels are dropped abruptly on server restart)
--addressrequires a resolved IP, not a hostname- No metrics/observability for active tunnels or throughput
- Passwords aren't hashed
Add a license (e.g. MIT or Apache-2.0) before publishing, if you haven't already.