feat: implement traffic insight marker backend (ubridge mark filter)

Add compute-side marker subsystem that receives ubridge UDP MARK signals
and forwards them as project-scoped notifications to the web UI for
real-time traffic coloring. Matched packets are always saved to per-link
pcaps for future replay.

Key components:
- gns3server/compute/marker/: MarkerManager (singleton, UDP listener +
  O(1) registry keyed by (node_id, filter_name)) + MarkerListener
  (DatagramProtocol parsing MARK lines per ubridge integration contract)
- gns3server/compute/base_node.py: marker sink/node config at ubridge
  startup; shared _ubridge_add_marker_filter / _ubridge_delete_marker_filter
- Per-node start_marker/stop_marker: VPCS (VPCS-{id}), QEMU
  (QEMU-{id}-{adapter}), Docker (bridge{adapter})
- Compute REST /markers/start + /markers/stop (vpcs/qemu/docker route files)
- Controller Link._markers state + UDPLink.start_marker/stop_marker/
  update_marker (mirror capture pattern: BPF validation, _choose_capture_side,
  node.post forwarding, topology persistence)
- Controller REST GET/POST/DELETE/PUT /v3/projects/{p}/links/{l}/markers
- Config: marker_listen_host / marker_listen_port in ServerSettings
- Signal routing: creation-time registry O(1) lookup, no node-table scan;
  project-scoped WS stream (not global); event payload always carries
  project_id for frontend scoping

Tests: 14 unit tests (registry, listener parsing, UDP round-trip);
562 existing tests pass with zero regressions.
This commit is contained in:
YueGuobin committed 2026-07-16 00:39:51 +08:00
1 parent 01d72aed9a
commit d2e9823e6c
21 files changed
+1135 -1

No files matched your search

+70
View File
@@ -935,9 +935,38 @@ class BaseNode:
f"Hypervisor {self._ubridge_hypervisor.host}:{self._ubridge_hypervisor.port} has successfully started"
)
await self._ubridge_hypervisor.connect()
# Tell this uBridge where to send MARK signals and which node id to
# tag them with. Marker is opt-in and inert until a `mark` filter is
# added, so this never disturbs the data plane.
await self._ubridge_configure_marker_sink()
# save if privileged are required in case uBridge needs to be restarted in self._ubridge_send()
self._ubridge_require_privileged_access = require_privileged_access
async def _ubridge_configure_marker_sink(self):
"""
Point this node's uBridge at the compute's marker UDP sink and tag its
signals with this node's id. Safe to call before any marker filter
exists — uBridge stays inert until a ``mark`` filter is configured.
Old uBridge builds without the marker module are tolerated: the failure
is downgraded to a warning so node start is not blocked by an opt-in
observability feature.
"""
from gns3server.compute.marker.marker_manager import MarkerManager
manager = MarkerManager.instance()
if not manager.running or not manager.host or not manager.port:
return
try:
await self._ubridge_send(f"marker sink {manager.host} {manager.port}")
await self._ubridge_send(f"marker node {self._id}")
except UbridgeError:
log.warning(
"uBridge does not support the marker module; traffic insight disabled for node %r",
self.name,
)
async def _stop_ubridge(self):
"""
Stops uBridge.
@@ -1042,6 +1071,47 @@ class BaseNode:
)
i += 1
async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None):
"""
Attach a `mark` packet filter to a uBridge bridge for traffic insight.
On BPF match uBridge (a) emits a UDP MARK signal to the configured sink
and (b) appends the packet to ``pcap_path``. Unlike the impairment
filters, this is an observability tap: it never drops or alters traffic,
and it is added/removed on its own (not via reset_packet_filters) so the
pcap is not closed/reopened on unrelated filter changes.
:param bridge_name: uBridge bridge carrying the link's traffic
:param name: stable, gns3server-chosen filter name (pcap identity + echoed in signals)
:param bpf: libpcap BPF expression
:param pcap_path: absolute path ubridge appends matched packets to
:param tag: optional correlation id echoed in MARK signals
"""
# mark <bpf> [tag <id>] [pcap <path>] — tag/pcap keyword pairs, any order.
cmd = 'bridge add_packet_filter {bridge} {name} mark "{bpf}"'.format(
bridge=bridge_name, name=name, bpf=bpf
)
if tag is not None:
cmd += f" tag {tag}"
cmd += ' pcap "{path}"'.format(path=pcap_path)
# Let BPF compile errors propagate — the marker is the user's intent, so a
# bad expression must surface instead of being silently dropped.
await self._ubridge_send(cmd)
async def _ubridge_delete_marker_filter(self, bridge_name, name):
"""
Remove a `mark` filter from a uBridge bridge.
uBridge closes and flushes the filter's pcap on delete; the file itself
persists on disk for later replay.
:param bridge_name: uBridge bridge the filter is attached to
:param name: filter name previously passed to _ubridge_add_marker_filter
"""
await self._ubridge_send(f"bridge delete_packet_filter {bridge_name} {name}")
async def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=False):
"""
Creates a connection with an Ethernet interface in uBridge.