marker: forward dir= from ubridge and add per-marker direction filter

Direction field in marker.match events
=======================================

Read ubridge dir=<tx|rx> from MARK signal datagrams and forward it
as a "dir" key in the marker.match notification event.  The field is
additive -- older ubridge builds omit it and the parser leaves it null,
so consumers fall back to undirected rendering with no version coupling.
Semantics are relative to the capture node (the signal's node=<id>):
tx = capture node is sending (ingressed device-side NIO), rx = it is
receiving (ingressed link-side NIO).

Per-marker direction filter (opt-in, server-side pipeline)
==========================================================

Add a direction field to MarkerCreate and MarkerDefinitionCreate
schemas ("tx" | "rx" | null).  Plumb it through the full pipeline:

  Schema -> controller (start_marker/update_marker, marker_entry,
  _markers_for_node, update_marker_definition sync)
  -> REST/MCP handlers -> compute _ubridge_add_marker_filter +
  IOU _ubridge_apply_markers -> bridge add_packet_filter dir <tx|rx>

When set, ubridge only fires the mark handler (signal + pcap) for
packets matching the chosen direction.  null (default/legacy) = both
directions -- zero behavioural change for existing markers.

Docs and tests
==============

- docs/features/marker-traffic-insight.md: signal format updated,
  new Direction section with NIO mapping, arrow mapping, and additive
  compatibility note.
- tests/compute/marker/test_marker_manager.py: 3 new parser tests
  (dir tx/rx/absent) plus existing test extended to assert dir=None.

13 files, +123/-22, 72 tests pass (zero breakage)
This commit is contained in:
YueGuobin committed 2026-08-01 14:52:07 +08:00
1 parent d729f76856
commit 6749b872fa
13 files changed
+122 -21

No files matched your search

+3 -2
View File
@@ -121,6 +121,7 @@ class Link:
name=f"global-{def_name}",
bpf=marker_def["bpf"],
tag=marker_def.get("tag"),
direction=marker_def.get("direction"),
color=marker_def.get("color"),
highlight_duration=marker_def.get("highlight_duration"),
inherited_from=def_name,
@@ -333,7 +334,7 @@ class Link:
raise NotImplementedError
async def start_marker(self, name, bpf, tag=None):
async def start_marker(self, name, bpf, tag=None, direction=None):
"""
Attach a traffic-insight marker to this link (base — UDPLink overrides).
"""
@@ -345,7 +346,7 @@ class Link:
"""
raise NotImplementedError
async def update_marker(self, name, bpf=None, tag=None, enabled=None):
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=None):
"""
Update an existing marker's BPF, tag, or enabled flag.
+6 -4
View File
@@ -930,7 +930,7 @@ class Project:
"""
return self._marker_definitions
async def create_marker_definition(self, name, bpf, tag=None, color=None, highlight_duration=None):
async def create_marker_definition(self, name, bpf, tag=None, direction=None, color=None, highlight_duration=None):
"""
Create a project-level marker definition and fan out to every existing
link that has a capable node. Links without a capable node are silently
@@ -942,12 +942,12 @@ class Project:
f"Marker definition '{name}' already exists in this project"
)
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "color": color, "highlight_duration": highlight_duration}
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "direction": direction, "color": color, "highlight_duration": highlight_duration}
await self._apply_def_to_all_links(name)
self.dump()
self.emit_notification("project.updated", self.asdict())
async def update_marker_definition(self, name, bpf=None, tag=None, color=None, highlight_duration=None):
async def update_marker_definition(self, name, bpf=None, tag=None, direction=None, color=None, highlight_duration=None):
"""
Update a marker definition and sync every inherited copy on every link.
"""
@@ -966,13 +966,15 @@ class Project:
d["color"] = color
if highlight_duration is not None:
d["highlight_duration"] = highlight_duration
if direction is not None:
d["direction"] = direction
# Sync: update every inherited copy across all links.
for link in list(self._links.values()):
marker_name = f"global-{name}"
if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name:
await link.update_marker(
marker_name, bpf=d["bpf"], tag=d.get("tag"), color=d.get("color"),
marker_name, bpf=d["bpf"], tag=d.get("tag"), direction=d.get("direction"), color=d.get("color"),
highlight_duration=d.get("highlight_duration"), inherited=True
)
self.dump()
+6 -3
View File
@@ -62,7 +62,7 @@ class UDPLink(Link):
marker only rides the NIO of the node whose uBridge will host it.
"""
return {
name: {"bpf": m["bpf"], "tag": m.get("tag"), "link_id": self._id}
name: {"bpf": m["bpf"], "tag": m.get("tag"), "link_id": self._id, "direction": m.get("direction")}
for name, m in self._markers.items()
if m.get("enabled", True) and m.get("capture_node_id") == node.id
}
@@ -322,7 +322,7 @@ class UDPLink(Link):
# explicitly deletes a marker via the REST API, and a marker is torn
# down automatically only when its link is deleted.
async def start_marker(self, name, bpf, tag=None, color=None, highlight_duration=None, inherited_from=None):
async def start_marker(self, name, bpf, tag=None, direction=None, color=None, highlight_duration=None, inherited_from=None):
"""
Attach a traffic-insight marker to this link.
@@ -358,6 +358,7 @@ class UDPLink(Link):
"color": color,
"highlight_duration": highlight_duration,
"capture_node_id": marker_side["node"].id,
"direction": direction,
}
if inherited_from:
marker_entry["inherited_from"] = inherited_from
@@ -396,7 +397,7 @@ class UDPLink(Link):
self._project.emit_notification("link.updated", self.asdict())
self._project.dump()
async def update_marker(self, name, bpf=None, tag=None, enabled=None, color=None, highlight_duration=None, inherited=False):
async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=None, color=None, highlight_duration=None, inherited=False):
"""
Update an existing marker's BPF/tag/enabled/color. Any change pushes via
``self.update()``; uBridge picks up the new params on the next NIO
@@ -436,6 +437,8 @@ class UDPLink(Link):
marker_info["color"] = color
if highlight_duration is not None:
marker_info["highlight_duration"] = highlight_duration
if direction is not None:
marker_info["direction"] = direction
if self._created:
await self.update()