gns3-server/docs/features/vnc-spice-websocket-console.md
YueGuobin c98f802362 feat(compute): add SPICE WebSocket console support
Add WebSocket-based console support for SPICE protocol, enabling
   browser-based graphical console access with enhanced features
   like clipboard sharing, USB redirection, and audio streaming.

   Changes:
   - Add start_spice_websocket_console() method in BaseNode for SPICE
     WebSocket to TCP bridging with bidirectional binary forwarding
   - Add /console/spice WebSocket endpoints in QEMU and Docker compute APIs
   - Add /console/spice WebSocket proxy endpoint in controller API
   - Add comprehensive API documentation in docs/features/

   Supported console types:
   - spice: Basic SPICE protocol support
   - spice+agent: SPICE with spice-vdagent for enhanced features

   Architecture:
   - Browser WebSocket → Controller (JWT + RBAC) → Compute (Basic Auth)
   - No external websockify processes required
   - Consistent with existing VNC WebSocket implementation
2026-03-16 22:21:25 +08:00

24 KiB

VNC/SPICE WebSocket Console Support

Feature Overview

Date: 2026-03-16 Status: Implemented Branch: feature/vnc-interface Component: Compute API, Controller API, BaseNode

Summary

Add WebSocket-based console support for VNC and SPICE console types, enabling browser-based graphical console access to QEMU VMs and Docker containers without requiring standalone websockify processes.


Architecture

Design Principles

  1. Unified WebSocket Architecture: All console types (Telnet, VNC, SPICE) use consistent WebSocket endpoints
  2. Authentication Integration: Leverage existing GNS3 authentication mechanisms
  3. No External Dependencies: Eliminate standalone websockify processes and additional port allocations
  4. Layered Design: Controller → Compute → Node WebSocket forwarding

Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                         Browser Client                          │
│  - noVNC for VNC                                                │
│  - spice-html5 for SPICE                                        │
└────────────────────────┬────────────────────────────────────────┘
                         │ WebSocket Connection
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  GNS3 Controller API                                            │
│  - /v3/controller/projects/{id}/qemu/nodes/{id}/console/vnc     │
│  - /v3/controller/projects/{id}/qemu/nodes/{id}/console/spice   │
│  - HTTP Basic Auth + RBAC (Node.Console privilege)              │
└────────────────────────┬────────────────────────────────────────┘
                         │ WebSocket Forwarding
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  GNS3 Compute API                                               │
│  - /v3/compute/projects/{id}/qemu/nodes/{id}/console/vnc       │
│  - /v3/compute/projects/{id}/qemu/nodes/{id}/console/spice     │
│  - HTTP Basic Auth (compute credentials)                        │
└────────────────────────┬────────────────────────────────────────┘
                         │ WebSocket → TCP Bridge
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  BaseNode.start_vnc_websocket_console()                         │
│  BaseNode.start_spice_websocket_console()                       │
│  - Bidirectional binary data forwarding                         │
│  - WebSocket → TCP (VNC:5900+, SPICE:5900+)                     │
└────────────────────────┬────────────────────────────────────────┘
                         │ TCP Connection
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  QEMU VM / Docker Container                                     │
│  - VNC Server (QXL display)                                     │
│  - SPICE Server (with optional spice-vdagent)                   │
└─────────────────────────────────────────────────────────────────┘

API Endpoints

Compute Layer

VNC Console WebSocket

WS /v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/vnc
WS /v3/compute/projects/{project_id}/docker/nodes/{node_id}/console/vnc

Authentication: HTTP Basic Auth (compute credentials)

Request Headers:

Upgrade: websocket
Connection: Upgrade
Authorization: Basic <base64(username:password)>

Response:

  • Binary WebSocket connection for VNC protocol (RFB)

Implementation:

# gns3server/api/routes/compute/qemu_nodes.py
@router.websocket("/{node_id}/console/vnc")
async def vnc_console_ws(
    websocket: WebSocket = Depends(ws_compute_authentication),
    node: QemuVM = Depends(dep_node)
):
    await node.start_vnc_websocket_console(websocket)

SPICE Console WebSocket

WS /v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/spice
WS /v3/compute/projects/{project_id}/docker/nodes/{node_id}/console/spice

Authentication: HTTP Basic Auth (compute credentials)

Request Headers:

Upgrade: websocket
Connection: Upgrade
Authorization: Basic <base64(username:password)>

Response:

  • Binary WebSocket connection for SPICE protocol

