Appearance
Terminal Access
Students get a real shell in their lab containers from the browser. There is no SSH server involved: the bridge uses the LXD control plane.
Architecture
Connection sequence
Details
| Topic | Behavior | Source |
|---|---|---|
| Server | Python websockets 12 (the bridge refuses to start with another major version) | cyberrange/bridge-src/bridge.py, requirements.txt |
| Bind | BRIDGE_BIND_HOST:BRIDGE_BIND_PORT; the README and manual use 10.115.77.1:8765. The code's fallback host is 10.0.10.11, so the env file must set it | bridge.py, bridge-src/README.md |
| Container per tab | pod_type kali, meta or dvwa maps to the lab row's vmid_* container name | container_name_for() |
| User per container | kali and dvwa: student; meta: msfadmin | POD_USERS |
| Session | A reattachable tmux session named lab (history limit 10000), falling back to a login shell if tmux is missing. Reconnecting reattaches the same session, so running programs survive a page reload. | launch_command_for() |
| One viewer per container | A new connection to the same container closes the previous one (its process group is terminated and its WebSocket closed) | _evict_existing_session() |
| First size | The bridge waits up to 1 second for the browser's first resize message so the session opens at the real size (falling back to 24x80); keystrokes that arrive first are replayed | _first_client_size() |
| Resize | The browser sends {"type":"resize","cols":N,"rows":M}; sizes are clamped to 5 to 200 rows and 20 to 500 columns. The bridge resizes its own PTY and signals lxc exec, but lxc exec does not carry a mid-session resize into the container, so since pull request 212 it also runs a short root command in the container that sets the TTY size of every client attached to the lab tmux session and sends that client SIGWINCH. Bursts of resizes (dragging a window) are collapsed into one run with the latest size | parse_client_message(), tmux_resize_command_for(), ResizeCoalescer |
| Reconnect | Since pull request 224 the browser reconnects a dropped terminal automatically, retrying after 1, 2 and 4 seconds and then showing a Reconnect button. It does not retry on close codes 1008 (authorization) or 1011 (internal error, for example a missing container). The backoff resets only after the bridge confirms [term] Authenticated and the session stays up for 5 seconds, so a second tab that keeps evicting the first cannot loop forever. Because the shell lives in tmux, a reconnect reattaches the same session | portal/src/components/terminal/XtermView.tsx, terminalSessionManager.ts |
| Idle | nginx keeps the WebSocket open for up to 3600 seconds without traffic | cyberrange/deploy/guacamole/nginx-ampere.conf |
| Errors | Bad input or authorization: message in the terminal and close code 1008; internal errors: close 1011 | handler() |
| Config | AUTH_ENABLED, KEYCLOAK_INTROSPECT_URL, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, PROVISION_API_URL, BRIDGE_BIND_HOST, BRIDGE_BIND_PORT in bridge-src/.env.bridge (mode 600) | .env.bridge.example |
| Service | user unit cyberrange-ssh-bridge, virtualenv Python, logs in /tmp/ssh-bridge.log | cyberrange/deploy/systemd/cyberrange-ssh-bridge.service |
Why tmux matters for scoring
Scenario 1 Milestone 4 checks the student's tmux lab session directly: the pane must be running Metasploit, show a successful tomcat_mgr_deploy session to meta, and hold a live connection to it (check_live_tomcat_msf_session in cyberrange/src/provisioning/scoring_checks.sh). Keeping the shell in a named tmux session is what makes that check possible.
Security notes
- Token in the URL. The access token travels as a query parameter. Query strings are commonly written to proxy access logs; the gateway nginx has no custom
access_logsetting, so its default access log would record them. See Findings. - Admins can open any lab. The ownership check calls
/pods/{id}/status, which allows the owner or an admin. No admin page uses this, but an admin token works for any lab. Instructors cannot open student terminals. - Root inside the lab only. Students can
sudoinside their own containers, which are unprivileged LXD containers. - Bypass mode.
AUTH_ENABLED=falseskips introspection and ownership (development only; logged as a warning).
Sources
cyberrange/bridge-src/bridge.py, test_bridge.py, README.md, .env.bridge.example, cyberrange/portal/src/components/terminal/XtermView.tsx, terminalSessionManager.ts, cyberrange/deploy/guacamole/nginx-ampere.conf, cyberrange/src/provisioning/auth.py.