Skip to content

Latest commit

 

History

248 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wormhole 🕳️

Tests

A simple HTTP tunneling tool that exposes your local development server to the internet through custom subdomains.

Wormhole CLI

What it does

Wormhole creates a tunnel between your local development server and the internet, allowing you to:

  • Share your local web application with others instantly
  • Test webhooks and APIs that require public URLs
  • Demo your work-in-progress applications
  • Access your local development environment from anywhere

Quick Start

1. Start the server

go run cmd/server/main.go

2. Connect your local app

go run cmd/client/main.go -server=http://localhost:8080 -domain=myapp -local=http://localhost:3000

3. Access your app

Your local server is now available at: http://myapp.localhost:8080

Installation

Download Pre-built Binaries

Download the latest release for your platform from the Releases page.

Linux:

tar -xzf wormhole-client-linux-amd64.tar.gz
chmod +x wormhole
./wormhole -domain=myapp -local=http://localhost:3000

macOS:

The binaries are not signed with an Apple Developer certificate. After downloading, you need to remove the quarantine attribute:

tar -xzf wormhole-client-darwin-arm64.tar.gz
xattr -d com.apple.quarantine wormhole
chmod +x wormhole
./wormhole -domain=myapp -local=http://localhost:3000

Alternatively, you can right-click the binary in Finder, select "Open", and confirm in the dialog.

Build from Source

git clone https://github.com/dbackowski/wormhole.git
cd wormhole
go mod download
make build

This produces bin/client/wormhole and bin/server/wormhole.

To run the test suite:

make test

Usage

Server Options

go run cmd/server/main.go -port=8080

Optional flags:

  • -port: Port to run the server on (default: 8080)
  • -host: Public hostname of this server (e.g. wormhole.tools). When set, /health and /metrics are only served on this host; requests to subdomains are tunneled to the matching client. Required for any deployment on a real domain — see the note below.
  • -auth-token: Authentication token required for client connections
  • -debug: Enable debug mode
  • -version: Print version and exit

Note on -host: When -host is unset, the server falls back to a heuristic — any Host containing a dot is treated as a tunnel subdomain. This works for local development (*.localhost) and bare-IP access, but on a real domain it has two consequences: requests to your apex (e.g. wormhole.tools/health) are treated as a tunnel and return 502 instead of serving health/metrics, and the apex's first label (wormhole) becomes a reserved subdomain that shadows any client claiming it. Set -host to your public hostname in production to route exactly.

Client Options

go run cmd/client/main.go -server=http://localhost:8080 -domain=mysubdomain -local=http://localhost:3000

Required flags:

  • -domain: Your unique subdomain name
  • -local: URL of your local development server

Optional flags:

  • -server: Server URL (default: https://wormhole.tools)
  • -auth-token: Authentication token for server connection
  • -webui-port: Port for the Web UI dashboard (default: 4040)
  • -version: Print version and exit

Note: Unlike the server, the client does not read environment variables. It takes its configuration from flags or from ~/.wormhole/config (see Authentication).

Features

✅ Custom Subdomains - Choose your own subdomain name
✅ Real-time Tunneling - Instant request forwarding via WebSockets
✅ Header Forwarding - End-to-end headers are forwarded; hop-by-hop headers are stripped, Host is set to the tunnel host, and X-Forwarded-For/-Proto/-Host are added so your local app sees the original client IP and public URL (standard proxy behavior)
✅ Multiple Clients - Support for multiple simultaneous tunnels
✅ Automatic Cleanup - Domains are released when clients disconnect
✅ Automatic Reconnect - The client retries with exponential backoff if the connection drops
✅ Web UI Dashboard - Monitor tunneled requests in a browser at http://localhost:4040
✅ Optional Authentication - Secure your server with token-based auth

Example Use Cases

  • Frontend Development: Share your React/Vue/Angular app with team members
  • API Testing: Test webhook endpoints from external services
  • Mobile Development: Test your local API with mobile apps
  • Client Demos: Show work-in-progress to clients without deployment

Web UI

When the client is running, a dashboard is available at http://localhost:4040 where you can:

  • See the active tunnel URL
  • View recent requests with method, path, status code, and timestamp
  • Inspect request/response headers and bodies
  • Clear the request history with the Clear button (also clears the terminal view)

Dashboard with request list

Request/response inspector

To use a different port:

go run cmd/client/main.go -domain=myapp -local=http://localhost:3000 -webui-port=5050

Authentication

You can secure your Wormhole server by requiring an authentication token for client connections.

Using flags

Server:

./wormhole-server -port=8080 -auth-token=my-secret-token

Client:

./wormhole -domain=myapp -local=http://localhost:3000 -auth-token=my-secret-token

Using a config file

Both the server and client can read the token from ~/.wormhole/config:

AUTH_TOKEN=my-secret-token

The -auth-token flag takes precedence over the config file. When a token is configured on the server, clients must provide the same token to connect — unauthenticated connections will be rejected with HTTP 401.

How it works

graph TD
    A[Browser] -->|HTTP Request| B[Wormhole Server<br>myapp.server:8080]
    B -->|WebSocket| C[Wormhole Client]
    C -->|HTTP| D[Local Dev Server<br>localhost:3000]
    D -->|Response| C
    C -->|WebSocket| B
    B -->|HTTP Response| A
Loading
  1. The server listens for client connections and HTTP requests
  2. Clients connect via WebSocket and claim a subdomain
  3. HTTP requests to subdomain.server:port are forwarded to the client over WebSocket
  4. The client forwards requests to your local server and returns responses

Server Endpoints

Endpoint Description
/health Health check, returns OK. Served on the server's own host only
/metrics Active connection count. Served on the server's own host only
/* Everything else is tunneled to the matching subdomain client

Client connections are not a path. The server recognizes them as WebSocket upgrades carrying the X-Wormhole-Client header, at whatever path the client dialed, so no path is reserved on your tunnel.

Status codes

Statuses the server returns for tunneled requests, rather than passing through from your local app:

Status Meaning
400 Bad Request The Host header has no subdomain to route on
413 Request Entity Too Large Request body exceeds 10 MB
501 Not Implemented WebSocket upgrade request (see Limitations)
502 Bad Gateway No client is connected for the subdomain, the tunnel dropped mid-request, or the local server was unreachable, too slow, or returned a body over 10 MB
503 Service Unavailable The client is already handling 64 concurrent requests
504 Gateway Timeout No response from the client within 15 seconds

Self-Hosting

To run your own Wormhole server, you need:

  1. Wildcard DNS - Point *.yourdomain.com to your server so that subdomain-based routing works
  2. TLS termination - Wormhole does not handle TLS natively. Use a reverse proxy like Caddy or nginx to terminate TLS
  3. Run the server with -host set to your public hostname so apex requests (health/metrics) are routed correctly:
    ./wormhole-server -port=8080 -host=yourdomain.com

Forwarded headers behind TLS termination: The server adds X-Forwarded-Proto to each tunneled request. Because TLS is terminated by your reverse proxy, an inbound X-Forwarded-Proto (set by Caddy/nginx) is always honored; when absent, the value defaults to https if -host is set and http otherwise. X-Forwarded-For appends the incoming peer to any existing chain, so configure your proxy to forward the real client IP.

Docker

docker build -t wormhole .
docker run -p 8080:8080 -e HOST=yourdomain.com -e AUTH_TOKEN=my-secret-token wormhole

The server reads AUTH_TOKEN and HOST from the environment (equivalent to the -auth-token and -host flags), so secrets stay out of the process command line. Flags, when provided, take precedence over the environment.

Fly.io

The repository includes a fly.toml for deployment to Fly.io. Set your FLY_API_TOKEN as a GitHub secret for automatic deploys on push to main.

Limitations

  • No WebSocket passthrough - WebSocket upgrade requests to tunneled services are rejected with 501 Not Implemented
  • No built-in TLS - Requires a reverse proxy for HTTPS
  • 10 MB request body limit - Requests larger than 10 MB are rejected with 413 Request Entity Too Large
  • 10 MB response body limit - Responses larger than 10 MB from your local server are rejected with 502 Bad Gateway. Bodies travel base64-encoded inside a WebSocket frame capped at 16 MB, which is what sets both limits
  • 10 second request timeout - The client gives up on your local server after 10 seconds and returns 502 Bad Gateway. The server independently stops waiting after 15 seconds and returns 504 Gateway Timeout
  • 64 concurrent requests per tunnel - Beyond that the client returns 503 Service Unavailable until a slot frees up
  • Redirects are passed through unchanged - The client does not follow redirects from your local server. A Location header pointing at http://localhost:3000 is sent to the browser as-is, taking it off the tunnel. Configure your app to emit relative redirects, or to build absolute URLs from the X-Forwarded-Host and X-Forwarded-Proto headers
  • Reconnect is time-limited - If the connection drops, the client retries with exponential backoff (500 ms up to 30 s) for 5 minutes, then exits. Requests in flight when the connection drops fail with 502 Bad Gateway
  • Reconnect can be delayed after an unclean drop - If the connection dies without closing cleanly (laptop sleep, Wi-Fi drop), the server only notices when its heartbeat times out, up to 60 seconds later. Until then the subdomain is still held by the stale connection and reconnect attempts fail as already taken. The client keeps retrying, so it recovers on its own

Requirements

  • Go 1.25 or later
  • A client and server from the same release line. Clients up to v1.0.5 identified themselves by connecting to /ws; the server now requires the X-Wormhole-Client header, so those clients cannot register and fail with websocket: bad handshake. Upgrade the client
  • Available port for the server (default: 8080)
  • Local development server to tunnel

License

Released under the MIT License.

Contributing

Feel free to open issues and submit pull requests to improve Wormhole!

About

This is a simple HTTP tunnelling tool written in Go that is similar to ngrok.io.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages