YueGuobin c343ce0b8f
feat(esw): wire peer anchors straight into the switch bridge
The Ethernet switch's kernel fast path: instead of a link crossing the
switch's per-port uBridge relay, the switch absorbs the peer's anchor into
its own kernel bridge. The peer's interface (Docker veth host end, QEMU
TAP, IOU port TAP, Dynamips port TAP) becomes the switch port, and frames
cross anchor -> switch bridge -> switch ports entirely in the kernel.

Wire format: the switch end receives nio_anchor ('join this interface to
my own bridge' — the mirror of nio_bridge), the peer end nio_bridge with a
null bridge ('your anchor is bridged elsewhere': carrier, markers and
impairments only — never bridge membership, and never a bridge delete, on
an interface this node does not own). Anchor names come from the shared
naming contract (utils.kernel_anchor), a pure function of node type, id,
adapter and port — the controller computes the peer's anchor while the
node may be stopped, which is what makes links to stopped nodes wireable.

Stopped peers defer the join; after any node of a link starts
(UDPLink.node_started) the controller re-pushes the switch NIO, and the
switch completes — or repairs, when a peer restart replaced its interface
under the same name — the join against the kernel's actual membership
instead of assuming it.

VLAN changes now edit membership in place: only the ports whose mode/VLAN
actually changed are touched, each losing exactly the previous mode's
entries the new mode does not want (the dot1q admits-all entry is one
range operation in both directions), and no port ever leaves the bridge to
change its VLANs. This replaces the old 'reset by re-enslaving' shortcut
(delif+addif on every wired port, added when the ports were the switch's
own TAPs): on absorbed anchors, re-enslaving an interface owned by another
node is where live testing watched links die — anchors occasionally lost
their bridge membership right after a ports_mapping update (later updates
then failed with delif EINVAL), reproducibly with qemu, IOU and Dynamips
peers and never with Docker's veths.

Eligibility: the switch side needs no capability of its own (its bridge
exists by construction); the peer must be able to anchor. Switch-to-switch
cascades stay on the relay — a kernel interface belongs to one bridge, and
joining two bridges needs a veth pair, deferred.

Verified live on a real server: qemu peers through four VLAN cycles with
both anchors enslaved throughout; a mixed switch (2 IOU + 2 Dynamips, four
kernel ports); Docker peers with real ICMP isolating and recovering (with
both anchors still enslaved); VPCS relay ports likewise in place.
2026-10-06 00:21:09 +08:00

879 lines
32 KiB
Python

#!/usr/bin/env python
#
# Copyright (C) 2016 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/>.
import html
import logging
import os
import re
import uuid
from gns3server.agent.web_wireshark.manager import WebWiresharkManager
from gns3server.config import Config
from gns3server.utils.packet_filter_validation import (
FilterValidationError,
filter_inactive_filters,
kernel_only_features,
validate_all_filters,
)
from .controller_error import ControllerError, ControllerNotFoundError
log = logging.getLogger(__name__)
# Sentinel for "argument not passed". Distinct from None so marker/definition
# updaters can tell "caller omitted direction" (keep current value) from
# "caller passed direction=None" (clear it back to both directions). See
# UDPLink.update_marker and Project.update_marker_definition.
_UNSET = object()
FILTERS = [
{
"type": "frequency_drop",
"name": "Frequency drop",
"description": "It will drop everything with a -1 frequency, drop every Nth packet with a positive "
"frequency, or drop nothing. On kernel-datapath links this is exact (eBPF every-Nth counter)",
"parameters": [{"name": "Frequency", "minimum": -1, "maximum": 32767, "type": "int", "unit": "th packet"}],
},
{
"type": "packet_loss",
"name": "Packet loss",
"description": "The percentage represents the chance for a packet to be lost. The optional correlation "
"(kernel-datapath links only) makes consecutive losses dependent, like a bursty real network",
"parameters": [
{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Correlation", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "delay",
"name": "Delay",
"description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay, and "
"(kernel-datapath links only) shape the jitter with a distribution",
"parameters": [
{"name": "Latency", "minimum": 1, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Jitter (-/+)", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Distribution", "type": "str", "unit": "uniform|normal|pareto|paretonormal"},
],
},
{
"type": "corrupt",
"name": "Corrupt",
"description": "The percentage represents the chance for a packet to be corrupted",
"parameters": [{"name": "Chance", "minimum": 0, "maximum": 100, "unit": "%", "type": "int"}],
},
{
"type": "bpf",
"name": "Berkeley Packet Filter (BPF)",
"description": "This filter will drop any packet matching a BPF expression. Put one expression per line",
"parameters": [{"name": "Filters", "type": "text"}],
},
# Everything below runs on kernel-datapath links only (tc netem on the
# veth host end; the uBridge relay has no equivalent).
{
"type": "rate",
"name": "Bandwidth limit",
"description": "Shape the link to a maximum bandwidth, tc-style value (kernel-datapath links only)",
"parameters": [{"name": "Rate", "type": "str", "unit": "e.g. 512kbit, 10mbit"}],
},
{
"type": "reorder",
"name": "Reorder",
"description": "The percentage represents the chance for a packet to be reordered (held until a later "
"packet passes it). Requires the delay filter; kernel-datapath links only",
"parameters": [
{"name": "Reorder", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Correlation", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Gap", "minimum": 1, "maximum": 1000, "type": "int", "unit": "packets"},
],
},
{
"type": "gemodel",
"name": "Gilbert-Elliot loss",
"description": "Bursty loss model: p is the chance of moving from the good to the bad state, r the chance "
"of moving back, and 1-h the loss chance while in the bad state (the good state loses nothing) — the "
"steady-state mean loss is p/(p+r) x (1-h). Mutually exclusive with packet loss; kernel-datapath links "
"only",
"parameters": [
{"name": "p (good-to-bad)", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "r (bad-to-good)", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "1-h (bad-state loss)", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "duplicate",
"name": "Duplicate",
"description": "The percentage represents the chance for a packet to be duplicated; the optional "
"correlation makes consecutive duplicates dependent (kernel-datapath links only)",
"parameters": [
{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Correlation", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "seed",
"name": "Random seed",
"description": "Make the netem random draws (loss, duplication, jitter) reproducible for repeated "
"experiments (kernel-datapath links only)",
"parameters": [{"name": "Seed", "minimum": 0, "maximum": 4294967295, "type": "int", "unit": ""}],
},
{
"type": "limit",
"name": "Queue limit",
"description": "Queue depth of the impairment qdisc in packets — raise it above the 1000 default when "
"combining a low rate with a long delay (kernel-datapath links only)",
"parameters": [{"name": "Limit", "minimum": 1, "maximum": 1000000, "type": "int", "unit": "packets"}],
},
{
"type": "quota",
"name": "Byte quota",
"description": "After the byte quota is consumed, each further packet drops with the given chance "
"(100 = hard cutoff) — a data cap, like a mobile plan (kernel-datapath links only)",
"parameters": [
{"name": "Quota", "minimum": 1, "maximum": 1000000000000000, "type": "int", "unit": "bytes"},
{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "window_drop",
"name": "Time window drop",
"description": "Drop packets with the given chance inside a time window starting Start ms from the "
"moment the filter is applied — a single outage (traffic passes before and after), or with a "
"period recurring flaps whose per-cycle timing a jitter randomizes (kernel-datapath links only)",
"parameters": [
{"name": "Start", "minimum": 0, "maximum": 1000000000000, "type": "int", "unit": "ms"},
{"name": "Outage", "minimum": 1, "maximum": 1000000000000, "type": "int", "unit": "ms"},
{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Period", "minimum": 1, "maximum": 1000000000000, "type": "int", "unit": "ms"},
{"name": "Jitter", "minimum": 0, "maximum": 1000000000, "type": "int", "unit": "ms"},
],
},
]
class Link:
"""
Base class for links.
"""
def __init__(self, project, link_id=None):
if link_id:
self._id = link_id
else:
self._id = str(uuid.uuid4())
self._nodes = []
self._project = project
self._capturing = False
self._capture_node = None
self._capture_file_name = None
self._streaming_pcap = None
self._created = False
self._link_type = "ethernet"
self._suspended = False
self._filters = {}
self._markers = {}
self._link_style = {}
self._wireshark = False
self._show_filters_icon = True
@property
def filters(self):
"""
Get an array of filters
"""
return self._filters
@property
def kernel_datapath(self):
"""
Whether this link is wired on the kernel datapath (no uBridge relay
in the forwarding path): either both endpoints enslave their
anchors into a per-link Linux bridge (``nio_bridge``), or an
Ethernet switch absorbs the peer's anchor into its own kernel
bridge (``nio_anchor`` on the switch end, ``nio_bridge`` with no
bridge on the peer end). Impairment filters run as tc netem on the
anchors; markers and capture are served by uBridge's AF_PACKET
module.
"""
return any(d.get("type") in ("nio_bridge", "nio_anchor") for d in (getattr(self, "_link_data", None) or []))
@property
def markers(self):
"""
Get the traffic insight markers dict: name → {bpf, tag, enabled}
"""
return self._markers
async def inherit_marker(self, def_name, marker_def, dump=True, memory_only=False):
"""
Apply a project-level marker definition to this link.
The marker is stored under ``global-{def_name}`` so it cannot collide
with a per-link private marker of the same name. It carries an
``inherited_from`` back-reference that (a) guards against per-link
edits and (b) lets the project sync changes to every copy at once.
The pcap link-layer follows the link type: Ethernet is always EN10MB.
A serial link needs the definition's WAN encapsulation (HDLC / PPP /
Frame Relay); if none was chosen the serial link is skipped — an EN10MB
pcap on a serial link is undecodable.
"""
def_data_link_type = marker_def.get("data_link_type", "DLT_EN10MB")
if self._link_type == "serial":
if def_data_link_type.upper() == "DLT_EN10MB":
return # definition is Ethernet-only; skip this serial link
data_link_type = def_data_link_type
else:
data_link_type = "DLT_EN10MB"
await self.start_marker(
name=f"global-{def_name}",
bpf=marker_def["bpf"],
tag=marker_def.get("tag"),
direction=marker_def.get("direction"),
data_link_type=data_link_type,
color=marker_def.get("color"),
highlight_duration=marker_def.get("highlight_duration"),
enabled=not marker_def.get("paused", False),
inherited_from=def_name,
dump=dump,
memory_only=memory_only,
)
def _persist_markers(self):
"""
Return only the per-link (non-inherited) markers suitable for
persistence in a topology dump. Inherited markers are re-created from
``project._marker_definitions`` on load so they do not need to be saved.
"""
return {k: v for k, v in self._markers.items() if not v.get("inherited_from")}
@property
def show_filters_icon(self):
"""
Get whether to show filters icon in Web UI
"""
return getattr(self, "_show_filters_icon", True)
@property
def project(self):
"""
Get the project this link belongs to.
:return: Project instance.
"""
return self._project
@property
def capture_node(self):
"""
Get the capturing node
:return: Node instance.
"""
return self._capture_node
@property
def compute(self):
"""
Get the capturing node
:return: Node instance.
"""
assert self.capture_node
return self.capture_node["node"].compute
def get_active_filters(self):
"""
Return the active filters.
Filters are overridden if the link is suspended.
"""
if self._suspended:
# this is to allow all node types to support suspend link
return {"frequency_drop": [-1]}
return self._filters
async def update_filters(self, filters):
"""
Modify the filters list.
Filters with value 0 will be filtered out as inactive, with special
handling for delay filter to distinguish between "disabled" and "invalid config".
"""
# Filter out inactive filters using the utility function
new_filters = filter_inactive_filters(filters)
# Validate filter parameters before applying
try:
validate_all_filters(new_filters)
except FilterValidationError as e:
raise ControllerError(f"Invalid packet filter parameters: {e!s}")
if self._created and not self.kernel_datapath:
# Kernel-only filters (the netem extensions, the eBPF quota and
# window modes)
# have no relay equivalent. Only enforced on created links — while
# loading a project the datapath is not decided yet (a link that
# will be wired on the kernel datapath must accept them), and the
# relay prepare path drops what it cannot run with a warning.
conflicts = kernel_only_features(new_filters)
if conflicts:
raise ControllerError(
"Packet filter(s) {} only run on a kernel-datapath link (tc netem on the "
"veth host end); this link is wired on the uBridge relay — delete and "
"recreate the link to switch it to the kernel datapath".format(", ".join(sorted(conflicts)))
)
# An unchanged filters dict is normally a no-op skip, but a PUT that
# still carries window_drop must reconcile: the outage schedule is
# measured from the apply, so re-sending the same window has to
# re-arm the one-shot outage (the documented contract is that any
# filter update restarts the schedule).
if new_filters != self.filters or "window_drop" in new_filters:
self._filters = new_filters
if self._created:
await self.update()
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
async def update_suspend(self, value):
if value != self._suspended:
self._suspended = value
await self.update()
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
async def update_show_filters_icon(self, value):
"""
Update the show_filters_icon property.
:param value: Boolean indicating whether to show filters icon in Web UI
"""
if value != self._show_filters_icon:
self._show_filters_icon = value
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
async def update_link_style(self, link_style):
if link_style != self._link_style:
self._link_style = link_style
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
@property
def created(self):
"""
:returns: True the link has been created on the computes
"""
return self._created
async def add_node(self, node, adapter_number, port_number, label=None, dump=True, batch=False):
"""
Add a node to the link
:param dump: Dump project on disk
:param batch: When True, do not create the link on the computes once
both nodes are attached — the caller drives creation via the
project-open bulk path. Used to avoid one HTTP round-trip per link.
"""
port = node.get_port(adapter_number, port_number)
if port is None:
raise ControllerNotFoundError(f"Port {adapter_number}/{port_number} for {node.name} not found")
if port.link is not None:
raise ControllerError("Port is already used")
self._link_type = port.link_type
for other_node in self._nodes:
if other_node["node"] == node:
raise ControllerError("Cannot connect to itself")
if node.node_type in ["nat", "cloud"]:
if other_node["node"].node_type in ["nat", "cloud"]:
raise ControllerError(
"Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type)
)
# Check if user is not connecting serial => ethernet
other_port = other_node["node"].get_port(other_node["adapter_number"], other_node["port_number"])
if other_port is None:
raise ControllerNotFoundError(
"Port {}/{} for {} not found".format(
other_node["adapter_number"], other_node["port_number"], other_node["node"].name
)
)
if port.link_type != other_port.link_type:
raise ControllerError(
f"Connecting a {other_port.link_type} interface to a {port.link_type} interface is not allowed"
)
if label is None:
label = {
"text": html.escape(f"{adapter_number}/{port_number}"),
"style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;",
}
self._nodes.append(
{"node": node, "adapter_number": adapter_number, "port_number": port_number, "port": port, "label": label}
)
if len(self._nodes) == 2 and not batch:
await self.create()
for n in self._nodes:
n["node"].add_link(self)
n["port"].link = self
self._created = True
self._project.emit_notification("link.created", self.asdict())
if dump:
self._project.dump()
async def update_nodes(self, nodes):
for node_data in nodes:
node = self._project.get_node(node_data["node_id"])
for port in self._nodes:
if port["node"] == node:
label = node_data.get("label")
if label:
port["label"] = label
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
async def create(self):
"""
Create the link
"""
raise NotImplementedError
async def update(self):
"""
Update a link
"""
raise NotImplementedError
async def delete(self):
"""
Delete the link
"""
for n in self._nodes:
# It could be different of self if we rollback an already existing link
if n["port"].link == self:
n["port"].link = None
n["node"].remove_link(self)
async def reset(self):
"""
Reset a link
"""
raise NotImplementedError
async def start_marker(
self,
name,
bpf,
tag=None,
direction=None,
data_link_type="DLT_EN10MB",
capture_node_id=None,
color=None,
highlight_duration=None,
enabled=True,
inherited_from=None,
dump=True,
memory_only=False,
):
"""
Attach a traffic-insight marker to this link (base — UDPLink overrides).
"""
raise NotImplementedError
async def stop_marker(self, name):
"""
Remove a traffic-insight marker from this link (base — UDPLink overrides).
"""
raise NotImplementedError
async def update_marker(
self,
name,
bpf=None,
tag=None,
enabled=None,
direction=_UNSET,
color=None,
highlight_duration=None,
inherited=False,
dump=True,
memory_only=False,
):
"""
Update an existing marker's BPF, tag, or enabled flag.
A BPF change is a delete+re-add on the ubridge side so the pcap is
flushed and the new filter takes effect.
"""
raise NotImplementedError
async def start_capture(self, data_link_type="DLT_EN10MB", capture_file_name=None, wireshark=False, jwt_token=None):
"""
Start capture on the link
:param data_link_type: PCAP data link type
:param capture_file_name: PCAP capture file name
:param wireshark: Enable Web Wireshark
:param jwt_token: JWT token for authentication
:returns: Capture object
"""
self._capturing = True
self._capture_file_name = capture_file_name
self._project.emit_notification("link.updated", self.asdict())
# Start Web Wireshark if requested
if wireshark:
await self._start_web_wireshark(jwt_token)
async def stop_capture(self):
"""
Stop capture on the link
"""
# Stop Web Wireshark
if self._capturing:
await self._stop_web_wireshark()
self._capturing = False
self._project.emit_notification("link.updated", self.asdict())
async def _start_web_wireshark(self, jwt_token: str):
"""Start Web Wireshark.
Args:
jwt_token: JWT authentication token
Raises:
ControllerError: If startup fails
"""
if not jwt_token:
raise ControllerError("JWT token is required for Web Wireshark")
# Load WebWireshark configuration from config file
webwireshark_config = Config.instance().settings.WebWireshark
manager = WebWiresharkManager()
try:
log.info(f"Starting Web Wireshark for link {self.id}")
result = await manager.start_wireshark_session(
project_id=self._project.id,
link_id=self.id,
jwt_token=jwt_token,
memory=webwireshark_config.memory,
cpus=webwireshark_config.cpus,
pids_limit=webwireshark_config.pids_limit,
)
# Send notification
self._project.emit_notification(
"link.web_wireshark_started", {"link_id": self.id, "ws_url": result.get("ws_url", result.get("url"))}
)
log.info(f"Web Wireshark started for link {self.id}: {result.get('ws_url')}")
self._wireshark = True
except ControllerError:
# Re-raise ControllerError to return error to client
raise
except Exception as e:
error_msg = f"Error starting Web Wireshark: {e!s}"
log.error(error_msg)
raise ControllerError(error_msg)
finally:
await manager.close()
async def _stop_web_wireshark(self):
"""Stop Web Wireshark"""
manager = WebWiresharkManager()
try:
log.info(f"Stopping Web Wireshark for link {self.id}")
await manager.stop_wireshark_session(project_id=self._project.id, link_id=self.id)
log.info(f"Web Wireshark stopped for link {self.id}")
self._wireshark = False
except Exception as e:
log.error(f"Error stopping Web Wireshark: {e}")
finally:
await manager.close()
async def _restart_web_wireshark(self, jwt_token: str):
"""Restart Web Wireshark after window was closed.
Args:
jwt_token: JWT authentication token
Raises:
ControllerError: If restart fails
"""
# Get capture stream URL from compute
manager = WebWiresharkManager()
try:
log.info(f"Restarting Web Wireshark for link {self.id}")
result = await manager.restart_wireshark_session(
project_id=self._project.id, link_id=self.id, jwt_token=jwt_token
)
self._project.emit_notification(
"link.web_wireshark_started", {"link_id": self.id, "ws_url": result.get("ws_url", result.get("url"))}
)
log.info(f"Web Wireshark restarted for link {self.id}: {result.get('ws_url')}")
except ControllerError:
raise
except Exception as e:
error_msg = f"Error restarting Web Wireshark: {e!s}"
log.error(error_msg)
raise ControllerError(error_msg)
finally:
await manager.close()
def pcap_streaming_url(self):
"""
Get the PCAP streaming URL on compute
:returns: URL
"""
assert self.capture_node
compute = self.capture_node["node"].compute
node_type = self.capture_node["node"].node_type
node_id = self.capture_node["node"].id
adapter_number = self.capture_node["adapter_number"]
port_number = self.capture_node["port_number"]
url = (
f"/projects/{self.project.id}/{node_type}/nodes/{node_id}/adapters/{adapter_number}/"
f"ports/{port_number}/capture/stream"
)
return compute._getUrl(url)
async def node_updated(self, node):
"""
Called when a node member of the link is updated
"""
raise NotImplementedError
async def node_started(self, node):
"""
Called after a node member of the link reached the started state.
The base link has nothing to re-synchronise; UDPLink uses it for
the Ethernet-switch fast path (the switch must re-join a peer anchor
that only comes into existence when the peer starts).
"""
def default_capture_file_name(self):
"""
:returns: File name for a capture on this link
"""
capture_file_name = "{}_{}-{}_to_{}_{}-{}".format(
self._nodes[0]["node"].name,
self._nodes[0]["adapter_number"],
self._nodes[0]["port_number"],
self._nodes[1]["node"].name,
self._nodes[1]["adapter_number"],
self._nodes[1]["port_number"],
)
return re.sub(r"[^0-9A-Za-z_-]", "", capture_file_name) + ".pcap"
@property
def id(self):
return self._id
@property
def nodes(self):
return [node["node"] for node in self._nodes]
@property
def capturing(self):
return self._capturing
@property
def capture_file_path(self):
"""
Get the path of the capture
"""
if self._capture_file_name:
return os.path.join(self._project.captures_directory, self._capture_file_name)
else:
return None
@property
def capture_compute_id(self):
"""
Get the capture compute ID.
"""
if self._capture_node:
return self.capture_node["node"].compute.id
else:
return None
def available_filters(self):
"""
Return the list of filters compatible with this link
:returns: Array of filters
"""
filter_node = self._get_filter_node()
if filter_node:
if self.kernel_datapath:
# Kernel-datapath links serve every filter type in principle:
# netem for delay/loss/corrupt and the netem extensions,
# cls_bpf for bpf, the eBPF classifier for frequency_drop,
# quota and window_drop. What is offered is gated on the
# endpoints' computes reporting a uBridge that can run each
# type; computes that report nothing (older servers, failed
# probe) keep the full list — update_filters still rejects
# with 409 on apply.
return [f for f in FILTERS if self._filter_supported_by_computes(f["type"])]
# Relay links: hide the kernel-only types the relay cannot run
# (they would be rejected with 409 on update).
from gns3server.utils.packet_filter_validation import KERNEL_ONLY_FILTERS
return [f for f in FILTERS if f["type"] not in KERNEL_ONLY_FILTERS]
return []
def _filter_supported_by_computes(self, filter_type):
"""
Whether every endpoint's compute reports a uBridge able to run
*filter_type* on a kernel-datapath link (both ends apply their own
filters, so one incapable compute is enough to hide the type).
Computes without a uBridge tc report impose no constraint.
"""
from gns3server.utils.tc_capabilities import FILTER_EBPF_MODES, FILTER_NETEM_KEYWORDS
reported = []
for side in self._nodes:
capabilities = side["node"].compute.capabilities
if isinstance(capabilities, dict) and capabilities.get("ubridge_tc"):
reported.append(capabilities["ubridge_tc"])
if not reported:
return True
for caps in reported:
if filter_type in FILTER_EBPF_MODES:
if FILTER_EBPF_MODES[filter_type] not in caps["ebpf_modes"]:
return False
elif filter_type == "bpf":
if not caps["cbpf"]:
return False
elif filter_type in FILTER_NETEM_KEYWORDS:
if FILTER_NETEM_KEYWORDS[filter_type] not in caps["netem"]:
return False
# the remaining types (delay, packet_loss, corrupt) run on any
# build that reports tc capabilities at all
return True
def _get_filter_node(self):
"""
Return the node where the filter will run
:returns: None if no node support filtering else the node
"""
for node in self._nodes:
if node["node"].node_type in (
"vpcs",
"vmware",
"dynamips",
"qemu",
"iou",
"cloud",
"nat",
"virtualbox",
"docker",
# the brctl Ethernet switch applies filters on its per-port uBridge relays
"ethernet_switch",
):
return node["node"]
return None
def __eq__(self, other):
if not isinstance(other, Link):
return False
return self.id == other.id
def __hash__(self):
return hash(self._id)
def asdict(self, topology_dump=False):
"""
:param topology_dump: Filter to keep only properties require for saving on disk
"""
res = []
for side in self._nodes:
res.append(
{
"node_id": side["node"].id,
"adapter_number": side["adapter_number"],
"port_number": side["port_number"],
"label": side["label"],
}
)
if topology_dump:
return {
"nodes": res,
"link_id": self._id,
"filters": self._filters,
"markers": self._persist_markers(),
"link_style": self._link_style,
"suspend": self._suspended,
"show_filters_icon": getattr(self, "_show_filters_icon", True),
}
result = {
"nodes": res,
"link_id": self._id,
"project_id": self._project.id,
"capturing": self._capturing,
"capture_file_name": self._capture_file_name,
"capture_file_path": self.capture_file_path,
"capture_compute_id": self.capture_compute_id,
"link_type": self._link_type,
# runtime fact, not persisted: recomputed from the NIO wiring on
# every load (tells clients which datapath impairs this link —
# kernel-only filter types absent from available_filters are
# then distinguishable from capability gaps)
"kernel_datapath": self.kernel_datapath,
"filters": self._filters,
"markers": self._markers,
"suspend": self._suspended,
"link_style": self._link_style,
"wireshark": self._wireshark,
"show_filters_icon": getattr(self, "_show_filters_icon", True),
}
return result