Skip to content

Instantly share code, notes, and snippets.

@kcosr
Last active February 9, 2026 02:31
Show Gist options
  • Select an option

  • Save kcosr/c8f09ae9a7cd67b527c2bcc38c4ed461 to your computer and use it in GitHub Desktop.

Select an option

Save kcosr/c8f09ae9a7cd67b527c2bcc38c4ed461 to your computer and use it in GitHub Desktop.
HTTP Service Tunneling Design

HTTP Service Tunneling Design

Repository: https://github.com/kcosr/termstation

Purpose

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.

Problem This Solves

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:

  1. A helper process inside the container dials out to the host API via WebSocket.
  2. The host backend multiplexes many logical streams over that single tunnel.
  3. API requests to /service/:port/... are proxied over those streams to 127.0.0.1:<port> in the container.

This keeps service access session-scoped, authenticated, and compatible with both HTTP and WebSocket protocols.

High-Level Architecture

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.js
  • backend/template-loader.js
  • backend/tools/ts-tunnel/bin/ts-tunnel.js
  • backend/managers/tunnel-manager.js
  • backend/routes/service-proxy.js
  • backend/service-proxy-upgrade.js
  • backend/server.js

Diagram

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
Loading

Lifecycle

1. Session creation and token provisioning

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.

2. Workspace/bootstrap materialization

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 /workspace mount behavior: backend/template-loader.js

The generated run.sh sets core env vars such as:

  • SESSION_ID
  • SESSIONS_API_BASE_URL
  • SESSION_TOK

It then launches the helper (.bootstrap/bin/ts-tunnel.js) in the background when token data exists.

3. Helper dials backend and registers tunnel

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.

4. Backend tracks one active tunnel per session

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.

Multiplexing Protocol

Tunnel protocol has two frame classes:

  1. Text JSON control messages.
  2. Binary data frames.

Binary frame format:

  • [type:1][streamId:4 big-endian][payload...]
  • type=0x01 for data
  • type=0x02 for 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.

HTTP Service Proxy Flow

Entry point

/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

Request handling model

For each proxied HTTP request:

  1. Resolve session alias to canonical session ID.
  2. Validate session existence and requester access.
  3. Validate port and tunnel availability.
  4. Open a logical tunnel stream to 127.0.0.1:<port> in the container.
  5. Use Node HTTP client with a custom connection returning that tunnel stream.
  6. Forward request method/path/query/headers/body.
  7. Return upstream response, filtering hop-by-hop headers.

The proxy supports all methods (router.all) and preserves query strings/path suffixes.

Forwarding details

  • Adds forwarded headers (x-forwarded-proto, x-forwarded-host, x-forwarded-for, x-forwarded-prefix).
  • Rewrites downstream Host to 127.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.js
  • backend/utils/session-access.js
  • backend/utils/rate-limiters.js

WebSocket Upgrade Proxy Flow

Why separate path handling

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.

Upgrade bridging model

For matched /service/:port upgrade requests:

  1. Parse/resolve session and validate active session state.
  2. Authenticate via token query or cookie/basic fallback (depending on auth mode).
  3. Authorize session visibility access.
  4. Open tunnel stream to 127.0.0.1:<port>.
  5. Compose a minimal upstream HTTP/1.1 upgrade request and copy relevant headers.
  6. 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.js
  • backend/server.js
  • backend/utils/session-cookie.js
  • backend/utils/session-access-token.js
  • backend/utils/session-access.js

Authentication and Authorization Model

Token model

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.

HTTP route auth

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

Access control

After auth, access is checked against session visibility rules (private/public/shared_readonly) and admin capability.

  • Access checks: backend/utils/session-access.js

Alias handling

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

Configuration and Deployment Considerations

Relevant settings and derived behavior live in:

  • backend/config-loader.js

Notable controls:

  • features.proxy_container_services toggles service proxy route mounting.
  • sessions_api_base_url / container_sessions_api_base_url define helper dial target.
  • container_use_socket_adapter allows in-container TCP->Unix-socket bridging via socat.
  • session_token.ttl_seconds controls token expiration behavior.
  • Multiple listener types (http, socket) share the same upgrade/tunnel machinery.

Failure Modes and Behavior

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/403 response 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.

Observability

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.js and backend/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

Design Characteristics (Summary)

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment