mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-14 05:59:43 +03:00
feat: add VNC WebSocket console support for Docker and QEMU nodes
Add VNC console WebSocket endpoints for Docker and QEMU nodes: - Add /console/vnc WebSocket endpoint to compute API (Docker & QEMU) - Add /console/vnc WebSocket endpoint to controller API - Implement start_vnc_websocket_console() in BaseNode - Forward VNC WebSocket traffic between controller and compute layers The implementation provides bidirectional WebSocket to TCP forwarding for VNC protocol (RFB) connections, allowing browser-based VNC console access to containers and VMs. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
09aa12cf94
commit
ba92b405da
@ -384,6 +384,21 @@ async def console_ws(
|
|||||||
await node.start_websocket_console(websocket)
|
await node.start_websocket_console(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket(
|
||||||
|
"/{node_id}/console/vnc"
|
||||||
|
)
|
||||||
|
async def vnc_console_ws(
|
||||||
|
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||||
|
node: DockerVM = Depends(dep_node)
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
VNC Console WebSocket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if websocket:
|
||||||
|
await node.start_vnc_websocket_console(websocket)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/{node_id}/console/reset",
|
"/{node_id}/console/reset",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
|||||||
@ -27,6 +27,7 @@ from typing import Union
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from gns3server import schemas
|
from gns3server import schemas
|
||||||
|
from gns3server.compute import qemu
|
||||||
from gns3server.compute.qemu import Qemu
|
from gns3server.compute.qemu import Qemu
|
||||||
from gns3server.compute.qemu.qemu_vm import QemuVM
|
from gns3server.compute.qemu.qemu_vm import QemuVM
|
||||||
|
|
||||||
@ -414,6 +415,21 @@ async def console_ws(
|
|||||||
await node.start_websocket_console(websocket)
|
await node.start_websocket_console(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket(
|
||||||
|
"/{node_id}/console/vnc"
|
||||||
|
)
|
||||||
|
async def vnc_console_ws(
|
||||||
|
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||||
|
node: QemuVM = Depends(dep_node)
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
VNC Console WebSocket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if websocket:
|
||||||
|
await node.start_vnc_websocket_console(websocket)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/{node_id}/console/reset",
|
"/{node_id}/console/reset",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
|||||||
@ -646,6 +646,80 @@ async def ws_console(
|
|||||||
except aiohttp.ClientError as e:
|
except aiohttp.ClientError as e:
|
||||||
log.error(f"Client error received when forwarding to compute console WebSocket: {e}")
|
log.error(f"Client error received when forwarding to compute console WebSocket: {e}")
|
||||||
|
|
||||||
|
@router.websocket("/{node_id}/console/vnc")
|
||||||
|
async def vnc_console(
|
||||||
|
websocket: WebSocket,
|
||||||
|
current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
|
||||||
|
node: Node = Depends(dep_node)
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
VNC WebSocket console.
|
||||||
|
|
||||||
|
Required privilege: Node.Console
|
||||||
|
"""
|
||||||
|
|
||||||
|
if current_user is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
compute = node.compute
|
||||||
|
log.info(
|
||||||
|
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller VNC console WebSocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_host = compute.host
|
||||||
|
try:
|
||||||
|
# handle IPv6 address
|
||||||
|
ip = ipaddress.ip_address(compute_host)
|
||||||
|
if isinstance(ip, ipaddress.IPv6Address):
|
||||||
|
compute_host = '[' + compute_host + ']'
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
vnc_console_compute_url = (
|
||||||
|
f"{websocket.url.scheme}://{compute_host}:{compute.port}/v3/compute/projects/"
|
||||||
|
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/vnc"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def vnc_receive(vnc_console_compute):
|
||||||
|
"""
|
||||||
|
Receive binary WebSocket data from client and forward to compute VNC console WebSocket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_bytes()
|
||||||
|
if data:
|
||||||
|
await vnc_console_compute.send_bytes(data)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
await vnc_console_compute.close()
|
||||||
|
log.info(
|
||||||
|
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||||
|
f" VNC console WebSocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# receive binary data from compute VNC console WebSocket and forward to client
|
||||||
|
log.info(f"Forwarding VNC console WebSocket to '{vnc_console_compute_url}'")
|
||||||
|
server_config = Config.instance().settings.Server
|
||||||
|
user = server_config.compute_username
|
||||||
|
password = server_config.compute_password
|
||||||
|
if not user:
|
||||||
|
raise ControllerForbiddenError("Compute username is not set")
|
||||||
|
user = user.strip()
|
||||||
|
if user and password:
|
||||||
|
auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8")
|
||||||
|
else:
|
||||||
|
auth = aiohttp.BasicAuth(user, "")
|
||||||
|
ssl_context = Controller.instance().ssl_context()
|
||||||
|
async with HTTPClient.get_client().ws_connect(vnc_console_compute_url, auth=auth, ssl_context=ssl_context) as ws:
|
||||||
|
asyncio.ensure_future(vnc_receive(ws))
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.BINARY:
|
||||||
|
await websocket.send_bytes(msg.data)
|
||||||
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
|
break
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
log.error(f"Client error received when forwarding to compute VNC console WebSocket: {e}")
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/console/reset",
|
"/console/reset",
|
||||||
|
|||||||
@ -484,7 +484,7 @@ class BaseNode:
|
|||||||
"""
|
"""
|
||||||
Connect to console using Websocket.
|
Connect to console using Websocket.
|
||||||
|
|
||||||
:param ws: Websocket object
|
:param websocket: FastAPI WebSocket object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
@ -541,6 +541,76 @@ class BaseNode:
|
|||||||
for task in pending:
|
for task in pending:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
|
async def start_vnc_websocket_console(self, websocket):
|
||||||
|
"""
|
||||||
|
Connect to VNC console using WebSocket.
|
||||||
|
|
||||||
|
:param websocket: FastAPI WebSocket object
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute "
|
||||||
|
f"VNC console WebSocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
try:
|
||||||
|
vnc_reader, vnc_writer = await asyncio.open_connection(
|
||||||
|
self._manager.port_manager.console_host,
|
||||||
|
self.console # VNC port
|
||||||
|
)
|
||||||
|
log.info(f"Connected to VNC server {self._manager.port_manager.console_host}:{self.console}")
|
||||||
|
except ConnectionError as e:
|
||||||
|
await websocket.close(code=1000)
|
||||||
|
raise NodeError(f"Cannot connect to node {self.name} VNC server: {e}")
|
||||||
|
|
||||||
|
async def ws_forward(vnc_writer):
|
||||||
|
# Browser → VNC: Forward binary WebSocket data to VNC server
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_bytes()
|
||||||
|
if data:
|
||||||
|
vnc_writer.write(data)
|
||||||
|
await vnc_writer.drain()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
log.info(
|
||||||
|
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute "
|
||||||
|
f"VNC console WebSocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def vnc_forward(vnc_reader):
|
||||||
|
# VNC → Browser: Forward VNC frames to WebSocket
|
||||||
|
try:
|
||||||
|
while not vnc_reader.at_eof():
|
||||||
|
data = await vnc_reader.read(65536) # Larger buffer for VNC frames
|
||||||
|
if data:
|
||||||
|
await websocket.send_bytes(data)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Exception while forwarding VNC data to WebSocket: {e}")
|
||||||
|
|
||||||
|
# Keep forwarding WebSocket data in both directions
|
||||||
|
if sys.version_info >= (3, 11, 0):
|
||||||
|
# Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden.
|
||||||
|
aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))]
|
||||||
|
else:
|
||||||
|
aws = [ws_forward(vnc_writer), vnc_forward(vnc_reader)]
|
||||||
|
|
||||||
|
done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
for task in done:
|
||||||
|
if task.exception():
|
||||||
|
log.warning(f"Exception while forwarding WebSocket data to VNC server: {task.exception()}")
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
vnc_writer.close()
|
||||||
|
await vnc_writer.wait_closed()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aux(self):
|
def aux(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user