Implementation:

# gns3server/api/routes/compute/qemu_nodes.py
@router.websocket("/{node_id}/console/spice")
async def spice_console_ws(
    websocket: WebSocket = Depends(ws_compute_authentication),
    node: QemuVM = Depends(dep_node)
):
    await node.start_spice_websocket_console(websocket)

Controller Layer

VNC Console WebSocket

WS /v3/controller/projects/{project_id}/qemu/nodes/{node_id}/console/vnc
WS /v3/controller/projects/{project_id}/docker/nodes/{node_id}/console/vnc

Authentication: User JWT token + RBAC privilege check

Required Privilege: Node.Console

Response:

  • Binary WebSocket connection (proxied to compute layer)

SPICE Console WebSocket

WS /v3/controller/projects/{project_id}/qemu/nodes/{node_id}/console/spice
WS /v3/controller/projects/{project_id}/docker/nodes/{node_id}/console/spice

Authentication: User JWT token + RBAC privilege check

Required Privilege: Node.Console

Response:

  • Binary WebSocket connection (proxied to compute layer)

Console Types

Supported Console Types

Console Type Description Node Support
telnet Serial console via Telnet protocol All nodes
vnc Graphical console via VNC (RFB) protocol QEMU, Docker
spice Enhanced graphical console via SPICE protocol QEMU only*
spice+agent SPICE with spice-vdagent for features like clipboard sharing, folder sharing QEMU only*
none No console All nodes

*Docker supports the console type in schema but typically doesn't use SPICE (SPICE is designed for KVM/QEMU VMs).

Console Type Configuration

QEMU VM:

{
  "console_type": "spice+agent",
  "console": 5900
}

Docker Container:

{
  "console_type": "vnc",
  "console": 5900
}

Implementation Details

BaseNode WebSocket Forwarding

File: gns3server/compute/base_node.py

VNC Console Implementation

async def start_vnc_websocket_console(self, websocket):
    """
    Connect to VNC console using WebSocket.

    :param websocket: FastAPI WebSocket object
    """
    # 1. Validation
    if self.status != "started":
        await websocket.close(code=1000)
        raise NodeError(f"Node {self.name} is not started")
    if self._console_type != "vnc":
        await websocket.close(code=1000)
        raise NodeError(f"Node {self.name} console type is not vnc")

    # 2. Connect to VNC server
    vnc_reader, vnc_writer = await asyncio.open_connection(
        self._manager.port_manager.console_host,
        self.console  # VNC TCP port (e.g., 5900)
    )

    # 3. Bidirectional forwarding
    async def ws_forward(vnc_writer):
        # Browser → VNC
        while True:
            data = await websocket.receive_bytes()
            vnc_writer.write(data)
            await vnc_writer.drain()

    async def vnc_forward(vnc_reader):
        # VNC → Browser
        while not vnc_reader.at_eof():
            data = await vnc_reader.read(65536)  # Larger buffer for VNC frames
            await websocket.send_bytes(data)

    # 4. Run both directions concurrently
    aws = [
        asyncio.create_task(ws_forward(vnc_writer)),
        asyncio.create_task(vnc_forward(vnc_reader))
    ]
    done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)

    # 5. Cleanup
    for task in pending:
        task.cancel()
    vnc_writer.close()
    await vnc_writer.wait_closed()

SPICE Console Implementation

SPICE implementation is identical to VNC, except for:

  • Console type validation: self._console_type in ("spice", "spice+agent")
  • Connection target: SPICE TCP port
  • Log messages reference SPICE instead of VNC

Controller Layer Proxy

File: gns3server/api/routes/controller/nodes.py

@router.websocket("/{node_id}/console/spice")
async def spice_console(
    websocket: WebSocket,
    current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
    node: Node = Depends(dep_node)
):
    """
    SPICE WebSocket console.

    Required privilege: Node.Console
    """
    # 1. Authentication & Authorization
    if current_user is None:
        return

    # 2. Build compute layer URL
    compute = node.compute
    spice_console_compute_url = (
        f"{websocket.url.scheme}://{compute.host}:{compute.port}"
        f"/v3/compute/projects/{node.project.id}/{node.node_type}/nodes/{node.id}/console/spice"
    )

    # 3. Forward to compute layer
    async def spice_receive(spice_console_compute):
        """Client → Compute"""
        while True:
            data = await websocket.receive_bytes()
            await spice_console_compute.send_bytes(data)

    async with HTTPClient.get_client().ws_connect(
        spice_console_compute_url,
        auth=aiohttp.BasicAuth(user, password),
        ssl_context=ssl_context
    ) as ws:
        asyncio.ensure_future(spice_receive(ws))
        async for msg in ws:
            if msg.type == aiohttp.WSMsgType.BINARY:
                await websocket.send_bytes(msg.data)  # Compute → Client

Authentication Flow

Compute Layer Authentication

File: gns3server/api/routes/compute/dependencies/authentication.py

async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]:
    server_settings = Config.instance().settings.Server

    # Accept connection first
    await websocket.accept()

    # Skip auth if disabled
    if not server_settings.enable_http_auth:
        return websocket

    # Extract Authorization header
    authorization = websocket.headers.get("Authorization")
    scheme, param = get_authorization_scheme_param(authorization)

    # Validate Basic Auth
    if scheme.lower() != "basic":
        await websocket.close(code=1008)
        return websocket

    # Decode and verify credentials
    data = base64.b64decode(param).decode("ascii")
    username, separator, password = data.partition(":")

    username_valid = secrets.compare_digest(username, server_settings.compute_username)
    password_valid = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())

    if not (username_valid and password_valid):
        await websocket.close(code=1008)
        return websocket

    return websocket

Controller Layer Authentication

# WebSocket dependency for privilege checking
async def has_privilege_on_websocket(privilege_id: str):
    async def _has_privilege(websocket: WebSocket):
        # 1. Extract and verify JWT token
        token = websocket.query_params.get("ticket")
        current_user = decode_jwt_token(token)

        # 2. Check privilege
        if not check_privilege(current_user, privilege_id):
            await websocket.close(code=1008)
            return None

        return current_user

    return _has_privilege

Usage Examples

JavaScript Client (noVNC)

// Connect to VNC console
const host = "gns3-server.example.com";
const port = 3080;  // GNS3 controller port
const projectId = "uuid";
const nodeId = "uuid";
const token = "jwt_token";  // User authentication token

const vncUrl = `wss://${host}:${port}/v3/controller/projects/${projectId}/qemu/nodes/${nodeId}/console/vnc?ticket=${token}`;

const rfb = new RFB({
    target: document.getElementById('vnc-canvas'),
    url: vncUrl,
    credentials: { password: '' }  // VNC password (if configured)
});

rfb.addEventListener("connect", () => {
    console.log("Connected to VNC console");
});

rfb.addEventListener("disconnect", () => {
    console.log("Disconnected from VNC console");
});

JavaScript Client (spice-html5)

// Connect to SPICE console
const host = "gns3-server.example.com";
const port = 3080;
const projectId = "uuid";
const nodeId = "uuid";
const token = "jwt_token";

const spiceUrl = `wss://${host}:${port}/v3/controller/projects/${projectId}/qemu/nodes/${nodeId}/console/spice?ticket=${token}`;

const client = new SpiceHtml5.Client({
    uri: spiceUrl,
    screenId: 'spice-screen',
    password: '',  // SPICE password (if configured)
    tls: true  // Use TLS
});

client.addEventListener('connect', () => {
    console.log("Connected to SPICE console");
});

client.connect();

Python Client (websockets)

import asyncio
import websockets
import base64

async def connect_vnc_console(host, port, project_id, node_id, username, password):
    # Compute endpoint URL
    url = f"ws://{host}:{port}/v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/vnc"

    # HTTP Basic Auth
    credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
    headers = {"Authorization": f"Basic {credentials}"}

    async with websockets.connect(url, extra_headers=headers) as ws:
        # Send/receive VNC binary data
        while True:
            data = await ws.recv()
            # Process VNC protocol data...

asyncio.run(connect_vnc_console("localhost", 3080, "...", "...", "admin", "password"))

cURL (WebSocket Test)

# Test WebSocket connection (using websocat)
websocat \
  -H "Authorization: Basic $(echo -n 'admin:password' | base64)" \
  ws://localhost:3080/v3/compute/projects/{project_id}/qemu/nodes/{node_id}/console/vnc

Comparison: Old vs New Architecture

Old Architecture (websockify standalone)

Browser → websockify process 1 → VNC TCP (5900)
         → websockify process 2 → VNC TCP (5901)
         → websockify process N → VNC TCP (590N)

Issues:
- Each node requires separate websockify process
- Each node consumes additional WebSocket port
- No authentication on websockify port
- Complex process lifecycle management
- Resource intensive (many processes)

New Architecture (GNS3 API WebSocket)

Browser → GNS3 Controller WebSocket (single port)
         → GNS3 Compute WebSocket
         → Internal TCP bridge to VNC/SPICE

Advantages:
- Single WebSocket endpoint
- Integrated authentication
- No external processes
- Consistent with telnet console
- Lower resource footprint

Configuration

Server Settings

File: gns3server/config.py

Server:
  # Enable/disable compute authentication
  enable_http_auth: true

  # Compute credentials (for controller → compute auth)
  compute_username: "admin"
  compute_password: "password"

  # Console host configuration
  console_host: "0.0.0.0"  # or "::" for IPv6

QEMU VM Console Configuration

# Start QEMU VM with SPICE console
node = await qemu_manager.create_node(
    project_id=project_id,
    name="ubuntu-vm",
    console_type="spice+agent",  # SPICE with guest agent
    console=5900,  # SPICE port
    # ... other QEMU parameters
)

Resulting QEMU Command:

qemu-system-x86_64 \
  -spice addr=0.0.0.0,port=5900,disable-ticketing \
  -vga qxl \
  -device virtio-serial \
  -chardev spicevmc,id=vdagent,debug=0,name=vdagent \
  -device virtserialport,chardev=vdagent,name=com.redhat.spice.0 \
  # ... other QEMU options

Docker Container Console Configuration

# Start Docker container with VNC console
node = await docker_manager.create_node(
    project_id=project_id,
    name="ubuntu-container",
    image="ubuntu:latest",
    console_type="vnc",
    console=5900,
    console_resolution="1280x720",
    # ... other Docker parameters
)

Result:

  • TigerVNC server starts in container
  • VNC server listens on port 5900
  • WebSocket endpoint available at /console/vnc

Performance Considerations

Buffer Sizes

  • VNC frames: Up to 65536 bytes per read (larger for graphics)
  • SPICE data: Up to 65536 bytes per read
  • WebSocket → TCP: Unbuffered (immediate write)
  • TCP → WebSocket: Buffered reads for efficiency

Concurrent Connections

  • Multiple clients: Supported (like telnet console)
  • Broadcast behavior: Each WebSocket connection is independent
  • Resource usage: Linear with number of concurrent connections

Network Bandwidth

Console Type Typical Bandwidth Notes
Telnet < 1 KB/s Text only
VNC 1-10 MB/s Depends on screen changes
SPICE 1-20 MB/s Variable compression, can be higher with video

Troubleshooting

Common Issues

1. Connection Refused

Symptom: WebSocket connection to 'ws://...' failed: Error in connection establishment

Possible Causes:

  • Node not started
  • Wrong console type configured
  • Firewall blocking port

Solution:

# Check node status
curl -X GET http://localhost:3080/v3/controller/projects/{project_id}/qemu/nodes/{node_id}

# Verify console type
curl -X GET http://localhost:3080/v3/controller/projects/{project_id}/qemu/nodes/{node_id} | jq '.console_type'

2. Authentication Failed

Symptom: WebSocket closes with code 1008

Possible Causes:

  • Invalid credentials
  • Missing Authorization header
  • enable_http_auth enabled but no credentials provided

Solution:

# Verify compute credentials
# In gns3_server configuration
Server.compute_username = "admin"
Server.compute_password = "password"

# Test with curl
curl -v \
  -H "Authorization: Basic $(echo -n 'admin:password' | base64)" \
  http://localhost:3080/v3/compute/projects/{project_id}/qemu/nodes/{node_id}

3. Console Type Mismatch

Symptom: Node console type is not vnc or Node console type is not SPICE

Possible Causes:

  • Node configured with different console type
  • Wrong WebSocket endpoint used

Solution:

# Check actual console type
curl http://localhost:3080/v3/controller/projects/{project_id}/qemu/nodes/{node_id} | jq '.console_type'

# Use correct endpoint
# - If console_type == "vnc": /console/vnc
# - If console_type == "spice" or "spice+agent": /console/spice

4. SPICE Not Working

Symptom: Cannot connect to SPICE console

Possible Causes:

  • QEMU not built with SPICE support
  • SPICE port already in use
  • Node not started

Solution:

# Check QEMU SPICE support
qemu-system-x86_64 --help | grep spice

# Check if SPICE port is listening
netstat -tuln | grep 5900

# Check QEMU process
ps aux | grep qemu-system | grep spice

Testing

Unit Tests

Test VNC WebSocket forwarding:

async def test_vnc_websocket_console():
    # Create QEMU node with VNC console
    node = QemuVM(
        name="test-vm",
        console_type="vnc",
        console=5900,
        # ...
    )

    # Start node
    await node.start()

    # Create mock WebSocket
    websocket = MockWebSocket()

    # Connect
    await node.start_vnc_websocket_console(websocket)

    # Verify data forwarding
    assert websocket.sent_data  # VNC data received

Integration Tests

Test end-to-end VNC console:

# 1. Start GNS3 server
gns3server --local

# 2. Create project with QEMU VM
curl -X POST http://localhost:3080/v3/controller/projects \
  -H "Content-Type: application/json" \
  -d '{"name": "test-project"}'

# 3. Start QEMU VM with VNC
# (via API or GUI)

# 4. Connect WebSocket
websocat -H "Authorization: Basic ..." \
  ws://localhost:3080/v3/compute/projects/{id}/qemu/nodes/{id}/console/vnc

# 5. Send VNC handshake
# (requires RFB protocol client)

Future Enhancements

Potential Improvements

  1. WebSocket Compression

    • Enable permessage-deflate compression
    • Reduce bandwidth for text-heavy consoles
  2. Session Recording

    • Record VNC/SPICE sessions for playback
    • Useful for training and debugging
  3. Clipboard Integration

    • SPICE vdagent already supports clipboard
    • Could expose via WebSocket extensions
  4. File Transfer

    • SPICE supports folder sharing via webdav
    • Could integrate with GNS3 file management
  5. Audio Support

    • SPICE supports audio streaming
    • Could add WebSocket audio forwarding

Security Considerations

Authentication

  • Controller Layer: JWT token + RBAC privilege check
  • Compute Layer: HTTP Basic Auth
  • No standalone websockify: Single authentication point
  • Old websockify approach: No authentication on websockify port

Authorization

  • Privilege Required: Node.Console
  • Project Isolation: Users can only access consoles in projects they have access to
  • Node Access Control: Checked via dep_node dependency

Transport Security

  • WebSocket (ws://): Unencrypted, for development
  • WebSocket Secure (wss://): Encrypted, recommended for production
  • SSL/TLS: Configured via GNS3 server SSL settings

Console Protocol Security

  • VNC: Uses RFB protocol, supports password authentication
  • SPICE: Supports ticket-based authentication (disable-ticketing in current implementation)
  • Recommendation: Enable VNC/SPICE passwords for production

References

Files Modified

File Description
gns3server/compute/base_node.py Add start_vnc_websocket_console() and start_spice_websocket_console() methods
gns3server/api/routes/compute/qemu_nodes.py Add /{node_id}/console/vnc and /{node_id}/console/spice endpoints
gns3server/api/routes/compute/docker_nodes.py Add /{node_id}/console/vnc and /{node_id}/console/spice endpoints
gns3server/api/routes/controller/nodes.py Add controller layer WebSocket proxy endpoints

Commits

  • ba92b405 - feat: add VNC WebSocket console support for Docker and QEMU nodes
  • Current implementation extends this with SPICE support

External Libraries


Migration Guide

From Standalone websockify

Old Approach (not recommended):

# Start websockify for each node
websockify 10001 localhost:5900  # Process 1
websockify 10002 localhost:5901  # Process 2
websockify 10003 localhost:5902  # Process 3

New Approach (recommended):

# No websockify processes needed
# Just use GNS3 WebSocket API
WS /v3/controller/projects/{id}/qemu/nodes/{id}/console/vnc
WS /v3/controller/projects/{id}/qemu/nodes/{id}/console/spice

Client Migration:

// Old: Connect directly to websockify
const url = `ws://server:${websockify_port}/`;

// New: Connect through GNS3 API
const url = `wss://server:3080/v3/controller/projects/${projectId}/qemu/nodes/${nodeId}/console/vnc?ticket=${token}`;

Changelog

Version 1.0 (2026-03-16)

Added:

  • VNC WebSocket console support for QEMU and Docker nodes
  • SPICE WebSocket console support for QEMU nodes
  • Controller layer WebSocket proxy endpoints
  • Compute layer WebSocket endpoints with authentication
  • Bidirectional binary data forwarding in BaseNode

Changed:

  • N/A (new feature)

Deprecated:

  • Standalone websockify approach (still functional but not recommended)

Fixed:

  • N/A (new feature)