Repository: https://github.com/kcosr/termstation
This guide explains how TermStation exposes container-local services to API clients through a reverse tunnel, including HTTP proxying and WebSocket upgrades on paths like:
/api/sessions/:sessionId/service/:port/...
The emphasis is architectural behavior and control/data flow, with pointers to implementation files.
Containerized session workloads often bind services to 127.0.0.1:<port> inside the container. Those ports are not directly reachable from the host API process unless you publish ports or flatten network isolation.
TermStation solves this by using a per-session reverse tunnel:
- A helper process inside the container dials out to the host API via WebSocket.
- The host backend multiplexes many logical streams over that single tunnel.
- API requests to
/service/:port/...are proxied over those streams to127.0.0.1:<port>in the container.
This keeps service access session-scoped, authenticated, and compatible with both HTTP and WebSocket protocols.
Core components:
- Session bootstrap and helper startup in isolated workspaces.
- Backend tunnel registration endpoint (
/api/sessions/:id/tunnel). - In-memory tunnel/stream multiplexer on the backend.
- HTTP reverse proxy route (
/api/sessions/:id/service/:port). - Upgrade bridge for WebSocket service traffic on the same path.
Primary source files:
backend/services/session-workspace-builder.jsbackend/template-loader.jsbackend/tools/ts-tunnel/bin/ts-tunnel.jsbackend/managers/tunnel-manager.jsbackend/routes/service-proxy.jsbackend/service-proxy-upgrade.jsbackend/server.js
flowchart LR
subgraph Client[User Client]
C1[Browser/CLI\nHTTP or WebSocket]
end
subgraph Host[TermStation Backend Host]
S1[Service Proxy Route\n/api/sessions/:id/service/:port/*]
S2[Upgrade Bridge\nservice-proxy-upgrade]
S3[Tunnel Manager\nstream multiplexer]
S4[Tunnel WS Endpoint\n/api/sessions/:id/tunnel]
end
subgraph Container[Session Container]
T1[ts-tunnel helper]
T2[Local service\n127.0.0.1:PORT]
end
C1 -->|HTTP request| S1
C1 -->|WS upgrade| S2
S1 --> S3
S2 --> S3
T1 -->|register reverse tunnel\nWS + token| S4
S4 --> S3
S3 -->|open stream id + bytes| T1
T1 -->|TCP connect + relay| T2
T2 -->|response/frames| T1
T1 -->|bytes back on stream id| S3
S3 -->|proxied response/upgrade stream| C1
When a session is created, the backend issues a session-scoped access token (default no time expiry; still bound to session activity).
- Token creation and verification utilities:
backend/utils/session-access-token.js - Session creation flow and token injection into template/session variables:
backend/routes/sessions.js
This same token model is used for tunnel authentication and service access paths that accept token auth.
For container and directory isolation, the backend creates a per-session workspace with .bootstrap scripts and tools.
- Workspace builder:
backend/services/session-workspace-builder.js - Container command synthesis and
/workspacemount behavior:backend/template-loader.js
The generated run.sh sets core env vars such as:
SESSION_IDSESSIONS_API_BASE_URLSESSION_TOK
It then launches the helper (.bootstrap/bin/ts-tunnel.js) in the background when token data exists.
Inside the container, ts-tunnel builds a URL of the form:
wss://.../api/sessions/:id/tunnel?token=...
and connects to the backend.
- Helper behavior and reconnect loop:
backend/tools/ts-tunnel/bin/ts-tunnel.js - Helper usage notes:
backend/tools/ts-tunnel/README.md
On backend WebSocket connection, tunnel handshakes are recognized in backend/server.js and registered with TunnelManager.
TunnelManager stores a single active tunnel per session. A newer tunnel replaces an older one.
- Tunnel registry, stream allocation, and cleanup:
backend/managers/tunnel-manager.js
When a tunnel closes, all open logical streams are failed/cleaned up.
Tunnel protocol has two frame classes:
- Text JSON control messages.
- Binary data frames.
Binary frame format:
[type:1][streamId:4 big-endian][payload...]type=0x01for datatype=0x02for end-of-stream
Control messages include:
- Server -> helper:
{ type: 'open', id, host, port } - Helper -> server error signal:
{ type: 'err', id, message }
Implementation:
- Backend multiplexer:
backend/managers/tunnel-manager.js - Helper-side TCP socket fanout:
backend/tools/ts-tunnel/bin/ts-tunnel.js
Important safety property:
- Backend enforces loopback targeting (
127.0.0.1) and validates port ranges before opening streams.
/api/sessions/:sessionId/service/:port[/...] routes are handled by:
backend/routes/service-proxy.js
Mounted early (before body parsers) so request body streaming stays intact:
backend/server.js
For each proxied HTTP request:
- Resolve session alias to canonical session ID.
- Validate session existence and requester access.
- Validate port and tunnel availability.
- Open a logical tunnel stream to
127.0.0.1:<port>in the container. - Use Node HTTP client with a custom connection returning that tunnel stream.
- Forward request method/path/query/headers/body.
- Return upstream response, filtering hop-by-hop headers.
The proxy supports all methods (router.all) and preserves query strings/path suffixes.
- Adds forwarded headers (
x-forwarded-proto,x-forwarded-host,x-forwarded-for,x-forwarded-prefix). - Rewrites downstream
Hostto127.0.0.1:<port>. - Applies per-session operation rate limiting.
- Uses upstream timeout protection and error mapping (
502,503,429, etc.).
Reference files:
backend/routes/service-proxy.jsbackend/utils/session-access.jsbackend/utils/rate-limiters.js
HTTP upgrades are intercepted at server upgrade level before regular Express route handlers.
- Upgrade attach and dispatch order:
backend/server.js
Service upgrade handling is attempted first via:
backend/service-proxy-upgrade.js
If not a service path, upgrade is handed to the app WebSocket server.
For matched /service/:port upgrade requests:
- Parse/resolve session and validate active session state.
- Authenticate via token query or cookie/basic fallback (depending on auth mode).
- Authorize session visibility access.
- Open tunnel stream to
127.0.0.1:<port>. - Compose a minimal upstream HTTP/1.1 upgrade request and copy relevant headers.
- Pipe raw bytes both directions between client socket and tunnel stream.
This keeps WebSocket semantics intact over the same tunnel transport used for HTTP.
Reference files:
backend/service-proxy-upgrade.jsbackend/server.jsbackend/utils/session-cookie.jsbackend/utils/session-access-token.jsbackend/utils/session-access.js
Session access token format is HMAC-signed and includes session_id plus optional expiration.
- Create/verify/token URL helpers:
backend/utils/session-access-token.js
By default, no time-based expiration is set (ttl_seconds = 0), but backend handshake still requires session to be active.
Service proxy HTTP routes are mounted with basicAuth, which supports cookie/basic auth and session-token auth.
- Middleware and token flow:
backend/middleware/auth.js
After auth, access is checked against session visibility rules (private/public/shared_readonly) and admin capability.
- Access checks:
backend/utils/session-access.js
Session aliases in URLs are resolved to real session IDs before internal lookup.
- Alias registry and resolver:
backend/managers/session-manager.js - Alias-aware route parameter handling:
backend/routes/service-proxy.js - Alias-aware tunnel path handling:
backend/server.js,backend/service-proxy-upgrade.js
Relevant settings and derived behavior live in:
backend/config-loader.js
Notable controls:
features.proxy_container_servicestoggles service proxy route mounting.sessions_api_base_url/container_sessions_api_base_urldefine helper dial target.container_use_socket_adapterallows in-container TCP->Unix-socket bridging via socat.session_token.ttl_secondscontrols token expiration behavior.- Multiple listener types (
http,socket) share the same upgrade/tunnel machinery.
Typical outcomes:
- No registered tunnel: service requests fail with
503 Service tunnel unavailable. - Tunnel stream errors/close during proxying: backend returns
502 Bad gateway. - Unauthorized/forbidden upgrade attempts: backend closes/destroys socket or sends
401/403response before close. - Tunnel helper down: requests remain unavailable until helper reconnects.
The helper includes reconnection backoff logic, so recovery is automatic once API reachability/auth succeed.
Operational logging is intentionally explicit in proxy and upgrade paths:
- Request lifecycle, bytes up/down, status, and duration in
backend/routes/service-proxy.js - Upgrade diagnostics in
backend/service-proxy-upgrade.jsandbackend/server.js - Tunnel connect/disconnect/open-stream logs in
backend/managers/tunnel-manager.js - Helper-side debug mode in
backend/tools/ts-tunnel/bin/ts-tunnel.js
- Single reverse tunnel per session, multiplexed into many logical streams.
- Loopback-only target enforcement limits lateral movement.
- Unified URL surface for HTTP and WebSocket service access.
- Session-scoped token model integrated with existing backend auth/access controls.
- Works across HTTP and local socket listeners with shared upgrade handling.