mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-05 23:01:15 +02:00
marker: serial-link (WAN) support via data_link_type -> uBridge linktype
Markers now work on serial links (Cisco HDLC / PPP / Frame Relay / ATM), not just Ethernet. A marker carries a data_link_type (default DLT_EN10MB); at the uBridge boundary it becomes the 'mark ... linktype <dlt>' keyword so the BPF compiles and the pcap is written with the matching link-layer. - MarkerCreate / MarkerDefinitionCreate gain data_link_type (default DLT_EN10MB). Per-link it is create-only; definitions are updatable (a change re-fans-out). - base_node._marker_linktype() normalizes the GNS3 DLT name (strip DLT_, uppercase, None for EN10MB). Single source is SerialPort.data_link_types, so Cisco PPP -> PPP_SERIAL (50), matching the capture path -- no second mapping table. - _ubridge_add_marker_filter (generic) and the IOU marker loop append 'linktype <dlt>'. - Definition fan-out branches on link_type in inherit_marker: Ethernet is always EN10MB; a serial link uses the definition's WAN encapsulation, or is SKIPPED when none was chosen (an EN10MB pcap on serial is undecodable). One definition covers a mixed topology. - MCP marker_definition exposes data_link_type (None = not forwarded). - No uBridge rebuild on a data_link_type change -- only that one marker's filter is swapped (delete + re-add), mirroring a BPF change; reset_packet_filters preserves sibling mark filters. Requires the uBridge build with 'mark ... linktype' support.
This commit is contained in:
11 files changed
+231
-29
No files matched your search
@@ -271,6 +271,7 @@ async def create_marker_definition(
|
||||
direction=def_data.direction,
|
||||
color=def_data.color,
|
||||
highlight_duration=def_data.highlight_duration,
|
||||
data_link_type=def_data.data_link_type,
|
||||
)
|
||||
return project.marker_definitions.get(name, {})
|
||||
|
||||
@@ -297,6 +298,7 @@ async def update_marker_definition(
|
||||
direction=def_data.direction if "direction" in def_data.model_fields_set else _UNSET,
|
||||
color=def_data.color,
|
||||
highlight_duration=def_data.highlight_duration,
|
||||
data_link_type=def_data.data_link_type if "data_link_type" in def_data.model_fields_set else _UNSET,
|
||||
)
|
||||
return project.marker_definitions.get(def_name, {})
|
||||
|
||||
|
||||
@@ -1052,6 +1052,7 @@ async def marker_definition(
|
||||
tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None,
|
||||
color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None,
|
||||
highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None,
|
||||
data_link_type: Annotated[str | None, Field(description="pcap link-layer type for serial links (DLT_C_HDLC / DLT_PPP_SERIAL / DLT_FRELAY / DLT_ATM_RFC1483). Omit = Ethernet-only (serial links skipped); setting it also covers serial links with that encapsulation")] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Manage project-level marker definitions — traffic-insight rules that apply to ALL links.
|
||||
|
||||
@@ -1060,7 +1061,7 @@ async def marker_definition(
|
||||
On delete, 'global-{name}' is removed from every link.
|
||||
|
||||
Create requires: project_id, action='create', bpf
|
||||
Update requires: project_id, action='update', def_name, and at least one of (bpf, tag, color, highlight_duration)
|
||||
Update requires: project_id, action='update', def_name, and at least one of (bpf, tag, color, highlight_duration, data_link_type)
|
||||
Delete requires: project_id, action='delete', def_name
|
||||
List requires: project_id, action='list'
|
||||
|
||||
@@ -1073,7 +1074,7 @@ async def marker_definition(
|
||||
Common BPF examples: 'arp', 'icmp', 'ospf', 'tcp port 22', 'udp port 53'
|
||||
"""
|
||||
params = {"project_id": project_id, "action": action}
|
||||
for opt in ("bpf", "def_name", "name", "tag", "color", "highlight_duration"):
|
||||
for opt in ("bpf", "def_name", "name", "tag", "color", "highlight_duration", "data_link_type"):
|
||||
val = locals().get(opt)
|
||||
if val is not None:
|
||||
params[opt] = val
|
||||
|
||||
@@ -457,7 +457,7 @@ def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
|
||||
if not bpf:
|
||||
return {"error": "bpf is required for create action"}
|
||||
body: dict[str, Any] = {"bpf": bpf}
|
||||
for opt in ("name", "tag", "color", "highlight_duration"):
|
||||
for opt in ("name", "tag", "color", "highlight_duration", "data_link_type"):
|
||||
if params.get(opt) is not None:
|
||||
body[opt] = params[opt]
|
||||
# No direction: a definition fans out to every link and auto-selects its
|
||||
@@ -473,11 +473,11 @@ def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
|
||||
|
||||
if action == "update":
|
||||
body = {}
|
||||
for opt in ("bpf", "tag", "color", "highlight_duration"):
|
||||
for opt in ("bpf", "tag", "color", "highlight_duration", "data_link_type"):
|
||||
if params.get(opt) is not None:
|
||||
body[opt] = params[opt]
|
||||
if not body:
|
||||
return {"error": "At least one update field is required (bpf, tag, color, highlight_duration)"}
|
||||
return {"error": "At least one update field is required (bpf, tag, color, highlight_duration, data_link_type)"}
|
||||
return conn.http_call("put", url, json_data=body).json()
|
||||
|
||||
# action == "delete"
|
||||
|
||||
@@ -1091,7 +1091,25 @@ class BaseNode:
|
||||
)
|
||||
i += 1
|
||||
|
||||
async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None, direction=None):
|
||||
@staticmethod
|
||||
def _marker_linktype(data_link_type):
|
||||
"""
|
||||
Normalize a GNS3 pcap data-link type (e.g. ``DLT_C_HDLC``) to the bare
|
||||
uBridge ``linktype`` token (``C_HDLC``) by stripping the ``DLT_`` prefix.
|
||||
Returns ``None`` for Ethernet (``DLT_EN10MB`` / unset) so the ``linktype``
|
||||
keyword is omitted and uBridge defaults to EN10MB. Values come straight
|
||||
from ``SerialPort.data_link_types`` (the single source of truth); uBridge
|
||||
resolves them with ``pcap_datalink_name_to_val``, which is case-sensitive
|
||||
and expects the canonical uppercase form.
|
||||
"""
|
||||
if not data_link_type:
|
||||
return None
|
||||
dlt = data_link_type.upper()
|
||||
if dlt.startswith("DLT_"):
|
||||
dlt = dlt[4:]
|
||||
return None if dlt == "EN10MB" else dlt
|
||||
|
||||
async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None, direction=None, data_link_type=None):
|
||||
"""
|
||||
Attach a `mark` packet filter to a uBridge bridge for traffic insight.
|
||||
|
||||
@@ -1132,6 +1150,9 @@ class BaseNode:
|
||||
cmd += f" link {link_id}"
|
||||
if direction is not None:
|
||||
cmd += f" dir {direction}"
|
||||
linktype = self._marker_linktype(data_link_type)
|
||||
if linktype is not None:
|
||||
cmd += f" linktype {linktype}"
|
||||
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.
|
||||
@@ -1232,7 +1253,8 @@ class BaseNode:
|
||||
)
|
||||
try:
|
||||
await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag, link_id,
|
||||
direction=spec.get("direction"))
|
||||
direction=spec.get("direction"),
|
||||
data_link_type=spec.get("data_link_type"))
|
||||
except UbridgeError as e:
|
||||
# Swallow BPF compile errors (warn + skip) so a single bad
|
||||
# expression can't break link creation / node restart — mirrors
|
||||
|
||||
@@ -1311,6 +1311,9 @@ class IOUVM(BaseNode):
|
||||
direction = spec.get("direction")
|
||||
if direction is not None:
|
||||
cmd += f" dir {direction}"
|
||||
linktype = self._marker_linktype(spec.get("data_link_type"))
|
||||
if linktype is not None:
|
||||
cmd += f" linktype {linktype}"
|
||||
cmd += ' pcap "{path}"'.format(path=pcap_path)
|
||||
try:
|
||||
await self._ubridge_send(cmd)
|
||||
|
||||
@@ -122,13 +122,27 @@ class Link:
|
||||
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),
|
||||
|
||||
@@ -1000,7 +1000,7 @@ class Project:
|
||||
"For a capture-node-relative direction on a single link, use a per-link marker."
|
||||
)
|
||||
|
||||
async def create_marker_definition(self, name, bpf, tag=None, direction=None, color=None, highlight_duration=None):
|
||||
async def create_marker_definition(self, name, bpf, tag=None, direction=None, color=None, highlight_duration=None, data_link_type="DLT_EN10MB"):
|
||||
"""
|
||||
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
|
||||
@@ -1014,12 +1014,12 @@ class Project:
|
||||
|
||||
self._validate_marker_definition_bpf(name, bpf)
|
||||
self._validate_marker_definition_direction(name, direction)
|
||||
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "direction": direction, "color": color, "highlight_duration": highlight_duration, "paused": False}
|
||||
self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "direction": direction, "color": color, "highlight_duration": highlight_duration, "data_link_type": data_link_type, "paused": False}
|
||||
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, direction=_UNSET, color=None, highlight_duration=None):
|
||||
async def update_marker_definition(self, name, bpf=None, tag=None, direction=_UNSET, color=None, highlight_duration=None, data_link_type=_UNSET):
|
||||
"""
|
||||
Update a marker definition and sync every inherited copy on every link.
|
||||
"""
|
||||
@@ -1042,15 +1042,33 @@ class Project:
|
||||
if direction is not _UNSET:
|
||||
self._validate_marker_definition_direction(name, direction)
|
||||
d["direction"] = direction # None = clear back to both directions
|
||||
if data_link_type is not _UNSET:
|
||||
d["data_link_type"] = data_link_type
|
||||
|
||||
# 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"), direction=d.get("direction"), color=d.get("color"),
|
||||
highlight_duration=d.get("highlight_duration"), inherited=True
|
||||
)
|
||||
if data_link_type is not _UNSET:
|
||||
# data_link_type decides which links host an inherited copy (serial
|
||||
# links are skipped unless a WAN encapsulation is chosen), so a change
|
||||
# needs a full re-fan-out: drop every copy, then re-apply.
|
||||
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:
|
||||
try:
|
||||
await link.stop_marker(marker_name, inherited=True)
|
||||
except ControllerError:
|
||||
log.warning(
|
||||
"Failed to remove inherited marker %s from link %s",
|
||||
marker_name, link.id
|
||||
)
|
||||
await self._apply_def_to_all_links(name)
|
||||
else:
|
||||
# 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"), direction=d.get("direction"), color=d.get("color"),
|
||||
highlight_duration=d.get("highlight_duration"), inherited=True
|
||||
)
|
||||
self.dump()
|
||||
self.emit_notification("project.updated", self.asdict())
|
||||
|
||||
|
||||
@@ -57,15 +57,18 @@ class UDPLink(Link):
|
||||
|
||||
def _markers_for_node(self, node):
|
||||
"""
|
||||
Marker specs (name -> {bpf, tag, link_id, direction, enabled}) for the
|
||||
markers whose capture side is ``node``. Routed by capture_node_id so a
|
||||
marker only rides the NIO of the node whose uBridge will host it. A
|
||||
disabled marker is included (installed then turned ``off`` at uBridge,
|
||||
not dropped) so the UI can toggle it instantly without an NIO rebuild.
|
||||
Marker specs (name -> {bpf, tag, link_id, direction, data_link_type,
|
||||
enabled}) for the markers whose capture side is ``node``. Routed by
|
||||
capture_node_id so a marker only rides the NIO of the node whose uBridge
|
||||
will host it. A disabled marker is included (installed then turned
|
||||
``off`` at uBridge, not dropped) so the UI can toggle it instantly
|
||||
without an NIO rebuild.
|
||||
"""
|
||||
return {
|
||||
name: {"bpf": m["bpf"], "tag": m.get("tag"), "link_id": self._id,
|
||||
"direction": m.get("direction"), "enabled": m.get("enabled", True)}
|
||||
"direction": m.get("direction"),
|
||||
"data_link_type": m.get("data_link_type", "DLT_EN10MB"),
|
||||
"enabled": m.get("enabled", True)}
|
||||
for name, m in self._markers.items()
|
||||
if m.get("capture_node_id") == node.id
|
||||
}
|
||||
@@ -350,7 +353,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, direction=None, capture_node_id=None, color=None, highlight_duration=None, enabled=True, inherited_from=None):
|
||||
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):
|
||||
"""
|
||||
Attach a traffic-insight marker to this link.
|
||||
|
||||
@@ -401,6 +404,7 @@ class UDPLink(Link):
|
||||
"highlight_duration": highlight_duration,
|
||||
"capture_node_id": marker_side["node"].id,
|
||||
"direction": direction,
|
||||
"data_link_type": data_link_type,
|
||||
}
|
||||
if inherited_from:
|
||||
marker_entry["inherited_from"] = inherited_from
|
||||
|
||||
@@ -189,6 +189,19 @@ class MarkerCreate(BaseModel):
|
||||
"Omitted = server auto-picks (first started marker-capable endpoint)."
|
||||
),
|
||||
)
|
||||
data_link_type: str = Field(
|
||||
"DLT_EN10MB",
|
||||
description=(
|
||||
"pcap link-layer type the marker's BPF compiles against and its "
|
||||
"capture file is written with (a uBridge `linktype` token). Defaults "
|
||||
"to DLT_EN10MB (Ethernet), which is omitted from the uBridge command. "
|
||||
"Only meaningful for serial links: set it to the matching serial DLT "
|
||||
"from the port's data_link_types — DLT_C_HDLC / DLT_PPP_SERIAL / "
|
||||
"DLT_FRELAY / DLT_ATM_RFC1483 — so the BPF offsets and pcap decode "
|
||||
"match the encapsulation configured in IOS. Create-only (changing it "
|
||||
"would invalidate the pcap)."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("direction", mode="before")
|
||||
@classmethod
|
||||
@@ -259,6 +272,17 @@ class MarkerDefinitionCreate(BaseModel):
|
||||
pattern=r"^(tx|rx|both)$",
|
||||
description="Direction filter: 'tx' = capture node sending only, 'rx' = capture node receiving only, 'both' or null = both directions.",
|
||||
)
|
||||
data_link_type: str = Field(
|
||||
"DLT_EN10MB",
|
||||
description=(
|
||||
"pcap link-layer type for inherited markers on serial links (uBridge "
|
||||
"`linktype`). Defaults to DLT_EN10MB (Ethernet): the definition then "
|
||||
"applies only to Ethernet links and serial links are skipped. Set a "
|
||||
"serial DLT — DLT_C_HDLC / DLT_PPP_SERIAL / DLT_FRELAY / "
|
||||
"DLT_ATM_RFC1483 — to also cover serial links with that encapsulation; "
|
||||
"Ethernet links stay EN10MB regardless. Changing it re-fans-out."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("direction", mode="before")
|
||||
@classmethod
|
||||
|
||||
Reference in new issue
Block a user