API Reference¶
Tunnel Whisperer exposes two APIs: a REST/WebSocket/SSE API served by the dashboard for browser and HTTP clients, and a gRPC API for CLI-to-daemon communication.
REST API (Dashboard)¶
The dashboard HTTP server registers the endpoints listed below. All REST endpoints accept and return JSON unless noted otherwise.
Authentication required
Every route except /login, /logout, and /static/* requires the dashboard token — either a SameSite=Strict session cookie (set at login) or an Authorization: Bearer <token> header. /metrics is gated too. Fetch the token with tw dashboard token (see Dashboard → Signing in). Unauthenticated browser requests are redirected to /login; unauthenticated API requests get 401.
Read-only¶
| Method | Path | Description |
|---|---|---|
GET | /api/status | Current daemon status (mode, version, relay, server/client state) |
GET | /api/config | Current configuration |
GET | /api/relay | Relay provisioning status (provisioned, domain, IP, provider, ssh_open) |
GET | /api/providers | List of supported cloud providers for relay provisioning |
GET | /api/stats | Bandwidth statistics snapshots and history (returns enabled: false when analytics is off) |
GET | /metrics | Prometheus-format bandwidth metrics |
Contexts¶
| Method | Path | Description |
|---|---|---|
GET | /api/config/contexts | List stored contexts (name, role, user, relay, id, current) |
POST | /api/config/use-context | Switch the daemon's active context and reconnect. Body: { "name": "..." } |
Mode¶
| Method | Path | Description |
|---|---|---|
POST | /api/mode | Set the operating mode |
Request body:
Settings¶
| Method | Path | Description |
|---|---|---|
POST | /api/proxy | Set or clear the outbound proxy URL |
POST | /api/log-level | Set the log level (debug, info, warn, error) |
POST | /api/settings/server | Update server settings (ports, relay SSH user, temp Xray port) |
POST | /api/settings/xray | Update Xray transport settings (relay host, port, path) |
POST | /api/settings/client | Update client settings (SSH user, server SSH port, Xray port, listen address) |
POST | /api/settings/analytics | Enable/disable analytics and set history size |
Proxy request body:
Log level request body:
Server settings request body:
{
"ssh_port": 2222,
"api_port": 50051,
"dashboard_port": 8080,
"relay_ssh_port": 22,
"relay_ssh_user": "ubuntu",
"remote_port": 2222,
"xray_port": 54001,
"temp_xray_port": 59000
}
Only non-zero / non-empty fields are applied; omitted fields keep their current value.
Xray settings request body:
Client settings request body:
{
"ssh_user": "tunnel",
"server_ssh_port": 2222,
"xray_port": 54001,
"listen_address": "127.0.0.1"
}
xray_port and listen_address are optional. listen_address sets the local interface forwarded tunnels bind to (0.0.0.0 to expose them on all interfaces).
Analytics settings request body:
Analytics changes take effect immediately — no restart required. The stats collector is created or destroyed on the fly.
Restart required
Settings changes are persisted to config.yaml immediately. A restart (server) or reconnect (client) is needed for most changes to take effect. Exception: analytics settings take effect immediately.
Server control¶
| Method | Path | Description |
|---|---|---|
POST | /api/server/start | Start all server components (SSH, Xray, reverse tunnel) |
POST | /api/server/stop | Stop the server |
POST | /api/server/restart | Stop and restart the server |
Client control¶
| Method | Path | Description |
|---|---|---|
POST | /api/client/start | Start the client (Xray + SSH tunnel) |
POST | /api/client/stop | Stop the client |
POST | /api/client/reconnect | Disconnect and reconnect the client |
POST | /api/client/upload | Upload a client context bundle (.twctx) to configure the client |
POST | /api/client/port-override | Set or clear a tunnel's local port override, keyed by server port (client mode only; the HTTP counterpart of tw client set-port) |
Upload: POST /api/client/upload expects a multipart/form-data body with the bundle in a config file field (10 MB max).
Port override: POST /api/client/port-override takes a JSON body {"server_port": <int>, "local_port": <int>}, or {"server_port": <int>, "clear": true} to remove an override. Takes effect on next reconnect.
Relay management¶
| Method | Path | Description |
|---|---|---|
POST | /api/relay/test-creds | Validate cloud provider credentials |
POST | /api/relay/provision | Provision a new relay server via Terraform |
POST | /api/relay/destroy | Destroy the provisioned relay server |
POST | /api/relay/test | Run connectivity tests against the relay |
POST | /api/relay/generate-script | Generate the manual install script for a bring-your-own-VM relay |
POST | /api/relay/save-manual | Save relay details from a manual (non-Terraform) setup |
WS | /api/relay/ssh | WebSocket-based interactive SSH shell to the relay server |
POST | /api/relay/close-ssh | Close the interactive relay SSH session |
WebSocket: /api/relay/ssh
This endpoint upgrades to a WebSocket connection and provides a full interactive terminal session to the relay server. The dashboard uses xterm.js to render the terminal in the browser.
Enrolled servers (relay mode only)¶
These endpoints mirror the tw relay … enrollment commands and answer only when the daemon runs in relay mode.
| Method | Path | Description |
|---|---|---|
GET | /api/servers | List enrolled servers with live tunnel state (tw relay get-servers) |
POST | /api/servers/unenroll | Un-enroll a server. Body: { "server_id": "..." } |
Enrollment has no REST endpoint
tw relay invite and tw join require a spoken SAS confirmation between two humans, which doesn't map to a stateless REST call — there is no /api/servers/enroll. Enroll from the CLI; the dashboard's Servers page only manages already-enrolled tenants.
User management¶
| Method | Path | Description |
|---|---|---|
GET | /api/users | List all configured users |
DELETE | /api/users/{name} | Delete a user by name |
POST | /api/users/{name}/single-session | Enable/disable the user's single-session flag |
POST | /api/users/apply | Register users on the relay. Body: { "names": [...] } (empty = all) |
POST | /api/users/unregister | Unregister users from the relay |
GET | /api/users/online | List currently connected users |
There is no create or edit endpoint: users come to exist only through the interactive invite ceremony (tw server user invite), and their mappings are fixed at enrollment — changing them means deleting the user and inviting them again.
Application templates¶
| Method | Path | Description |
|---|---|---|
GET | /api/apps | List all application templates |
POST | /api/apps | Create a new application template |
PUT | /api/apps/{name} | Update an application template |
DELETE | /api/apps/{name} | Delete an application template |
Create/update application request body:
{
"name": "web-app",
"mappings": [
{ "client_port": 3000, "server_port": 3000 },
{ "client_port": 5432, "server_port": 5432 }
]
}
Application templates are reusable port mapping bundles. They are stored in config.yaml under server.applications and are not synced to the relay.
Server-Sent Events (SSE)¶
| Method | Path | Description |
|---|---|---|
GET | /api/events/{session_id} | SSE stream of daemon events (status changes, progress) |
GET | /api/logs | SSE stream of real-time log output |
The {session_id} parameter identifies a browser session so multiple dashboard tabs can each receive events independently.
Event format — unnamed data: frames only (no event: field). Each frame is one ProgressEvent (or, on /api/logs, one log entry):
Daemon status is polled via GET /api/status, not streamed.
gRPC API¶
The gRPC API listens on 127.0.0.1:50051 (loopback only; port configurable via server.api_port) and is used for CLI-to-daemon communication. It starts automatically with tw server start and tw dashboard. When a daemon is running, CLI commands like tw status and tw server user list connect to this API instead of reading state directly from disk.
Authentication¶
Loopback is not treated as an auth boundary: every RPC requires a per-daemon bearer token. The daemon generates a 32-byte token into a 0600 file (api.token in the config directory) on startup; the built-in client reads it from the same directory and attaches it as authorization: Bearer <token> metadata. A caller that cannot read the token file gets Unauthenticated, so a local process cannot overwrite keys or config, flip mode, delete users, or read secrets through the API merely by reaching loopback.
JSON codec — the proto is documentation only¶
The API uses the gRPC server machinery, but the wire format is JSON, not protobuf. A custom codec (registered under the content-subtype json) marshals hand-written Go structs directly, so no protoc-generated code is involved. The file proto/api/v1/service.proto exists as documentation only; make proto regenerates stubs that are not used on the wire. The service is registered as api.v1.TunnelWhisperer and every RPC is unary.
Clients must therefore dial with the JSON call option and attach the bearer token (the built-in client does: grpc.CallContentSubtype("json"), per-RPC token credentials over the plaintext loopback transport, 2-second dial timeout).
Note
The gRPC API is an internal interface. Its message shapes may change between versions. Use the REST API for integrations.
Service: api.v1.TunnelWhisperer¶
| Method | Request → Response | Description |
|---|---|---|
GetStatus | Empty → StatusResponse | Mode, version, relay status, user count, connected-user count, server/client component state |
GetConfig | Empty → ConfigResponse | The current on-disk configuration |
SetMode | SetModeRequest → Empty | Set the operating mode |
ListProviders | Empty → ListProvidersResponse | Supported cloud providers for relay provisioning |
GetRelayStatus | Empty → RelayStatusResponse | Relay provisioning/connection status |
TestCredentials | TestCredentialsRequest → Empty | Validate cloud-provider credentials |
ProvisionRelay | ProvisionRelayRequest → ProvisionRelayResponse | Provision a relay VM via Terraform |
DestroyRelay | DestroyRelayRequest → Empty | Destroy the relay (accepts cloud credentials map) |
TestRelay | Empty → TestRelayResponse | Run relay connectivity tests; returns per-step results |
StartServer / StopServer | Empty → Empty | Start/stop all server components |
StartClient / StopClient | Empty → Empty | Start/stop the client |
UploadClientConfig | UploadClientConfigRequest → Empty | Import a client context bundle (bytes) in client mode |
ListUsers | Empty → ListUsersResponse | All configured users with tunnel mappings |
DeleteUser | DeleteUserRequest → Empty | Delete a user by name |
The CLI's built-in client wraps the subset it needs: GetStatus, TestRelay, ListUsers, DeleteUser, and DestroyRelay; every CLI command that can use the daemon falls back to local (on-disk) operation when no daemon answers on server.api_port. The invite flows (tw relay invite, tw server user invite, tw join) don't go through this API at all — they dial the relay directly over the Xray/SSH tunnel.