mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-29 03:32:20 +02: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")
|
raise NodeError("uBridge requires root access or the capability to interact with network adapters")
|
||||||
|
|
||||||
server_host = self._manager.config.settings.Server.host
|
server_host = self._manager.config.settings.Server.host
|
||||||
|
transport = self._manager.config.settings.Server.ubridge_control_transport
|
||||||
if not self.ubridge:
|
if not self.ubridge:
|
||||||
self._ubridge_hypervisor = Hypervisor(self._project, self.ubridge_path, self.working_dir, server_host)
|
self._ubridge_hypervisor = Hypervisor(
|
||||||
log.info(f"Starting new uBridge hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port}")
|
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()
|
await self._ubridge_hypervisor.start()
|
||||||
if self._ubridge_hypervisor:
|
if self._ubridge_hypervisor:
|
||||||
log.info(
|
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()
|
await self._ubridge_hypervisor.connect()
|
||||||
# Tell this uBridge where to send MARK signals and which node id to
|
# 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():
|
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()
|
await self._ubridge_hypervisor.stop()
|
||||||
self._ubridge_hypervisor = None
|
self._ubridge_hypervisor = None
|
||||||
|
|
||||||
|
|||||||
@ -20,9 +20,10 @@ Represents a uBridge hypervisor and starts/stops the associated uBridge process.
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import asyncio
|
import asyncio
|
||||||
import socket
|
import tempfile
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from gns3server.utils import parse_version
|
from gns3server.utils import parse_version
|
||||||
@ -44,17 +45,36 @@ class Hypervisor(UBridgeHypervisor):
|
|||||||
:param project: Project instance
|
:param project: Project instance
|
||||||
:param path: path to uBridge executable
|
:param path: path to uBridge executable
|
||||||
:param working_dir: working directory
|
:param working_dir: working directory
|
||||||
:param host: host/address for this hypervisor
|
:param transport: control channel transport — "unix" (-U) or "tcp" (-H)
|
||||||
:param port: port for this hypervisor
|
: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:
|
try:
|
||||||
port = None
|
|
||||||
info = socket.getaddrinfo(host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
|
info = socket.getaddrinfo(host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
|
||||||
if not info:
|
if not info:
|
||||||
raise UbridgeError(f"getaddrinfo returns an empty list on {host}")
|
raise UbridgeError(f"getaddrinfo returns an empty list on {host}")
|
||||||
@ -68,11 +88,8 @@ class Hypervisor(UBridgeHypervisor):
|
|||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UbridgeError(f"Could not find free port for the uBridge hypervisor: {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._command = []
|
||||||
self._process = None
|
self._process = None
|
||||||
self._stdout_file = ""
|
self._stdout_file = ""
|
||||||
@ -214,6 +231,16 @@ class Hypervisor(UBridgeHypervisor):
|
|||||||
os.remove(self._stdout_file)
|
os.remove(self._stdout_file)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.warning(f"could not delete temporary uBridge log file: {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._process = None
|
||||||
self._started = False
|
self._started = False
|
||||||
|
|
||||||
@ -250,7 +277,10 @@ class Hypervisor(UBridgeHypervisor):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
command = [self._path]
|
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:
|
if log.getEffectiveLevel() == logging.DEBUG:
|
||||||
command.extend(["-d", "1"])
|
command.extend(["-d", "1"])
|
||||||
return command
|
return command
|
||||||
|
|||||||
@ -28,20 +28,29 @@ log = logging.getLogger(__name__)
|
|||||||
class UBridgeHypervisor:
|
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
|
Two transports, selected by which argument is set:
|
||||||
:param port: the tcp port integer
|
* ``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
|
: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
|
# Used to parse Ubridge response codes
|
||||||
error_re = re.compile(r"""^2[0-9]{2}-""")
|
error_re = re.compile(r"""^2[0-9]{2}-""")
|
||||||
success_re = re.compile(r"""^1[0-9]{2}\s{1}""")
|
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._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
self._version = "N/A"
|
self._version = "N/A"
|
||||||
@ -54,22 +63,23 @@ class UBridgeHypervisor:
|
|||||||
Connects to the hypervisor.
|
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()
|
begin = time.time()
|
||||||
connection_success = False
|
connection_success = False
|
||||||
last_exception = None
|
last_exception = None
|
||||||
while time.time() - begin < timeout:
|
while time.time() - begin < timeout:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
try:
|
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:
|
except OSError as e:
|
||||||
last_exception = e
|
last_exception = e
|
||||||
continue
|
continue
|
||||||
@ -77,9 +87,9 @@ class UBridgeHypervisor:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not connection_success:
|
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:
|
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:
|
try:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
@ -122,7 +132,7 @@ class UBridgeHypervisor:
|
|||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
self._writer.close()
|
self._writer.close()
|
||||||
except OSError as e:
|
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
|
self._reader = self._writer = None
|
||||||
|
|
||||||
async def reset(self):
|
async def reset(self):
|
||||||
@ -133,44 +143,17 @@ class UBridgeHypervisor:
|
|||||||
await self.send("hypervisor reset")
|
await self.send("hypervisor reset")
|
||||||
|
|
||||||
@property
|
@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
|
if self._socket_path:
|
||||||
|
return self._socket_path
|
||||||
@port.setter
|
return f"{self._host}:{self._port}"
|
||||||
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
|
|
||||||
|
|
||||||
@locking
|
@locking
|
||||||
async def send(self, command):
|
async def send(self, command):
|
||||||
@ -205,8 +188,8 @@ class UBridgeHypervisor:
|
|||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UbridgeError(
|
raise UbridgeError(
|
||||||
"Lost communication with {host}:{port} when sending command '{command}': {error}, uBridge process running: {run}".format(
|
"Lost communication with {endpoint} when sending command '{command}': {error}, uBridge process running: {run}".format(
|
||||||
host=self._host, port=self._port, command=command, error=e, run=self.is_running()
|
endpoint=self.endpoint, command=command, error=e, run=self.is_running()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -232,8 +215,8 @@ class UBridgeHypervisor:
|
|||||||
if not chunk:
|
if not chunk:
|
||||||
if retries > max_retries:
|
if retries > max_retries:
|
||||||
raise UbridgeError(
|
raise UbridgeError(
|
||||||
"No data returned from {host}:{port} after sending command '{command}', uBridge process running: {run}".format(
|
"No data returned from {endpoint} after sending command '{command}', uBridge process running: {run}".format(
|
||||||
host=self._host, port=self._port, command=command, run=self.is_running()
|
endpoint=self.endpoint, command=command, run=self.is_running()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -244,8 +227,8 @@ class UBridgeHypervisor:
|
|||||||
buf += chunk.decode("utf-8")
|
buf += chunk.decode("utf-8")
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UbridgeError(
|
raise UbridgeError(
|
||||||
"Lost communication with {host}:{port} after sending command '{command}': {error}, uBridge process running: {run}".format(
|
"Lost communication with {endpoint} after sending command '{command}': {error}, uBridge process running: {run}".format(
|
||||||
host=self._host, port=self._port, command=command, error=e, run=self.is_running()
|
endpoint=self.endpoint, command=command, error=e, run=self.is_running()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -255,8 +238,8 @@ class UBridgeHypervisor:
|
|||||||
continue
|
continue
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise UbridgeError(
|
raise UbridgeError(
|
||||||
"Could not communicate with {host}:{port} after sending command '{command}', uBridge process running: {run}".format(
|
"Could not communicate with {endpoint} after sending command '{command}', uBridge process running: {run}".format(
|
||||||
host=self._host, port=self._port, command=command, run=self.is_running()
|
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 executable location, default: search in PATH
|
||||||
;ubridge_path = ubridge
|
;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
|
; Marker (traffic-insight) UDP sink: one listener per compute process that
|
||||||
; receives uBridge MARK signals from every uBridge on this host.
|
; receives uBridge MARK signals from every uBridge on this host.
|
||||||
; marker_listen_host defaults to 127.0.0.1 because uBridge runs locally.
|
; marker_listen_host defaults to 127.0.0.1 because uBridge runs locally.
|
||||||
|
|||||||
@ -113,6 +113,16 @@ class ServerProtocol(str, Enum):
|
|||||||
https = "https"
|
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):
|
class BuiltinSymbolTheme(str, Enum):
|
||||||
|
|
||||||
classic = "Classic"
|
classic = "Classic"
|
||||||
@ -154,6 +164,10 @@ class ServerSettings(BaseModel):
|
|||||||
udp_start_port_range: int = Field(10000, gt=0, le=65535)
|
udp_start_port_range: int = Field(10000, gt=0, le=65535)
|
||||||
udp_end_port_range: int = Field(30000, gt=0, le=65535)
|
udp_end_port_range: int = Field(30000, gt=0, le=65535)
|
||||||
ubridge_path: str = "ubridge"
|
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
|
# Marker (traffic-insight) UDP sink: one listener per compute process that
|
||||||
# receives ubridge MARK signals from every ubridge on this host. The host
|
# 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.
|
# defaults to loopback because ubridge runs on the same host as the compute.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user