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

+79
View File
@@ -424,6 +424,85 @@ async def web_wireshark_websocket(
pass
@router.get(
"/{link_id}/markers",
dependencies=[Depends(has_privilege("Link.Audit"))]
)
async def get_markers(link: Link = Depends(dep_link)) -> dict:
"""
Return all traffic-insight markers configured on this link.
Required privilege: Link.Audit
"""
return link.markers
@router.post(
"/{link_id}/markers",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def create_marker(
marker_data: schemas.MarkerCreate,
link: Link = Depends(dep_link)
) -> dict:
"""
Attach a traffic-insight marker to the link.
On BPF match uBridge emits MARK signals and appends packets to a pcap.
Required privilege: Link.Modify
"""
await link.start_marker(
name=marker_data.name or f"marker-{link.id[:8]}",
bpf=marker_data.bpf,
tag=marker_data.tag,
)
return link.markers.get(marker_data.name, {})
@router.delete(
"/{link_id}/markers/{marker_name}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def delete_marker(
marker_name: str,
link: Link = Depends(dep_link)
) -> None:
"""
Remove a traffic-insight marker from the link.
Required privilege: Link.Modify
"""
await link.stop_marker(marker_name)
@router.put(
"/{link_id}/markers/{marker_name}",
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def update_marker(
marker_name: str,
marker_data: schemas.MarkerCreate,
link: Link = Depends(dep_link)
) -> dict:
"""
Update a traffic-insight marker (change BPF, tag, or enabled).
Required privilege: Link.Modify
"""
await link.update_marker(
name=marker_name,
bpf=marker_data.bpf if marker_data.bpf else None,
tag=marker_data.tag,
)
return link.markers.get(marker_name, {})
@router.get(
"/{link_id}/iface",
response_model=Union[schemas.UDPPortInfo, schemas.EthernetPortInfo],