@explita/tunnel
v0.2.1
Published
Secure reverse tunneling CLI & library with application-layer encryption (ECDH P-256 + AES-256-GCM) over WebSockets
Maintainers
Readme
@explita/tunnel
Client CLI and library for Explita Tunnel to securely expose local HTTP and WebSocket servers to the public internet with encrypted application-layer tunnel transport using ephemeral ECDH P-256 key exchange and AES-256-GCM, carried over TLS-secured WebSockets.
🚀 Installation
Global CLI
npm install -g @explita/tunnel
# or
pnpm add -g @explita/tunnelRun via npx / pnpm dlx
npx @explita/tunnel 3000💻 CLI Usage
Basic Usage
Expose a local server running on port 3000:
eta 3000Request a custom subdomain:
eta 3000 --subdomain my-app
# or
eta 3000 -s my-app[!NOTE] Subdomains are first-come, first-served. Custom subdomains are not permanently owned or reserved. While your tunnel session is active, the subdomain is locked exclusively to your connection. Once you disconnect, the subdomain is released immediately and can be claimed by anyone.
Connect with an explicit token (or use auto-saved token):
eta 3000 -s my-app -t my-secret-tokenConnect to a custom tunnel server:
eta 3000 -S wss://tunnel.mycompany.com/_connect🔒 HTTP Basic Authentication (-a, --auth)
Protect your public tunnel with username and password credentials. When enabled, incoming HTTP requests without valid credentials receive 401 Unauthorized with a WWW-Authenticate: Basic challenge:
eta 3000 -a "alice:secret123"
# or
eta 3000 --auth "alice:secret123"🛡️ IP / CIDR Allowlisting (-i, --allow-ip)
Restrict tunnel access exclusively to specific developer IP addresses or third-party webhook gateways (e.g. Stripe, Shopify, GitHub). All other requests are rejected with 403 Forbidden:
# Allow only your office public IP
eta 3000 -i 203.0.113.50
# Allow a local or VPN subnet
eta 3000 -i 10.0.0.0/8
# Allow multiple IPs and webhook CIDRs (e.g. Stripe webhook ranges)
eta 3000 -i "54.187.174.169/32, 54.187.205.235/32, 192.168.1.0/24"You can also specify allowIp directly in .etarc:
{
"port": 3000,
"subdomain": "my-webhook",
"allowIp": [
"54.187.174.169/32",
"10.0.0.0/8"
]
}Authentication & Tokens
Request and save an authentication token directly from your terminal:
# 1. Interactive: requests a 6-digit OTP and prompts directly in terminal
eta token -e [email protected]
# 2. Direct: verify with OTP code via flag
eta token -e [email protected] -v 123456Once verified, your token is securely stored in ~/.explita/eta/config.json. From then on, running eta <port> is authenticated automatically!
eta token # View currently saved token and email
eta token --clear # Clear saved token📁 Project Configuration (.etarc & eta init)
Instead of specifying flags on every run, configure project-level defaults in an .etarc file in your repository:
# Initialize with a port
eta init 3000
# Or include preferred subdomain and basic authentication
eta init 3000 -s my-app -a "alice:secret123"This creates an .etarc file in your project root:
{
"port": 3000,
"subdomain": "my-app",
"auth": "alice:secret123"
}Once .etarc exists, simply run eta with zero arguments!
etaConfiguration Precedence
Explita Tunnel resolves settings in the following order:
- Command-line flags (
--port,--subdomain,--auth, etc.) - Project config (
.etarcin the current working directory) - Machine config (
~/.explita/eta/config.json) - Built-in defaults
[!NOTE] Project-specific settings like
port,subdomain, andauthare strictly repository-level (.etarc) and are never written to your global machine config to prevent cross-project collisions.
⚙️ Configuration Management (eta config)
View and manage tunnel settings:
eta config list # Display resolved project and global configuration
eta config path # Show file paths for project and global configs
eta config get port # Read a specific setting
eta config set port 8080 # Save setting to project .etarc (or --global)📋 Complete CLI Flags & Commands Reference
Commands
| Command | Description | Example |
| :-------------------- | :------------------------------------------------------------------- | :---------------------------- |
| eta <port> | Start tunnel exposing local port | eta 3000 |
| eta | Start tunnel using local .etarc configuration | eta |
| eta init [port] | Initialize or update project .etarc file | eta init 3000 -s my-app |
| eta config [cmd] | Manage local and global configuration (list, get, set, path) | eta config list |
| eta token [options] | Request and verify 6-digit email OTP developer token | eta token -e [email protected] |
Options & Flags
| Flag | Long Option | Type | Default | Description |
| :--- | :----------------------- | :----- | :------ | :-------------------------------------------------------------------------------- |
| -p | --port <num> | Number | None | Local TCP port of your running web server or API (required unless in .etarc) |
| -s | --subdomain <name> | String | Random | Request custom subdomain (3 to 32 characters, lowercase alphanumeric and hyphens) |
| -a | --auth <user:pass> | String | None | Protect tunnel with HTTP Basic Authentication |
| -i | --allow-ip <cidr> | String | None | Restrict tunnel access to specific IP addresses or CIDR ranges |
| -t | --token <token> | String | Saved | Authentication token override (or via TUNNEL_TOKEN environment variable) |
| -S | --server <url> | String | Gateway | Custom WebSocket tunnel gateway server URL |
| -R | --no-reconnect | Flag | false | Do not automatically reconnect if the connection drops |
| -E | --no-encrypt | Flag | false | Disable inner application-layer encryption (debug mode) |
| | --inspect [port] | Number | 39755 | Enable local request inspector web UI (optional port override, e.g. --inspect 5000)|
| | --no-inspect | Flag | false | Disable local request inspector web dashboard (runs in quiet/headless mode) |
| -h | --help | Flag | - | Show help message and exit |
Interactive Terminal Hotkeys
While an active tunnel session is running in your terminal, the following keyboard shortcuts are available:
| Key | Action | Description | | :----------: | :--------- | :--------------------------------------------------------------------------------- | | r | Replay | Replays the most recently received HTTP request directly against your local server | | c | Clear | Clears the terminal output buffer without interrupting the active tunnel | | h | Help | Displays interactive keyboard shortcut help in the terminal |
🔍 In-Memory Request Inspector & Webhook Replay
Every active tunnel session comes with a zero-dependency, local web inspector running on http://127.0.0.1:39755 (auto-increments if the port is busy):
- Real-Time Traffic: Live streaming of incoming HTTP requests and WebSocket frames via Server-Sent Events (SSE).
- Deep Inspection: Inspect headers, query params, parsed JSON bodies, and raw request/response payloads.
- Image Previews: Automatic transparency checkerboard previews for
Content-Type: image/*(PNG, SVG, JPEG, WebP, GIF, AVIF, ICO, BMP). - One-Click Replay: Instantly resend any recorded webhook or API request directly to your local server without re-triggering external services.
- Interactive Terminal Hotkeys:
- Press
rin the terminal to replay the latest request. - Press
cto clear the terminal screen. - Press
hto toggle keyboard shortcuts.
- Press
[!IMPORTANT] Strictly Local & Zero-Logging: The inspector dashboard and its request buffer run 100% locally in memory on
http://127.0.0.1:39755. Requests, headers, and payloads are captured directly by your local CLI agent and never pass through or get stored on the remote tunnel server. Nothing is written to disk or sent to external telemetry.
To disable the web inspector UI, use --no-inspect (or set "inspect": false in .etarc). To customize the port, use --inspect [port] (e.g. eta 3000 --inspect 5000 or "inspect": 5000 in .etarc).
🌐 Multi-Tenant & Nested Subdomains (-- separator)
Testing multi-tenant SaaS applications, tenant subdomains, or preview environments locally is seamless with Explita Tunnel.
When you register a base tunnel subdomain:
eta 3000 -s myapp
# Public URL: https://myapp.tunnel.explita.ngAny request prefixed with a double hyphen (--) automatically routes to your active tunnel session:
https://tenant1--myapp.tunnel.explita.ng➔ routes to your local serverhttps://acme--myapp.tunnel.explita.ng➔ routes to your local serverhttps://docs--acme--myapp.tunnel.explita.ng➔ routes to your local server
Why -- instead of dots?
Public wildcard TLS certificates (*.tunnel.explita.ng) strictly cover one dot level under RFC 6125 standards. The double-hyphen convention allows any number of nested tenant levels to be fully covered by the standard wildcard certificate without requiring separate On-Demand TLS certs, while preserving normal hyphenated names like my-app or acme-corp.
Injected Context Headers
Explita Tunnel automatically attaches routing context to forwarded requests and WebSockets so your middleware or application logic can easily inspect the calling tenant:
| Header | Example Value | Description |
| :--------------------------- | :--------------------------------- | :------------------------------- |
| x-explita-subdomain | tenant1--myapp | The full requested subdomain |
| x-explita-parent-subdomain | myapp | Your registered tunnel subdomain |
| x-explita-subdomain-prefix | tenant1 | The nested tenant/prefix |
| x-explita-tenant | tenant1 | Convenience alias for the tenant |
| x-explita-subdomain-parts | tenant1 (or docs,acme) | Comma-separated nested levels |
| x-forwarded-host | tenant1--myapp.tunnel.explita.ng | Original requested host |
| x-explita-request-id | uuid | Unique trace ID for the request |
Example in Next.js / Express Middleware:
export function middleware(request: Request) {
// Read tenant from Explita header
const tenant = request.headers.get("x-explita-tenant");
if (tenant) {
console.log(`Handling request for tenant: ${tenant}`);
}
}⚙️ CLI Options
Tunnel Flags (eta <port>)
| Flag | Shorthand | Default | Description |
| ------------------------- | --------- | -------------------- | ---------------------------------------------------------------------------- |
| --port <number> | -p | (positional) | Local port to expose (e.g. 3000, 8080) |
| --subdomain <name> | -s | auto-generated | Request a custom subdomain (3 to 32 chars, first-come first-served) |
| --server <url> | -S | $TUNNEL_SERVER_URL | Tunnel server WebSocket URL (defaults to wss://tunnel.explita.ng/_connect) |
| --token <token> | -t | $TUNNEL_TOKEN | Authentication token for the tunnel server |
| --inspect [port] | — | 39755 | Enable local web inspector UI and optionally set custom port |
| --no-inspect | — | false | Disable local web inspector UI dashboard (runs in quiet/headless mode) |
| --no-reconnect | -R | false | Do not automatically reconnect if the connection drops |
| --no-encrypt | -E | false | Disable inner AES-256-GCM encryption (send plaintext JSON) |
| --help | -h | — | Show help information |
Token Commands (eta token / eta login)
eta token [options]
eta login [options]| Flag / Command | Shorthand | Default | Description |
| ----------------- | --------- | -------------------- | ----------------------------------------------------------- |
| eta token | — | — | Show currently saved token, email, and config file location |
| --email <email> | -e | — | Your email address to request a 6-digit verification code |
| --verify <code> | -v | — | 6-digit OTP code to verify and save token (alias: --otp) |
| --clear | — | — | Clear saved token and email from local configuration |
| --server <url> | -S | $TUNNEL_SERVER_URL | Tunnel server URL (defaults to https://tunnel.explita.ng) |
| --help | -h | — | Show token command help |
🔒 Security & Verification
Explita Tunnel employs defense-in-depth: while connections are carried over TLS/WSS, an additional application-layer encryption layer runs inside the tunnel stream.
Client ephemeral P-256 key
│
│ ECDH (prime256v1)
▼
Shared secret
│
│ HKDF-SHA-256 ("explita-tunnel-e2ee")
▼
256-bit session key
│
▼
AES-256-GCM (12-byte random IV + auth tag)Every session establishes an ephemeral ECDH P-256 shared secret, derives a 256-bit key via HKDF-SHA-256, and encrypts all HTTP requests, response chunks, headers, and WebSocket frames with authenticated AES-256-GCM.
1. Session Fingerprint Verification
When connected, the client and server bind the key exchange and display a cryptographic SHA-256 session fingerprint:
Session Fingerprint = SHA-256("explita-tunnel-v1" || client_pubkey || server_pubkey)Explita Tunnel v0.2.0
——————————————————————————————————————————————
✓ Authenticated • Token active
✓ Key exchange complete • ECDH P-256 + AES-GCM (256-bit)
Public URL: https://my-app.tunnel.explita.ng
Forwarding: http://localhost:3000
Fingerprint: SHA256:7a:91:4f:2c:81:d3:05:e2:...
Incoming Traffic Stream:
200 OK GET /api/auth/session 12ms
101 SW WSS /ws/live-stream [Encrypted Frame]
200 OK POST /webhooks/stripe 34ms
502 Bad Gateway GET /api/users 3ms (ECONNREFUSED :3000)You can compare the client and server fingerprints to verify that both sides derived matching keys for the exact same key exchange participants, eliminating Man-in-the-Middle concerns.
📦 Programmatic Usage
You can embed the tunnel client directly inside Node.js applications or testing suites:
import { TunnelAgent } from "@explita/tunnel";
const agent = new TunnelAgent({
port: 3000,
subdomain: "my-app",
server: "ws://localhost:39755/_connect",
token: "my-token",
reconnect: true,
encrypt: true,
});
// Start the tunnel
const tunnel = await agent.start();
console.log(`Public URL: ${tunnel.url}`);
// Stop when done
process.on("SIGINT", () => {
agent.stop();
process.exit(0);
});🔌 WebSocket & Dev Server Support
The agent includes full duplex WebSocket proxying out of the box. Hot Module Replacement (HMR) and live-reloading work seamlessly with modern web dev frameworks:
- Next.js (Turbopack / Webpack HMR)
- Vite
- Astro / Nuxt / SvelteKit / Remix
- Custom WebSocket servers (
ws,Socket.IO, GraphQL subscriptions)
⚖️ Production Limits & Server Guardrails
To protect shared gateway infrastructure against abuse, the server applies the following operational limits:
| Guardrail | Limit | Description |
| :---------------------- | :--------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| Concurrent Tunnels | 5 active per token | Connecting a 6th active tunnel will fail with MAX_TUNNELS_EXCEEDED. |
| Request Rate Limit | 100 req/s per tunnel | Requests above 100 req/s receive HTTP 429 Too Many Requests (Retry-After: 1). |
| Max Request Payload | 25 MB | HTTP request bodies above 25MB are rejected with HTTP 413 Payload Too Large. |
| Session Lifetime | 24 hours | Active connections are closed gracefully once a day (code 1000) so the agent automatically reconnects and rotates cryptographic keys. |
| Subdomain Lock | 10 seconds | Atomic lock during connection prevents race conditions when registering subdomains. |
📄 License
MIT © Explita
