LLM clients transcribing the console WebSocket URL into shell commands
reliably corrupted the ~200-char JWT embedded in it (dropped header
segment -> "MissingAlgorithmError: Missing 'alg' value in header" on
every connection attempt). The node_console tool now mints a short
random ticket ("gns3t_" + 16 urlsafe chars, 10 min TTL, multi-use)
stored server-side and bound to the node's console endpoints:
- new ConsoleTicketService (gns3server/services/console_tickets.py),
in-memory store with lazy expiry sweeps
- get_current_active_user_from_websocket redeems tickets through the
existing "token" query parameter, gated on websocket.path_params so a
ticket only authenticates the console/ws and console/vnc routes of
the node it was minted for; the JWT path is unchanged
- redemption reuses the existing user lookup, token_version revocation
and is_active checks, so logging out invalidates outstanding tickets
- vnc_url no longer embeds the full session JWT
- the tool docstring now tells clients to run the returned command
verbatim instead of reconstructing the URL
11 KiB
This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
VNC WebSocket Console
Overview
GNS3 server supports WebSocket-based VNC console access, enabling browser-based graphical console connections to QEMU and Docker VMs without requiring standalone VNC client applications.
This implementation uses GNS3's API layer as a transparent WebSocket-to-TCP proxy, forwarding binary VNC protocol data between the browser and the VM's VNC server.
Architecture
Connection Flow
graph LR
A[Browser noVNC] -->|WebSocket binary| B[GNS3 Controller]
B -->|WebSocket + BasicAuth + SSL| C[GNS3 Compute]
C -->|TCP| D[QEMU/Docker VNC :5900]
Controller acts as a WebSocket-to-WebSocket relay (JWT auth, IPv6 handling). Compute acts as a WebSocket-to-TCP bridge (validates node state, opens VNC TCP connection).
Components
-
Browser (noVNC)
- HTML5 VNC client running in the browser
- Connects via WebSocket using
binarysubprotocol - Handles RFB protocol (Remote Frame Buffer) for VNC
-
GNS3 Controller API
- WebSocket endpoint:
/v3/projects/{project_id}/nodes/{node_id}/console/vnc - Authentication: JWT token via query parameter
- Authorization: RBAC privilege check via
has_privilege_on_websocket("Node.Console") - Proxies WebSocket to compute node (WebSocket-to-WebSocket relay)
- Handles IPv6 addresses by wrapping in brackets
- WebSocket endpoint:
-
GNS3 Compute API
- WebSocket endpoint:
/v3/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/vnc - Authentication: HTTP Basic Auth via
ws_compute_authentication() - Establishes TCP connection to VNC server, bridges WebSocket ↔ TCP
- WebSocket endpoint:
-
Node (QEMU/Docker)
- VNC server listening on configured port (default: 5900+)
- RFB protocol for remote display
API Endpoints
Controller WebSocket Endpoint
URL: ws://{controller_host}:{port}/v3/projects/{project_id}/nodes/{node_id}/console/vnc?token={jwt_token}
Authentication:
- JWT token via query parameter, or a short-lived console ticket (
gns3t_…, minted per node by thenode_consoleMCP tool, valid 10 min, bound to this node's console endpoints) - User must have
Node.Consoleprivilege
WebSocket Subprotocols:
- Accepts:
binary - Used by noVNC for binary data transfer
Request Example:
const token = "eyJ0eXAiOiJKV1QiLCJhbGc...";
const wsUrl = `ws://localhost:3080/v3/projects/${projectId}/nodes/${nodeId}/console/vnc?token=${token}`;
const ws = new WebSocket(wsUrl, 'binary');
ws.binaryType = 'arraybuffer';
Compute WebSocket Endpoint
URL: ws://{compute_host}:{port}/v3/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/vnc
Authentication:
- HTTP Basic Auth via
ws_compute_authentication()dependency - Configured via
settings.Server.compute_username(default:gns3) andsettings.Server.compute_password(default: empty)
Response:
- Bidirectional binary WebSocket connection
- Transparent VNC protocol forwarding
Supported Node Types
QEMU VMs
Console Type Configuration:
{
"console_type": "vnc",
"console": 5900,
"console_resolution": "1024x768"
}
QEMU Parameters:
-vnc :0 # VNC on display 0 (port = 5900 + display)
Implementation:
- File:
gns3server/api/routes/compute/qemu_nodes.py - Endpoint:
/{node_id}/console/vnc - Method:
start_vnc_websocket_console(websocket)
Docker Containers
Console Type Configuration:
{
"console_type": "vnc",
"console": 5900,
"console_resolution": "1024x768",
"console_http_port": 8080,
"console_http_path": "/"
}
Implementation:
- File:
gns3server/api/routes/compute/docker_nodes.py - Endpoint:
/{node_id}/console/vnc - Method:
start_vnc_websocket_console(websocket)
WebSocket Data Forwarding
Compute Layer (WebSocket ↔ TCP)
Location: gns3server/compute/base_node.py — start_vnc_websocket_console()
- Validates node is started and
console_type == "vnc"; closes WebSocket with code 1000 otherwise - Opens TCP connection to VNC server at
console_host:console_port - Runs two concurrent tasks via
asyncio.wait(FIRST_COMPLETED):ws_forward(): WebSocket → TCP (catchesWebSocketDisconnect)vnc_forward(): TCP → WebSocket (reads 65536-byte buffer)
- Cancels pending tasks, closes TCP writer on completion
Controller Layer (WebSocket ↔ WebSocket)
Location: gns3server/api/routes/controller/nodes.py — vnc_console()
- Authenticates user via
has_privilege_on_websocket("Node.Console")dependency - Constructs compute URL with IPv6 bracket handling
- Connects to compute WebSocket using
aiohttp.ws_connect()with HTTP Basic Auth and SSL context - Uses
asyncio.ensure_future()for client→compute forwarding,async for msgiteration for compute→client
Data Flow
sequenceDiagram
participant B as Browser (noVNC)
participant C as Controller
participant W as Compute
participant V as VNC Server
B->>C: WebSocket connect (binary, JWT token)
C->>W: WebSocket connect (binary, BasicAuth, SSL)
W->>V: TCP connect (asyncio.open_connection)
Note over B,V: Bidirectional binary forwarding active
B->>C: WebSocket binary frame
C->>W: aiohttp send_bytes()
W->>V: TCP write(data)
V->>W: TCP data (read 65536)
W->>C: WebSocket binary frame
C->>B: send_bytes()
Authentication & Authorization
Controller Layer
-
Authentication:
- JWT token validation via
has_privilege_on_websocket("Node.Console")dependency - Token passed as query parameter:
?token={jwt}, or a console ticket (gns3t_…) redeemable only on the node it was minted for
- JWT token validation via
-
Authorization:
- RBAC privilege check:
Node.Console - Per-node access control
- RBAC privilege check:
-
WebSocket Subprotocol:
- Client requests:
binary - Server accepts:
binary(if requested)
- Client requests:
Compute Layer
-
Authentication:
- HTTP Basic Auth
- Credentials from controller config
- Username:
settings.Server.compute_username - Password:
settings.Server.compute_password
-
Node Validation:
- Check node exists
- Check node is started
- Check console type is
vnc
Configuration
Server Settings
Controller Configuration (gns3-server.conf):
[Server]
host = 0.0.0.0
port = 3080
Compute Configuration (same file):
[Server]
compute_username = gns3
compute_password = # empty by default, must be set for compute auth
VNC Port Range
VNC console ports are allocated from a configurable range (gns3server/schemas/config.py):
| Setting | Default | Range |
|---|---|---|
vnc_console_start_port_range |
5900 | 5900–65535 |
vnc_console_end_port_range |
10000 | 5900–65535 |
Validation: vnc_console_end_port_range must be greater than vnc_console_start_port_range.
Node Settings
QEMU VM Example:
{
"name": "vm-1",
"node_type": "qemu",
"console_type": "vnc",
"console": 5900,
"console_resolution": "1280x720",
"properties": {
"qemu_path": "/usr/bin/qemu-system-x86_64"
}
}
Docker Container Example:
{
"name": "container-1",
"node_type": "docker",
"console_type": "vnc",
"console": 5900,
"console_resolution": "1024x768",
"console_http_port": 8080
}
Troubleshooting
Common Issues
1. "Node is not started"
- Symptom: WebSocket closes immediately
- Solution: Start the VM before opening console
- API Check:
GET /v3/projects/{project_id}/nodes/{node_id}→ verifystatus == "started"
2. "Console type is not vnc"
- Symptom: WebSocket closes with error
- Solution: Set node console_type to "vnc"
- API Update:
PUT /v3/projects/{project_id}/nodes/{node_id}with{"console_type": "vnc"}
3. "Cannot connect to VNC server"
- Symptom: Connection timeout
- Possible Causes:
- VNC server not listening
- Port conflict
- Firewall blocking connection
- Verification:
# Check if VNC is listening netstat -tlnp | grep 5900 # Check QEMU process ps aux | grep qemu
4. WebSocket Subprotocol Negotiation Failed
- Symptom: Connection rejected during handshake
- Solution: Ensure noVNC sends
binarysubprotocol - Browser Console Check:
console.log(ws.protocol); // Should be "binary"
5. Authentication Errors
- Symptom: HTTP 401/403 responses
- Controller: Check JWT token is valid and not expired
- Compute: Verify compute username/password in config
Debug Logging
Enable Detailed Logging:
[gns3server]
debug = true
Check Controller Logs:
# Look for WebSocket connection messages
grep "VNC console WebSocket" /var/log/gns3/gns3.log
Check Compute Logs:
# Look for VNC forwarding messages
grep "Connected to VNC server" /var/log/gns3/gns3.log
Browser Console:
// Enable noVNC debugging
RFB.messages.log = function(msg) { console.log(msg); };
Security
Authentication
-
Controller → Client
- JWT token with expiration
- RBAC authorization
- Privilege:
Node.Console
-
Controller → Compute
- HTTP Basic Auth via
aiohttp.BasicAuth - SSL context from
Controller.instance().ssl_context() - Raises
ControllerForbiddenErrorifcompute_usernameis not set
- HTTP Basic Auth via
-
VNC Server
- Optional VNC password (QEMU only)
- Configured via node properties
Network Security
Recommendations:
- Use HTTPS/WSS for production deployments
- Firewall compute API ports
- Use short-lived JWT tokens
- Enable VNC password for sensitive VMs
Example:
# Controller with TLS
gns3server --ssl --cert /path/to/cert.pem --key /path/to/key.pem
# VNC with password
qemu-system-x86_64 -vnc :0,password
WebSocket Security
Subprotocol Validation:
- Only
binarysubprotocol is accepted - Prevents protocol downgrade attacks
Origin Validation:
- Browser enforces same-origin policy
- Configure CORS if using separate domains
Limitations
-
Single Client per VNC Port
- VNC protocol supports one client at a time
- New connections disconnect existing clients
-
No Audio Redirection
- VNC protocol does not support audio
-
No USB Redirection
- VNC protocol does not support device redirection
-
Performance Overhead
- WebSocket-to-TCP bridging adds processing overhead compared to native VNC clients
References
- RFB Protocol 3.8 - VNC Protocol Specification
- noVNC Documentation - HTML5 VNC Client
- FastAPI WebSocket - WebSocket Implementation
- GNS3 Documentation - General GNS3 Documentation
Version History
| Version | Date | Changes |
|---|---|---|
| 1.1 | 2026-04-19 | Review against code: fix buffer size, auth details, add port range config, remove unverified content |
| 1.0 | 2026-03-17 | Initial VNC WebSocket documentation |