mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
Add configurable uBridge control channel transport (tcp/unix)
Add ubridge_control_transport to [Server] config (default "tcp", fully backward compatible). Selecting "unix" switches the uBridge hypervisor control channel from the unauthenticated TCP listener (-H) to an AF_UNIX socket (-U) authenticated in-kernel via SO_PEERCRED. - UBridgeHypervisor: supports both socket_path (AF_UNIX) and host/port (TCP); a new `endpoint` property unifies log/error strings for both - Hypervisor: unix mode allocates a short socket path under a 0700 private runtime dir and unlinks it on stop; tcp mode restores the original getaddrinfo ephemeral-port allocation - base_node: reads the transport from config and passes host through - schemas/config.py + config_samples/gns3_server.conf: new option Default deployments are unchanged. "unix" requires a ubridge build that understands -U.
This commit is contained in:
parent
deba9f8409
commit
3d3e20f69e
@ -926,13 +926,16 @@ class BaseNode:
|
||||
raise NodeError("uBridge requires root access or the capability to interact with network adapters")
|
||||
|
||||
server_host = self._manager.config.settings.Server.host
|
||||
transport = self._manager.config.settings.Server.ubridge_control_transport
|
||||
if not self.ubridge:
|
||||
self._ubridge_hypervisor = Hypervisor(self._project, self.ubridge_path, self.working_dir, server_host)
|
||||
log.info(f"Starting new uBridge hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port}")
|
||||
self._ubridge_hypervisor = Hypervisor(
|
||||
self._project, self.ubridge_path, self.working_dir, transport, server_host
|
||||
)
|
||||
log.info(f"Starting new uBridge hypervisor at {self._ubridge_hypervisor.endpoint}")
|
||||
await self._ubridge_hypervisor.start()
|
||||
if self._ubridge_hypervisor:
|
||||
log.info(
|
||||
f"Hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port} has successfully started"
|
||||
f"Hypervisor at {self._ubridge_hypervisor.endpoint} has successfully started"
|
||||
)
|
||||
await self._ubridge_hypervisor.connect()
|
||||
# Tell this uBridge where to send MARK signals and which node id to
|
||||
@ -981,7 +984,7 @@ class BaseNode:
|
||||
"""
|
||||
|
||||
if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running():
|
||||
log.info(f"Stopping uBridge hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port}")
|
||||
log.info(f"Stopping uBridge hypervisor at {self._ubridge_hypervisor.endpoint}")
|
||||
await self._ubridge_hypervisor.stop()
|
||||
self._ubridge_hypervisor = None
|
||||
|
||||
|
||||
@ -20,9 +20,10 @@ Represents a uBridge hypervisor and starts/stops the associated uBridge process.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import asyncio
|
||||
import socket
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
from gns3server.utils import parse_version
|
||||
@ -44,17 +45,36 @@ class Hypervisor(UBridgeHypervisor):
|
||||
:param project: Project instance
|
||||
:param path: path to uBridge executable
|
||||
:param working_dir: working directory
|
||||
:param host: host/address for this hypervisor
|
||||
:param port: port for this hypervisor
|
||||
:param transport: control channel transport — "unix" (-U) or "tcp" (-H)
|
||||
:param host: host/address for the TCP transport (unused for "unix")
|
||||
"""
|
||||
|
||||
_instance_count = 1
|
||||
_instance_count = 0
|
||||
|
||||
def __init__(self, project, path, working_dir, host, port=None):
|
||||
def __init__(self, project, path, working_dir, transport, host=None):
|
||||
|
||||
if port is None:
|
||||
self._project = project
|
||||
self._path = path
|
||||
self._working_dir = working_dir
|
||||
|
||||
if transport == "unix":
|
||||
# AF_UNIX control socket (-U). sun_path is capped at 107 bytes, so
|
||||
# keep it under a private runtime dir — never under the project tree
|
||||
# (per-node UUIDs would overflow it).
|
||||
Hypervisor._instance_count += 1
|
||||
runtime_dir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir()
|
||||
socket_dir = os.path.join(runtime_dir, "gns3")
|
||||
try:
|
||||
os.makedirs(socket_dir, mode=0o700, exist_ok=True)
|
||||
os.chmod(socket_dir, 0o700)
|
||||
except OSError as e:
|
||||
raise UbridgeError(f"Could not create uBridge socket directory {socket_dir}: {e}")
|
||||
socket_path = os.path.join(socket_dir, f"ubridge-{Hypervisor._instance_count}.sock")
|
||||
super().__init__(socket_path=socket_path)
|
||||
else:
|
||||
# TCP control channel (-H): let the OS find an unused local port.
|
||||
port = None
|
||||
try:
|
||||
port = None
|
||||
info = socket.getaddrinfo(host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
|
||||
if not info:
|
||||
raise UbridgeError(f"getaddrinfo returns an empty list on {host}")
|
||||
@ -68,11 +88,8 @@ class Hypervisor(UBridgeHypervisor):
|
||||
break
|
||||
except OSError as e:
|
||||
raise UbridgeError(f"Could not find free port for the uBridge hypervisor: {e}")
|
||||
super().__init__(host=host, port=port)
|
||||
|
||||
super().__init__(host, port)
|
||||
self._project = project
|
||||
self._path = path
|
||||
self._working_dir = working_dir
|
||||
self._command = []
|
||||
self._process = None
|
||||
self._stdout_file = ""
|
||||
@ -214,6 +231,16 @@ class Hypervisor(UBridgeHypervisor):
|
||||
os.remove(self._stdout_file)
|
||||
except OSError as e:
|
||||
log.warning(f"could not delete temporary uBridge log file: {e}")
|
||||
|
||||
# ubridge unlinks its AF_UNIX control socket on a clean exit; for the
|
||||
# unix transport remove it here too so a killed process leaves no stale
|
||||
# socket behind. The TCP transport has no socket_path.
|
||||
if self._socket_path:
|
||||
try:
|
||||
os.unlink(self._socket_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._process = None
|
||||
self._started = False
|
||||
|
||||
@ -250,7 +277,10 @@ class Hypervisor(UBridgeHypervisor):
|
||||
"""
|
||||
|
||||
command = [self._path]
|
||||
command.extend(["-H", f"{self._host}:{self._port}"])
|
||||
if self._socket_path:
|
||||
command.extend(["-U", self._socket_path])
|
||||
else:
|
||||
command.extend(["-H", f"{self._host}:{self._port}"])
|
||||
if log.getEffectiveLevel() == logging.DEBUG:
|
||||
command.extend(["-d", "1"])
|
||||
return command
|
||||
|
||||
@ -28,20 +28,29 @@ log = logging.getLogger(__name__)
|
||||
class UBridgeHypervisor:
|
||||
|
||||
"""
|
||||
Creates a new connection to uBridge hypervisor.
|
||||
Creates a new connection to a uBridge hypervisor control channel.
|
||||
|
||||
:param host: the hostname or ip address string of the uBridge hypervisor
|
||||
:param port: the tcp port integer
|
||||
Two transports, selected by which argument is set:
|
||||
* ``socket_path`` -> AF_UNIX (``-U``), authenticated in-kernel via
|
||||
SO_PEERCRED (ubridge accepts only its own UID; the compute process that
|
||||
spawned it shares that UID). Recommended on Linux.
|
||||
* ``host``/``port`` -> TCP (``-H``), retained for backward compatibility.
|
||||
|
||||
:param socket_path: path to the uBridge AF_UNIX control socket (None for TCP)
|
||||
:param host: TCP hostname/IP (None for AF_UNIX)
|
||||
:param port: TCP port
|
||||
:param timeout: timeout integer for how long to wait for a response to commands sent to the
|
||||
hypervisor (defaults to 30 seconds)
|
||||
hypervisor (defaults to 30 seconds)
|
||||
"""
|
||||
|
||||
# Used to parse Ubridge response codes
|
||||
error_re = re.compile(r"""^2[0-9]{2}-""")
|
||||
success_re = re.compile(r"""^1[0-9]{2}\s{1}""")
|
||||
|
||||
def __init__(self, host, port, timeout=30.0):
|
||||
def __init__(self, socket_path=None, host=None, port=None, timeout=30.0):
|
||||
|
||||
# Exactly one transport is active: socket_path (AF_UNIX) or host/port (TCP).
|
||||
self._socket_path = socket_path
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._version = "N/A"
|
||||
@ -54,22 +63,23 @@ class UBridgeHypervisor:
|
||||
Connects to the hypervisor.
|
||||
"""
|
||||
|
||||
# connect to a local address by default
|
||||
# if listening to all addresses (IPv4 or IPv6)
|
||||
if self._host == "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
elif self._host == "::":
|
||||
host = "::1"
|
||||
else:
|
||||
host = self._host
|
||||
|
||||
begin = time.time()
|
||||
connection_success = False
|
||||
last_exception = None
|
||||
while time.time() - begin < timeout:
|
||||
await asyncio.sleep(0.1)
|
||||
try:
|
||||
self._reader, self._writer = await asyncio.open_connection(host, self._port)
|
||||
if self._socket_path:
|
||||
self._reader, self._writer = await asyncio.open_unix_connection(self._socket_path)
|
||||
else:
|
||||
# connect to a local address by default if listening on all addresses
|
||||
if self._host == "0.0.0.0":
|
||||
host = "127.0.0.1"
|
||||
elif self._host == "::":
|
||||
host = "::1"
|
||||
else:
|
||||
host = self._host
|
||||
self._reader, self._writer = await asyncio.open_connection(host, self._port)
|
||||
except OSError as e:
|
||||
last_exception = e
|
||||
continue
|
||||
@ -77,9 +87,9 @@ class UBridgeHypervisor:
|
||||
break
|
||||
|
||||
if not connection_success:
|
||||
raise UbridgeError(f"Couldn't connect to hypervisor on {host}:{self._port} :{last_exception}")
|
||||
raise UbridgeError(f"Couldn't connect to hypervisor on {self.endpoint} :{last_exception}")
|
||||
else:
|
||||
log.info(f"Connected to uBridge hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds")
|
||||
log.info(f"Connected to uBridge hypervisor on {self.endpoint} after {time.time() - begin:.4f} seconds")
|
||||
|
||||
try:
|
||||
await asyncio.sleep(0.1)
|
||||
@ -122,7 +132,7 @@ class UBridgeHypervisor:
|
||||
await self._writer.drain()
|
||||
self._writer.close()
|
||||
except OSError as e:
|
||||
log.debug(f"Stopping hypervisor {self._host}:{self._port} {e}")
|
||||
log.debug(f"Stopping hypervisor {self.endpoint} {e}")
|
||||
self._reader = self._writer = None
|
||||
|
||||
async def reset(self):
|
||||
@ -133,44 +143,17 @@ class UBridgeHypervisor:
|
||||
await self.send("hypervisor reset")
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
def endpoint(self):
|
||||
"""
|
||||
Returns the port used to start the hypervisor.
|
||||
Returns a human-readable control endpoint: the AF_UNIX socket path when
|
||||
using -U, or host:port when using -H. Used for logging and errors.
|
||||
|
||||
:returns: port number (integer)
|
||||
:returns: endpoint (string)
|
||||
"""
|
||||
|
||||
return self._port
|
||||
|
||||
@port.setter
|
||||
def port(self, port):
|
||||
"""
|
||||
Sets the port used to start the hypervisor.
|
||||
|
||||
:param port: port number (integer)
|
||||
"""
|
||||
|
||||
self._port = port
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""
|
||||
Returns the host (binding) used to start the hypervisor.
|
||||
|
||||
:returns: host/address (string)
|
||||
"""
|
||||
|
||||
return self._host
|
||||
|
||||
@host.setter
|
||||
def host(self, host):
|
||||
"""
|
||||
Sets the host (binding) used to start the hypervisor.
|
||||
|
||||
:param host: host/address (string)
|
||||
"""
|
||||
|
||||
self._host = host
|
||||
if self._socket_path:
|
||||
return self._socket_path
|
||||
return f"{self._host}:{self._port}"
|
||||
|
||||
@locking
|
||||
async def send(self, command):
|
||||
@ -205,8 +188,8 @@ class UBridgeHypervisor:
|
||||
await self._writer.drain()
|
||||
except OSError as e:
|
||||
raise UbridgeError(
|
||||
"Lost communication with {host}:{port} when sending command '{command}': {error}, uBridge process running: {run}".format(
|
||||
host=self._host, port=self._port, command=command, error=e, run=self.is_running()
|
||||
"Lost communication with {endpoint} when sending command '{command}': {error}, uBridge process running: {run}".format(
|
||||
endpoint=self.endpoint, command=command, error=e, run=self.is_running()
|
||||
)
|
||||
)
|
||||
|
||||
@ -232,8 +215,8 @@ class UBridgeHypervisor:
|
||||
if not chunk:
|
||||
if retries > max_retries:
|
||||
raise UbridgeError(
|
||||
"No data returned from {host}:{port} after sending command '{command}', uBridge process running: {run}".format(
|
||||
host=self._host, port=self._port, command=command, run=self.is_running()
|
||||
"No data returned from {endpoint} after sending command '{command}', uBridge process running: {run}".format(
|
||||
endpoint=self.endpoint, command=command, run=self.is_running()
|
||||
)
|
||||
)
|
||||
else:
|
||||
@ -244,8 +227,8 @@ class UBridgeHypervisor:
|
||||
buf += chunk.decode("utf-8")
|
||||
except OSError as e:
|
||||
raise UbridgeError(
|
||||
"Lost communication with {host}:{port} after sending command '{command}': {error}, uBridge process running: {run}".format(
|
||||
host=self._host, port=self._port, command=command, error=e, run=self.is_running()
|
||||
"Lost communication with {endpoint} after sending command '{command}': {error}, uBridge process running: {run}".format(
|
||||
endpoint=self.endpoint, command=command, error=e, run=self.is_running()
|
||||
)
|
||||
)
|
||||
|
||||
@ -255,8 +238,8 @@ class UBridgeHypervisor:
|
||||
continue
|
||||
except IndexError:
|
||||
raise UbridgeError(
|
||||
"Could not communicate with {host}:{port} after sending command '{command}', uBridge process running: {run}".format(
|
||||
host=self._host, port=self._port, command=command, run=self.is_running()
|
||||
"Could not communicate with {endpoint} after sending command '{command}', uBridge process running: {run}".format(
|
||||
endpoint=self.endpoint, command=command, run=self.is_running()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -92,6 +92,11 @@ udp_end_port_range = 30000
|
||||
; uBridge executable location, default: search in PATH
|
||||
;ubridge_path = ubridge
|
||||
|
||||
; uBridge control channel transport: "tcp" (-H host:port, default) or "unix"
|
||||
; (-U socket_path; AF_UNIX + SO_PEERCRED, recommended on Linux for kernel-level
|
||||
; peer authentication). TCP now binds loopback by default.
|
||||
;ubridge_control_transport = tcp
|
||||
|
||||
; Marker (traffic-insight) UDP sink: one listener per compute process that
|
||||
; receives uBridge MARK signals from every uBridge on this host.
|
||||
; marker_listen_host defaults to 127.0.0.1 because uBridge runs locally.
|
||||
|
||||
@ -113,6 +113,16 @@ class ServerProtocol(str, Enum):
|
||||
https = "https"
|
||||
|
||||
|
||||
class UbridgeControlTransport(str, Enum):
|
||||
|
||||
# TCP control channel: -H host:port. ubridge now binds loopback by default,
|
||||
# so this is reachable only locally. Retained for backward compatibility.
|
||||
tcp = "tcp"
|
||||
# AF_UNIX control channel: -U socket_path, authenticated in-kernel via
|
||||
# SO_PEERCRED (ubridge accepts only its own UID). Recommended on Linux.
|
||||
unix = "unix"
|
||||
|
||||
|
||||
class BuiltinSymbolTheme(str, Enum):
|
||||
|
||||
classic = "Classic"
|
||||
@ -154,6 +164,10 @@ class ServerSettings(BaseModel):
|
||||
udp_start_port_range: int = Field(10000, gt=0, le=65535)
|
||||
udp_end_port_range: int = Field(30000, gt=0, le=65535)
|
||||
ubridge_path: str = "ubridge"
|
||||
# Transport for the uBridge hypervisor control channel. "tcp" (-H) is the
|
||||
# historical default; "unix" (-U, AF_UNIX + SO_PEERCRED) is recommended on
|
||||
# Linux for kernel-level peer authentication.
|
||||
ubridge_control_transport: UbridgeControlTransport = UbridgeControlTransport.tcp
|
||||
# Marker (traffic-insight) UDP sink: one listener per compute process that
|
||||
# receives ubridge MARK signals from every ubridge on this host. The host
|
||||
# defaults to loopback because ubridge runs on the same host as the compute.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user