mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-13 05:55:35 +03:00
A VPCS node's single adapter anchors on a persistent TAP the server
creates (tap create + set_owner via uBridge) and the vpcs process
attaches to by name: -e -d <gp{id8}e0p0> -n, the same
server-creates/guest-attaches shape as QEMU's netdev. -n (a vpcs fork
feature) keeps the guest's ip command from ifconfig'ing the anchor on
the host; the vpcs build is probed once per binary identity
(vpcs_tap capability, usage scan) and gates kernel links together
with ubridge_tap — anything else keeps the node on the relay (the
legacy local UDP tunnel on a stock binary, an AF_PACKET endpoint on
the anchor with a capable one).
VPCSVM takes KernelDatapathMixin: _connect_nio dispatches kernel
(enslave into the per-link bridge) / relay-on-anchor / legacy,
captures pick capture start_kernel vs the bridge engine, stop runs
under an execute lock and terminates the process before deleting the
anchor TAPs (a held device refuses deletion), and the duplicate-port
guard backstops the controller's. The NIO routes accept nio_bridge
and write suspend back on update.
_delete_anchor_taps verifies each tap is gone from sysfs and retries
a bounded three times: a holder can release its fd a moment after its
process was reaped (vpcs's daemon parent kills its console child and
exits first), so the unconditional delete raced EBUSY and leaked the
anchor intermittently.
The vpcs side needs IFNAMSIZ (16) instead of its own IFNAMESIZ (12)
for the -d name buffer — anchor names are 14 chars and truncated to a
nonexistent device, which TUNSETIFF then tries to create without
CAP_NET_ADMIN (fixed in the vpcs fork).
e2e (tests/e2e/test_vpcs_kernel_datapath.py): kernel and relay
scenarios green against the real binary — real ICMP both ways, §E.1/§
E.2 on the anchors, delay/suspend/capture, link delete/re-create,
node stop/restart self-heal, zero leftovers; the relay control passes
with both the fork binary and a stock one.
797 lines
31 KiB
Python
797 lines
31 KiB
Python
#
|
|
# Copyright (C) 2015 GNS3 Technologies Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
VPCS VM management (creates command line, processes, files etc.) in
|
|
order to run a VPCS VM.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
|
|
from gns3server.utils import parse_version
|
|
from gns3server.utils.asyncio import monitor_process, subprocess_check_output, wait_for_process_termination
|
|
from gns3server.utils.kernel_anchor import kernel_anchor_name
|
|
|
|
from ..adapters.ethernet_adapter import EthernetAdapter
|
|
from ..base_node import BaseNode
|
|
from ..kernel_datapath import KernelDatapathMixin
|
|
from ..nios.nio_bridge import NIOBridge
|
|
from ..nios.nio_udp import NIOUDP
|
|
from .vpcs_error import VPCSError
|
|
from .vpcs_probe import probe_vpcs_tap
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class VPCSVM(KernelDatapathMixin, BaseNode):
|
|
module_name = "vpcs"
|
|
|
|
"""
|
|
VPCS VM implementation.
|
|
|
|
:param name: VPCS VM name
|
|
:param node_id: Node identifier
|
|
:param project: Project instance
|
|
:param manager: Manager instance
|
|
:param console: TCP console port
|
|
:param startup_script: content of the startup script file
|
|
"""
|
|
|
|
def __init__(self, name, node_id, project, manager, console=None, console_type="telnet", startup_script=None):
|
|
|
|
super().__init__(name, node_id, project, manager, console=console, console_type=console_type, wrap_console=True)
|
|
self._process = None
|
|
self._vpcs_stdout_file = ""
|
|
self._vpcs_version = None
|
|
self._started = False
|
|
self._local_udp_tunnel = None
|
|
self._execute_lock = asyncio.Lock()
|
|
# Kernel-datapath state (the anchor is a persistent TAP uBridge
|
|
# creates and the vpcs process attaches to — see the datapath
|
|
# section below).
|
|
self._tap_datapath = False
|
|
self._kernel_taps = {}
|
|
self._ubridge_tc_caps = None
|
|
|
|
# VPCS settings
|
|
if startup_script is not None and not self.script_file: # We disallow override at startup
|
|
self.startup_script = startup_script
|
|
self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
|
|
|
|
@property
|
|
def ethernet_adapter(self):
|
|
return self._ethernet_adapter
|
|
|
|
@property
|
|
def _ethernet_adapters(self):
|
|
"""
|
|
The adapter list the kernel-datapath mixin walks (the per-link
|
|
bridge sweep): VPCS owns exactly one single-port adapter.
|
|
"""
|
|
|
|
return [self._ethernet_adapter]
|
|
|
|
async def close(self):
|
|
"""
|
|
Closes this VPCS VM.
|
|
"""
|
|
|
|
if not (await super().close()):
|
|
return False
|
|
|
|
nio = self._ethernet_adapter.get_nio(0)
|
|
if isinstance(nio, NIOUDP):
|
|
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
|
|
|
if self._local_udp_tunnel:
|
|
self.manager.port_manager.release_udp_port(self._local_udp_tunnel[0].lport, self._project)
|
|
self.manager.port_manager.release_udp_port(self._local_udp_tunnel[1].lport, self._project)
|
|
self._local_udp_tunnel = None
|
|
|
|
# stop() tears the datapath down in the right order (the vpcs
|
|
# process holds the anchor TAPs open — a held device refuses
|
|
# deletion and would leak); the else branch covers a node that is
|
|
# not running but may still hold a uBridge with anchors.
|
|
if self.is_running():
|
|
await self.stop()
|
|
else:
|
|
await self._stop_ubridge()
|
|
|
|
return True
|
|
|
|
async def _check_requirements(self):
|
|
"""
|
|
Check if VPCS is available with the correct version.
|
|
"""
|
|
|
|
path = self._vpcs_path()
|
|
if not path:
|
|
raise VPCSError("No path to a VPCS executable has been set")
|
|
|
|
# This raise an error if ubridge is not available
|
|
self.ubridge_path
|
|
|
|
if not os.path.isfile(path):
|
|
raise VPCSError(f"VPCS program '{path}' is not accessible")
|
|
|
|
if not os.access(path, os.X_OK):
|
|
raise VPCSError(f"VPCS program '{path}' is not executable")
|
|
|
|
await self._check_vpcs_version()
|
|
|
|
def asdict(self):
|
|
|
|
return {
|
|
"name": self.name,
|
|
"node_id": self.id,
|
|
"node_directory": self.working_path,
|
|
"status": self.status,
|
|
"console": self._console,
|
|
"console_type": self._console_type,
|
|
"project_id": self.project.id,
|
|
"command_line": self.command_line,
|
|
}
|
|
|
|
def _vpcs_path(self):
|
|
"""
|
|
Returns the VPCS executable path.
|
|
|
|
:returns: path to VPCS
|
|
"""
|
|
|
|
vpcs_path = self._manager.config.settings.VPCS.vpcs_path
|
|
if not os.path.isabs(vpcs_path):
|
|
if sys.platform.startswith("win") and hasattr(sys, "frozen"):
|
|
vpcs_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "vpcs"))
|
|
os.environ["PATH"] = os.pathsep.join(vpcs_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
vpcs_path = shutil.which(vpcs_path)
|
|
return vpcs_path
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, new_name):
|
|
"""
|
|
Sets the name of this VPCS VM.
|
|
|
|
:param new_name: name
|
|
"""
|
|
|
|
if self.script_file:
|
|
content = self.startup_script
|
|
content = content.replace(self._name, new_name)
|
|
escaped_name = new_name.replace("\\", "")
|
|
content = re.sub(r"^set pcname .+$", "set pcname " + escaped_name, content, flags=re.MULTILINE)
|
|
self.startup_script = content
|
|
|
|
super(VPCSVM, VPCSVM).name.__set__(self, new_name)
|
|
|
|
@property
|
|
def startup_script(self):
|
|
"""
|
|
Returns the content of the current startup script
|
|
"""
|
|
|
|
script_file = self.script_file
|
|
if script_file is None:
|
|
return None
|
|
|
|
try:
|
|
with open(script_file, "rb") as f:
|
|
return f.read().decode("utf-8", errors="replace")
|
|
except OSError as e:
|
|
raise VPCSError(f'Cannot read the startup script file "{script_file}": {e}')
|
|
|
|
@startup_script.setter
|
|
def startup_script(self, startup_script):
|
|
"""
|
|
Updates the startup script.
|
|
|
|
:param startup_script: content of the startup script
|
|
"""
|
|
|
|
try:
|
|
startup_script_path = os.path.join(self.working_dir, "startup.vpc")
|
|
with open(startup_script_path, "w+", encoding="utf-8") as f:
|
|
if startup_script is None:
|
|
f.write("")
|
|
else:
|
|
startup_script = startup_script.replace("%h", self._name)
|
|
f.write(startup_script)
|
|
except OSError as e:
|
|
raise VPCSError(f'Cannot write the startup script file "{startup_script_path}": {e}')
|
|
|
|
async def _check_vpcs_version(self):
|
|
"""
|
|
Checks if the VPCS executable version is >= 0.8b or == 0.6.1.
|
|
"""
|
|
try:
|
|
output = await subprocess_check_output(self._vpcs_path(), "-v", cwd=self.working_dir)
|
|
match = re.search(r"Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output)
|
|
if match:
|
|
version = match.group(1)
|
|
self._vpcs_version = parse_version(version)
|
|
if self._vpcs_version < parse_version("0.6.1"):
|
|
raise VPCSError("VPCS executable version must be >= 0.6.1 but not a 0.8")
|
|
else:
|
|
raise VPCSError(f"Could not determine the VPCS version for {self._vpcs_path()}")
|
|
except (OSError, subprocess.SubprocessError) as e:
|
|
raise VPCSError(f"Error while looking for the VPCS version: {e}")
|
|
|
|
async def start(self):
|
|
"""
|
|
Starts the VPCS process.
|
|
"""
|
|
|
|
await self._check_requirements()
|
|
if not self.is_running():
|
|
nio = self._ethernet_adapter.get_nio(0)
|
|
# uBridge owns the anchor TAP and must be up before it is
|
|
# created — and before vpcs attaches to it. The whole setup runs
|
|
# inside the try so a failure at any step (probe refused, tap
|
|
# create, launch) takes the datapath down again: a node that
|
|
# reports stopped must not keep a uBridge process and a
|
|
# root-namespace TAP alive.
|
|
self._ubridge_tc_caps = None
|
|
try:
|
|
await self._start_ubridge()
|
|
await self._prepare_tap_datapath()
|
|
|
|
command = self._build_command()
|
|
try:
|
|
log.info(f"Starting VPCS: {command}")
|
|
self._vpcs_stdout_file = os.path.join(self.working_dir, "vpcs.log")
|
|
log.info(f"Logging to {self._vpcs_stdout_file}")
|
|
flags = 0
|
|
if sys.platform.startswith("win32"):
|
|
flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
with open(self._vpcs_stdout_file, "w", encoding="utf-8") as fd:
|
|
self.command_line = " ".join(command)
|
|
self._process = await asyncio.create_subprocess_exec(
|
|
*command, stdout=fd, stderr=subprocess.STDOUT, cwd=self.working_dir, creationflags=flags
|
|
)
|
|
monitor_process(self._process, self._termination_callback)
|
|
|
|
if nio:
|
|
# a link that outlived a stop is re-attached from its NIO
|
|
await self._connect_nio(0, nio)
|
|
await self._set_adapter_carrier(0, not nio.suspend)
|
|
|
|
await self.start_wrap_console()
|
|
|
|
log.info(f"VPCS instance {self.name} started PID={self._process.pid}")
|
|
self._started = True
|
|
self.status = "started"
|
|
except (OSError, subprocess.SubprocessError) as e:
|
|
vpcs_stdout = self.read_vpcs_stdout()
|
|
log.error(f"Could not start VPCS {self._vpcs_path()}: {e}\n{vpcs_stdout}")
|
|
raise VPCSError(f"Could not start VPCS {self._vpcs_path()}: {e}\n{vpcs_stdout}")
|
|
except Exception:
|
|
# uBridge was started (and the anchor TAP created) before the
|
|
# launch: don't leave either behind on a failed start.
|
|
await self._stop_ubridge()
|
|
raise
|
|
|
|
async def _termination_callback(self, returncode):
|
|
"""
|
|
Called when the process has stopped.
|
|
|
|
:param returncode: Process returncode
|
|
"""
|
|
|
|
if self._started:
|
|
log.info("VPCS process has stopped, return code: %d", returncode)
|
|
# stop() tears the datapath down under the execute lock (an API
|
|
# stop may be running concurrently) and sets the status.
|
|
await self.stop()
|
|
if returncode != 0:
|
|
self.project.emit(
|
|
"log.error",
|
|
{"message": f"VPCS process has stopped, return code: {returncode}\n{self.read_vpcs_stdout()}"},
|
|
)
|
|
|
|
async def stop(self):
|
|
"""
|
|
Stops the VPCS process.
|
|
"""
|
|
|
|
async with self._execute_lock:
|
|
# Release the datapath once vpcs is gone: the process holds the
|
|
# anchor TAP open (it is the guest side), and uBridge's tap
|
|
# delete refuses a held device ("Device or resource busy") —
|
|
# deleting it while vpcs still ran leaked the persistent TAP,
|
|
# because that best-effort delete is suppressed. The process
|
|
# monitor calls stop() on its own when vpcs dies, so an API stop
|
|
# and a process-death stop run concurrently in the normal case;
|
|
# the lock serializes their TAP sweeps (the second one finds
|
|
# uBridge already gone and sweeps nothing).
|
|
if self.is_running():
|
|
self._terminate_process()
|
|
if self._process.returncode is None:
|
|
try:
|
|
await wait_for_process_termination(self._process, timeout=3)
|
|
except asyncio.TimeoutError:
|
|
if self._process.returncode is None:
|
|
try:
|
|
self._process.kill()
|
|
except OSError as e:
|
|
log.error(f"Cannot stop the VPCS process: {e}")
|
|
if self._process.returncode is None:
|
|
log.warning(f'VPCS VM "{self._name}" with PID={self._process.pid} is still running')
|
|
|
|
self._process = None
|
|
self._started = False
|
|
await self._stop_ubridge()
|
|
await super().stop()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Kernel datapath: the adapter anchor is a persistent TAP
|
|
# ------------------------------------------------------------------
|
|
|
|
def _tap_name(self, adapter_number, port_number=0):
|
|
"""
|
|
Deterministic anchor TAP name for the adapter port (the shared
|
|
utils.kernel_anchor naming contract — the controller names a peer's
|
|
anchor with the same function when an Ethernet switch absorbs it).
|
|
"""
|
|
|
|
return kernel_anchor_name("vpcs", self._id, adapter_number, port_number)
|
|
|
|
def _kernel_host_ifc(self, adapter_number, port_number=0):
|
|
"""
|
|
The persistent TAP this adapter owns, or None before the node
|
|
started (or on the legacy UDP relay datapath).
|
|
"""
|
|
|
|
return self._kernel_taps.get((adapter_number, port_number))
|
|
|
|
def _kernel_anchors(self):
|
|
return set(self._kernel_taps.values())
|
|
|
|
def _kernel_error(self, message):
|
|
return VPCSError(message)
|
|
|
|
async def _prepare_tap_datapath(self):
|
|
"""
|
|
Decide this start's datapath: a vpcs build with ``-n`` (no host
|
|
auto-configuration) and a uBridge with the persistent-TAP module
|
|
anchor the adapter on a persistent TAP the vpcs process attaches to
|
|
(``-e -d``). Anything else (upstream vpcs, old uBridge, no
|
|
CAP_NET_ADMIN) keeps the legacy UDP relay datapath — kernel links
|
|
are refused on it.
|
|
"""
|
|
|
|
self._tap_datapath = False
|
|
self._kernel_taps.clear()
|
|
if not self.ubridge:
|
|
return
|
|
|
|
vpcs_tap = await probe_vpcs_tap()
|
|
if vpcs_tap is not True:
|
|
# Without -n every guest `ip` command would ifconfig the anchor
|
|
# on the host — the server owns the anchor's addresses and
|
|
# bridge membership, so the relay datapath is the only safe
|
|
# shape. False (a build definitively without -n) is actionable
|
|
# for the user; None (probe failed) stays server-log-only.
|
|
log.warning(
|
|
"VPCS '%s': vpcs '%s' cannot anchor a kernel link (no -n support)", self._name, self._vpcs_path()
|
|
)
|
|
if vpcs_tap is False:
|
|
self.project.emit(
|
|
"log.warning",
|
|
{
|
|
"message": f"VPCS '{self._name}': the vpcs binary does not support tap mode without host "
|
|
"auto-configuration (-n); this node runs on the legacy relay datapath and cannot carry "
|
|
"kernel links"
|
|
},
|
|
)
|
|
return
|
|
|
|
probe_error = await self._probe_persistent_taps(f"gp{self._id.replace('-', '')[:8]}prob")
|
|
if probe_error is not None:
|
|
log.warning("VPCS '%s': uBridge cannot create persistent TAPs (%s)", self._name, probe_error)
|
|
self.project.emit(
|
|
"log.warning",
|
|
{
|
|
"message": f"VPCS '{self._name}': uBridge does not support persistent TAPs ({probe_error}); "
|
|
"this node runs on the legacy relay datapath and cannot carry kernel links"
|
|
},
|
|
)
|
|
return
|
|
|
|
self._tap_datapath = True
|
|
# The shared lifecycle sweeps leftovers first, creates the TAP and
|
|
# hands it to this user (the unprivileged vpcs process opens it
|
|
# itself), leaving it down until a link attaches.
|
|
await self._create_anchor_taps([(0, 0)], set_owner=True)
|
|
|
|
async def _stop_ubridge(self):
|
|
"""
|
|
Stops uBridge, removing the anchor TAP first: it lives in the root
|
|
namespace and outlives the process (the tap module needs the control
|
|
channel to delete it), and vpcs holding the other end keeps the
|
|
device alive until it exits — the caller must terminate vpcs first,
|
|
or the suppressed tap delete leaks the anchor. The next start
|
|
spawns a fresh uBridge, so the tc capabilities must be probed again.
|
|
"""
|
|
|
|
await self._delete_anchor_taps()
|
|
self._ubridge_tc_caps = None
|
|
await super()._stop_ubridge()
|
|
|
|
async def _connect_nio(self, port_number, nio):
|
|
"""
|
|
Attach a link's NIO to the adapter's TAP anchor: a kernel link
|
|
enslaves the TAP into the per-link kernel bridge, a relay link
|
|
attaches it to the uBridge relay bridge as an AF_PACKET endpoint.
|
|
The legacy datapath (no anchor) bridges the local UDP tunnel
|
|
instead.
|
|
"""
|
|
|
|
bridge_name = f"VPCS-{self._id}"
|
|
if not self._tap_datapath:
|
|
if isinstance(nio, NIOBridge):
|
|
raise VPCSError(
|
|
f"VPCS '{self._name}' runs on the legacy relay datapath (no anchor TAP) "
|
|
"and cannot carry a kernel link"
|
|
)
|
|
await self.add_ubridge_udp_connection(bridge_name, self._local_udp_tunnel[1], nio)
|
|
return
|
|
|
|
anchor = self._kernel_host_ifc(0, port_number)
|
|
if anchor is None:
|
|
raise VPCSError(f"VPCS '{self._name}' has no anchor TAP interface; restart the node to attach a link to it")
|
|
if isinstance(nio, NIOBridge):
|
|
await self._kernel_attach(anchor, nio)
|
|
return
|
|
await self._relay_attach(anchor, bridge_name, nio)
|
|
|
|
async def reload(self):
|
|
"""
|
|
Reloads the VPCS process (stop & start).
|
|
"""
|
|
|
|
await self.stop()
|
|
await self.start()
|
|
|
|
def _terminate_process(self):
|
|
"""
|
|
Terminate the process if running
|
|
"""
|
|
|
|
log.info(f"Stopping VPCS instance {self.name} PID={self._process.pid}")
|
|
if sys.platform.startswith("win32"):
|
|
try:
|
|
self._process.send_signal(signal.CTRL_BREAK_EVENT)
|
|
except (SystemError, OSError):
|
|
pass
|
|
else:
|
|
try:
|
|
self._process.terminate()
|
|
# Sometime the process may already be dead when we garbage collect
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
def read_vpcs_stdout(self):
|
|
"""
|
|
Reads the standard output of the VPCS process.
|
|
Only use when the process has been stopped or has crashed.
|
|
"""
|
|
|
|
output = ""
|
|
if self._vpcs_stdout_file:
|
|
try:
|
|
with open(self._vpcs_stdout_file, "rb") as file:
|
|
output = file.read().decode("utf-8", errors="replace")
|
|
except OSError as e:
|
|
log.warning(f"Could not read {self._vpcs_stdout_file}: {e}")
|
|
return output
|
|
|
|
def is_running(self):
|
|
"""
|
|
Checks if the VPCS process is running
|
|
|
|
:returns: True or False
|
|
"""
|
|
|
|
if self._process and self._process.returncode is None:
|
|
return True
|
|
return False
|
|
|
|
async def reset_console(self):
|
|
"""
|
|
Reset console
|
|
"""
|
|
|
|
if self.is_running():
|
|
await self.reset_wrap_console()
|
|
|
|
@property
|
|
def console_type(self):
|
|
return self._console_type
|
|
|
|
@console_type.setter
|
|
def console_type(self, new_console_type):
|
|
"""
|
|
Sets the console type for this VPCS VM.
|
|
|
|
:param new_console_type: console type (string)
|
|
"""
|
|
|
|
if self.is_running() and self.console_type != new_console_type:
|
|
raise VPCSError(f'"{self._name}" must be stopped to change the console type to {new_console_type}')
|
|
|
|
super(VPCSVM, VPCSVM).console_type.__set__(self, new_console_type)
|
|
|
|
async def port_add_nio_binding(self, port_number, nio):
|
|
"""
|
|
Adds a port NIO binding.
|
|
|
|
:param port_number: port number
|
|
:param nio: NIO instance to add to the slot/port
|
|
"""
|
|
|
|
if not self._ethernet_adapter.port_exists(port_number):
|
|
raise VPCSError(f"Port {port_number} doesn't exist on adapter {self._ethernet_adapter}")
|
|
|
|
# The compute-side backstop of the controller's duplicate-port guard:
|
|
# a port carries at most one NIO, and overwriting the one a link
|
|
# still owns orphans that link's teardown (its host state leaks).
|
|
if self._ethernet_adapter.get_nio(port_number) is not None:
|
|
raise VPCSError(f"Port {port_number} of VPCS '{self._name}' already has a link")
|
|
|
|
if self.is_running():
|
|
await self._connect_nio(port_number, nio)
|
|
await self._set_adapter_carrier(0, not nio.suspend)
|
|
|
|
self._ethernet_adapter.add_nio(port_number, nio)
|
|
log.info(f'VPCS "{self._name}" [{self.id}]: {nio} added to port {port_number}')
|
|
|
|
return nio
|
|
|
|
async def port_update_nio_binding(self, port_number, nio):
|
|
"""
|
|
Updates a port NIO binding.
|
|
|
|
:param port_number: port number
|
|
:param nio: NIO instance to update on the slot/port
|
|
"""
|
|
|
|
if not self._ethernet_adapter.port_exists(port_number):
|
|
raise VPCSError(f"Port {port_number} doesn't exist on adapter {self._ethernet_adapter}")
|
|
if self.is_running():
|
|
if self._tap_datapath and isinstance(nio, NIOBridge):
|
|
# Filters or markers changed on an attached kernel link:
|
|
# re-apply on the anchor, no re-enslaving.
|
|
anchor = self._kernel_host_ifc(0, port_number)
|
|
if anchor is not None:
|
|
await self._kernel_update(anchor, nio)
|
|
elif self._tap_datapath:
|
|
# Relay link on the TAP anchor: re-apply on its bridge.
|
|
await self._ubridge_apply_filters(f"VPCS-{self._id}", nio.filters)
|
|
await self._ubridge_apply_markers(f"VPCS-{self._id}", nio)
|
|
else:
|
|
await self.update_ubridge_udp_connection(f"VPCS-{self._id}", self._local_udp_tunnel[1], nio)
|
|
await self._set_adapter_carrier(0, not nio.suspend)
|
|
|
|
async def port_remove_nio_binding(self, port_number):
|
|
"""
|
|
Removes a port NIO binding.
|
|
|
|
:param port_number: port number
|
|
|
|
:returns: NIO instance
|
|
"""
|
|
|
|
if not self._ethernet_adapter.port_exists(port_number):
|
|
raise VPCSError(f"Port {port_number} doesn't exist on adapter {self._ethernet_adapter}")
|
|
|
|
nio = self._ethernet_adapter.get_nio(port_number)
|
|
await self.stop_capture(port_number)
|
|
if self.is_running():
|
|
if self._tap_datapath and isinstance(nio, NIOBridge):
|
|
await self._remove_kernel_nio(nio, 0, port_number)
|
|
elif self._tap_datapath:
|
|
await self._relay_detach(f"VPCS-{self._id}")
|
|
else:
|
|
await self._ubridge_send("bridge delete {name}".format(name=f"VPCS-{self._id}"))
|
|
|
|
if isinstance(nio, NIOUDP):
|
|
self.manager.port_manager.release_udp_port(nio.lport, self._project)
|
|
self._ethernet_adapter.remove_nio(port_number)
|
|
|
|
log.info(f'VPCS "{self._name}" [{self.id}]: {nio} removed from port {port_number}')
|
|
return nio
|
|
|
|
def get_nio(self, port_number):
|
|
"""
|
|
Gets a port NIO binding.
|
|
|
|
:param port_number: port number
|
|
|
|
:returns: NIO instance
|
|
"""
|
|
|
|
if not self._ethernet_adapter.port_exists(port_number):
|
|
raise VPCSError(f"Port {port_number} doesn't exist on adapter {self._ethernet_adapter}")
|
|
nio = self._ethernet_adapter.get_nio(port_number)
|
|
if not nio:
|
|
raise VPCSError(f"Port {port_number} is not connected")
|
|
return nio
|
|
|
|
async def start_capture(self, port_number, output_file):
|
|
"""
|
|
Starts a packet capture.
|
|
|
|
:param port_number: port number
|
|
:param output_file: PCAP destination file for the capture
|
|
"""
|
|
|
|
nio = self.get_nio(port_number)
|
|
if nio.capturing:
|
|
raise VPCSError(f"Packet capture is already active on port {port_number}")
|
|
|
|
nio.start_packet_capture(output_file)
|
|
if self.ubridge:
|
|
anchor = self._kernel_host_ifc(0, port_number)
|
|
if isinstance(nio, NIOBridge) and anchor is not None:
|
|
# Kernel link: capture on the adapter's TAP anchor (AF_PACKET,
|
|
# one capture per uBridge process — see _reserve_kernel_capture;
|
|
# a failed start rolls the port's flag back so the port cannot
|
|
# later stop the winner's capture).
|
|
try:
|
|
self._reserve_kernel_capture(anchor)
|
|
await self._ubridge_send(f'capture start_kernel {anchor} "{output_file}"')
|
|
except Exception:
|
|
self._release_kernel_capture(anchor)
|
|
nio.stop_packet_capture()
|
|
raise
|
|
else:
|
|
await self._ubridge_send(
|
|
'bridge start_capture {name} "{output_file}"'.format(
|
|
name=f"VPCS-{self._id}", output_file=output_file
|
|
)
|
|
)
|
|
|
|
log.info(f"VPCS '{self.name}' [{self.id}]: starting packet capture on port {port_number}")
|
|
|
|
async def stop_capture(self, port_number):
|
|
"""
|
|
Stops a packet capture.
|
|
|
|
:param port_number: port number
|
|
"""
|
|
|
|
nio = self.get_nio(port_number)
|
|
if not nio.capturing:
|
|
return
|
|
|
|
nio.stop_packet_capture()
|
|
if self.ubridge:
|
|
anchor = self._kernel_host_ifc(0, port_number)
|
|
if isinstance(nio, NIOBridge) and anchor is not None:
|
|
# Process-wide and argument-less: only the port owning the
|
|
# slot may stop it (a second port's stop would kill this
|
|
# capture while its own flag still says capturing).
|
|
if self._kernel_capture_owned_by(anchor):
|
|
await self._ubridge_send("capture stop_kernel")
|
|
self._release_kernel_capture(anchor)
|
|
else:
|
|
await self._ubridge_send("bridge stop_capture {name}".format(name=f"VPCS-{self._id}"))
|
|
|
|
log.info(f"VPCS '{self.name}' [{self.id}]: stopping packet capture on port {port_number}")
|
|
|
|
def _build_command(self):
|
|
"""
|
|
Command to start the VPCS process.
|
|
(to be passed to subprocess.Popen())
|
|
|
|
VPCS command line:
|
|
usage: vpcs [options] [scriptfile]
|
|
Option:
|
|
-h print this help then exit
|
|
-v print version information then exit
|
|
|
|
-i num number of vpc instances to start (default is 9)
|
|
-p port run as a daemon listening on the tcp 'port'
|
|
-m num start byte of ether address, default from 0
|
|
-r file load and execute script file
|
|
compatible with older versions, DEPRECATED.
|
|
|
|
-e tap mode, using /dev/tapx by default (linux only)
|
|
-u udp mode, default
|
|
|
|
udp mode options:
|
|
-s port local udp base port, default from 20000
|
|
-c port remote udp base port (dynamips udp port), default from 30000
|
|
-t ip remote host IP, default 127.0.0.1
|
|
|
|
tap mode options:
|
|
-d vm device name, works only when -i is set to 1
|
|
-n do not auto-configure the host tap interface
|
|
|
|
hypervisor mode option:
|
|
-H port run as the hypervisor listening on the tcp 'port'
|
|
|
|
If no 'scriptfile' specified, vpcs will read and execute the file named
|
|
'startup.vpc' if it exsits in the current directory.
|
|
|
|
"""
|
|
|
|
command = [self._vpcs_path()]
|
|
command.extend(["-p", str(self._internal_console_port)]) # listen to console port
|
|
command.extend(
|
|
["-m", str(self._manager.get_mac_id(self.id))]
|
|
) # the unique ID is used to set the MAC address offset
|
|
command.extend(["-i", "1"]) # option to start only one VPC instance
|
|
command.extend(["-F"]) # option to avoid the daemonization of VPCS
|
|
if self._vpcs_version >= parse_version("0.8b"):
|
|
command.extend(["-R"]) # disable the relay feature of VPCS (starting with VPCS 0.8)
|
|
else:
|
|
log.warning("The VPCS relay feature could not be disabled because the VPCS version is below 0.8b")
|
|
|
|
if self._tap_datapath:
|
|
# Kernel-datapath anchor: uBridge created the persistent TAP
|
|
# before the launch, vpcs attaches to it by name and must leave
|
|
# its host-side configuration alone (-n).
|
|
command.extend(["-e"]) # tap mode
|
|
command.extend(["-d", self._tap_name(0, 0)]) # the anchor TAP
|
|
command.extend(["-n"]) # no host auto-configuration on the anchor
|
|
else:
|
|
# use the local UDP tunnel to uBridge instead
|
|
if not self._local_udp_tunnel:
|
|
self._local_udp_tunnel = self._create_local_udp_tunnel()
|
|
nio = self._local_udp_tunnel[0]
|
|
if nio and isinstance(nio, NIOUDP):
|
|
# UDP tunnel
|
|
command.extend(["-s", str(nio.lport)]) # source UDP port
|
|
command.extend(["-c", str(nio.rport)]) # destination UDP port
|
|
try:
|
|
command.extend(
|
|
["-t", socket.gethostbyname(nio.rhost)]
|
|
) # destination host, we need to resolve the hostname because VPCS doesn't support it
|
|
except socket.gaierror as e:
|
|
raise VPCSError(f"Can't resolve hostname {nio.rhost}")
|
|
|
|
if self.script_file:
|
|
command.extend([os.path.basename(self.script_file)])
|
|
return command
|
|
|
|
@property
|
|
def script_file(self):
|
|
"""
|
|
Returns the startup script file for this VPCS VM.
|
|
|
|
:returns: path to startup script file
|
|
"""
|
|
|
|
# use the default VPCS file if it exists
|
|
path = os.path.join(self.working_dir, "startup.vpc")
|
|
if os.path.exists(path):
|
|
return path
|
|
else:
|
|
return None
|