feat(support): add support service with WebSockets and Yamux#47
Open
edospadoni wants to merge 13 commits intomainfrom
Open
feat(support): add support service with WebSockets and Yamux#47edospadoni wants to merge 13 commits intomainfrom
edospadoni wants to merge 13 commits intomainfrom
Conversation
… management Support service (port 8082) for remote support sessions via WebSocket tunnels. Includes tunnel-client for NS8 and NethSecurity, yamux multiplexer, web terminal, HTTP proxy, session lifecycle management, rate limiting, and graceful reconnection.
Support session CRUD, WebSocket terminal with one-time tickets, subdomain proxy with body rewriting, access logging, RBAC with connect:systems permission, database migrations, and security hardening from penetration test findings.
Support sessions table with pagination and sorting, xterm.js web terminal with multi-tab support, service dropdown with multi-node grouping, connect:systems permission guard, and i18n translations.
Add support service routing in nginx proxy, Render.com deployment config, CI pipeline with tunnel-client Docker image and rolling dev release, release workflow with tunnel-client binary and SBOM, connect:systems RBAC permission.
Contributor
|
🔗 Redirect URIs Added to Logto The following redirect URIs have been automatically added to the Logto application configuration: Redirect URIs:
Post-logout redirect URIs:
These will be automatically removed when the PR is closed or merged. |
Contributor
🤖 My API structural change detectedStructural change detailsAdded (10)
Modified (5)
Powered by Bump.sh |
…h in artifact name
…t subdomain catch-all
Allows manually created services (not from Blueprint) to be reached from PR preview environments by setting their env var to a full FQDN.
c62b877 to
007bd6d
Compare
…kend Address 27 findings from security audit: prevent double-close panic with sync.Once, fix TOCTOU race in session creation with DB transaction, add gzip bomb protection, limit manifest size/rate, validate service names, use full session UUID in subdomain proxy, add org_role to proxy tokens, harden WebSocket origin checks, add session rate limiting, fix concurrent read/write safety, and multiple other hardening improvements.
…kend Address 23 findings from penetration testing report on the support service: - SSRF/DNS rebinding prevention with IP validation and DNS resolution checks - Open redirect fix via protocol-relative URL sanitization - CORS restriction from AllowAllOrigins to localhost-only in debug mode - HSTS, CSP, X-Content-Type-Options security headers in nginx proxy - InternalSecret middleware for defense-in-depth inter-service auth - PTY environment variable sanitization to prevent credential leakage - Cookie rewriting to prevent cross-session domain leakage - Global memory budget (50MB) for gzip decompression (bomb mitigation) - CONNECT protocol newline injection prevention with service name validation - Container hardening with nginx-unprivileged and non-root users - Input validation for node_id and service names - Nginx server_name regex anchoring for multi-environment support - Rate limiter single-instance design documentation - Non-functional default secrets in .env.example files
Add pid directive to /tmp/nginx.pid and create writable cache directories so nginx can run as non-root user without permission errors.
Add https://*.nethesis.it to connect-src so the frontend can reach the Logto identity provider for OIDC flows.
Member
Author
tunnel-client binary (linux/amd64)Download: Binary: tunnel-client.zip Quick start# Make it executable
chmod +x tunnel-client-linux-amd64
# Run it
./tunnel-client-linux-amd64 \
--url wss://my-proxy-qa-pr-47.onrender.com/support/api/tunnel \
--key <SYSTEM_KEY> \
--secret <SYSTEM_SECRET>Parameters
Service discovery modesThe tunnel-client auto-detects the environment:
Environment variablesAll flags can also be passed as env vars: export SUPPORT_URL=wss://my-proxy-qa-pr-47.onrender.com/support/api/tunnel
export SYSTEM_KEY=<your-key>
export SYSTEM_SECRET=<your-secret>
./tunnel-client-linux-amd64 |
Embed the support session ID directly in system list and detail endpoints to avoid N+1 API calls when checking session status per system.
aa25c3d to
825f86b
Compare
Show a clickable headset icon next to system name when an active support session exists. The popover displays session status, dates, and connected operators with per-node terminal badges. Backend now tracks terminal disconnect times via access log lifecycle (insert returns ID, disconnect updates disconnected_at).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📋 Description
🏗 Support Service — Architecture
How it works
A tunnel client on the customer's system opens a persistent WebSocket to our support service. The connection is multiplexed with yamux — one WebSocket carries many parallel streams. When an operator clicks "Open" in the UI, traffic flows through the tunnel to reach the remote service (web UI, terminal, API) as if it were local.
graph LR subgraph Customer System TC[tunnel-client<br/>yamux mux] --> WU[Web UI] TC --> SA[SSH/API] TC --> ETC[...] end TC ---|WebSocket<br/>single connection| SS BR[Browser<br/>operator] --> NG[nginx<br/>proxy] NG --> BE[Backend :8080<br/>sessions, auth] BE --> SS[Support :8082<br/>tunnels, yamux]Session Lifecycle
stateDiagram-v2 [*] --> pending pending --> active : WebSocket established active --> closed : operator closes active --> grace_period : disconnect grace_period --> active : reconnect (same session) grace_period --> expired : timeout (30-60s)WebSocket + yamux Multiplexing
The tunnel client opens one WebSocket to the support service. On top of it, yamux creates a multiplexed session — like having many TCP connections inside a single one.
How it connects:
GET /support/api/tunnelwith HTTP Basic Authnet.Connyamux.Serveris created over the wrapped connection (keepalive 15s)name,host,port,protocol)On disconnect: the tunnel enters a grace period (30-60s). If the client reconnects with its
reconnect_token, the same session is reused (no new session created). If the grace expires, the session is closed.How the UI Proxy works (subdomain)
When an operator clicks a service link (e.g. NethVoice UI), the browser opens a new tab on a dedicated subdomain. Each service gets its own origin, so all the app's absolute paths (
/_next/,/api/,/static/) work natively.The
?token=is removed from the URL after the first request (redirect), so it never leaks in logs, referrer headers, or browser history.How the Web Terminal works (xterm.js)
The terminal needs a WebSocket from the browser, but browsers can't send
Authorizationheaders on WebSocket connections. Solution: one-time ticket exchanged beforehand.The tunnel client spawns a PTY (pseudo-terminal) directly on the customer system — no SSH daemon involved. The PTY output is forwarded as raw bytes through the yamux stream back to the browser's xterm.js.
Why TCP hijacking instead of
httputil.ReverseProxy?When the browser opens a WebSocket, it sends an HTTP request with
Upgrade: websocket. The server responds with101 Switching Protocolsand from that point the connection is no longer HTTP — it becomes a raw bidirectional byte channel.httputil.ReverseProxycan't handle this. It's designed for the classic HTTP request/response cycle: read the response from the backend, copy it to the client, close. With a WebSocket there's no "response" to copy — there's a continuous stream of frames in both directions.Gin (which uses
net/httpunderneath) has the same problem: itsResponseWriterbuffers, manages headers, Content-Length... none of which make sense after the101.The solution is
http.Hijacker: a Go interface that lets you take control of the raw TCP connection from the HTTP server. You're telling Go "I'll handle it from here".The flow:
101 Switching Protocolsfrom the support serviceHijack()on the browser connection — now it has the raw TCP socket101to the browserio.Copy): browser ↔ support serviceNo HTTP, no buffering, no overhead. Just bytes flowing through.
Access Patterns & Auth
system_key:system_secret(SHA256), 3-tier cache (memory → Redis → DB), rate-limitedconnect:systemspermission, standard middleware chainGETDELon use → WebSocket via TCP hijack{session_id, service_name, org_role}→ SameSite=Strict cookie on subdomain → auto-redirect strips token from URLINTERNAL_SECRETX-Session-Token(64-char hex, per-session) + sharedSUPPORT_INTERNAL_SECRETfor service-level auth, constant-time validationSecurity Highlights
GETDEL), JWT never touches the URL?token=, gets stored asHttpOnly SameSite=Strictcookie, URL is cleaned via redirect +Referrer-Policy: no-referrercrypto/subtlefor all token validations — no timing attacks169.254.x.x), link-local, multicast, loopbackframe-ancestors 'self'on proxied responses — prevents clickjackingSubdomain Proxy
Each service gets its own browser origin — no URL rewriting needed:
Requires: DNS wildcard
*.support.{domain}+ matching wildcard SSL certificate +SUPPORT_PROXY_DOMAINenv var.Inter-service Communication
Components & Files
services/support/backend/methods/support_proxy.gofrontend/src/components/support/proxy/nginx.confbackend/database/migrations/009_*support_sessions,support_access_logstables.github/workflows/,render.yaml,deploy.shRelated Issue: #[ISSUE_NUMBER]
🚀 Testing Environment
To trigger a fresh deployment of all services in the PR preview environment, comment:
To download tunnel-client binary, reference here: #47 (comment)
Automatic PR environments:
✅ Merge Checklist
Code Quality:
Builds: