mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-14 14:05:20 +03:00
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
872 lines
24 KiB
Markdown
872 lines
24 KiB
Markdown
# 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**:
|
|
```http
|
|
Upgrade: websocket
|
|
Connection: Upgrade
|
|
Authorization: Basic <base64(username:password)>
|
|
```
|
|
|
|
**Response**:
|
|
- Binary WebSocket connection for VNC protocol (RFB)
|
|
|
|
**Implementation**:
|
|
```python
|
|
# 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**:
|
|
```http
|
|
Upgrade: websocket
|
|
Connection: Upgrade
|
|
Authorization: Basic <base64(username:password)>
|
|
```
|
|
|
|
**Response**:
|
|
- Binary WebSocket connection for SPICE protocol
|
|
|
|
**Implementation**:
|
|
```python
|
|
# 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**:
|
|
```json
|
|
{
|
|
"console_type": "spice+agent",
|
|
"console": 5900
|
|
}
|
|
```
|
|
|
|
**Docker Container**:
|
|
```json
|
|
{
|
|
"console_type": "vnc",
|
|
"console": 5900
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### BaseNode WebSocket Forwarding
|
|
|
|
**File**: `gns3server/compute/base_node.py`
|
|
|
|
#### VNC Console Implementation
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
@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`
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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)
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```python
|
|
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)
|
|
|
|
```bash
|
|
# 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`
|
|
|
|
```yaml
|
|
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
|
|
|
|
```python
|
|
# 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**:
|
|
```bash
|
|
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
|
|
|
|
```python
|
|
# 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**:
|
|
```bash
|
|
# 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**:
|
|
```python
|
|
# 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**:
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# 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**:
|
|
```python
|
|
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**:
|
|
```bash
|
|
# 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
|
|
|
|
### Related Documentation
|
|
|
|
- [Telnet Console Documentation](../bugs/telnet-server-connection-race-condition.md)
|
|
- [GNS3 API Documentation](./openapi.json)
|
|
- [noVNC Documentation](https://github.com/novnc/noVNC)
|
|
- [SPICE Protocol](https://www.spice-space.org/documentation.html)
|
|
|
|
### External Libraries
|
|
|
|
- **noVNC**: HTML5 VNC Client (https://github.com/novnc/noVNC)
|
|
- **spice-html5**: HTML5 SPICE Client (https://github.com/spice-html5/spice-html5)
|
|
|
|
---
|
|
|
|
## Migration Guide
|
|
|
|
### From Standalone websockify
|
|
|
|
**Old Approach** (not recommended):
|
|
```python
|
|
# 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):
|
|
```python
|
|
# 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**:
|
|
```javascript
|
|
// 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)
|