diff --git a/docs/features/marker-traffic-insight.md b/docs/features/marker-traffic-insight.md index 04d638282..b275c02b7 100644 --- a/docs/features/marker-traffic-insight.md +++ b/docs/features/marker-traffic-insight.md @@ -340,6 +340,8 @@ direction relative to the capture node; see [Direction](#direction). filter, the pcap filename, and `MARK` signal routing — so rename is a delete + recreate, not a field update. PUT ignores the body `name`; the `{name}` path parameter identifies the target, and only `bpf / tag / color / enabled / highlight_duration` are changeable. + Names are 1–32 chars (`[A-Za-z0-9][A-Za-z0-9_.-]*`); inherited copies carry a `global-` + prefix, so their filter names reach ~39. - **`global` prefix reserved.** User-chosen names may not start with `global`; inherited markers are stored as `global-{definition_name}` so the two namespaces cannot collide. Omitting `name` on create yields an auto-generated, prefix-free name. diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 76d543364..aaf3da9a1 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -1115,7 +1115,10 @@ class BaseNode: # marker definitions (inherit_marker). The prefix is only forbidden at the # user-facing schema layer, not at the uBridge boundary. _MARKER_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$") - if not _MARKER_NAME_RE.match(name): + # Defense-in-depth vs hand-edited topology: the user-facing name is capped + # at 32 by the schema; inherited copies carry a ``global-`` prefix (≤ 39), + # so allow up to 48 here. + if not _MARKER_NAME_RE.match(name) or len(name) > 48: raise UbridgeError(f"Invalid marker name: {name!r}") cmd = 'bridge add_packet_filter {bridge} {name} mark "{bpf}"'.format( bridge=bridge_name, name=name, bpf=bpf diff --git a/gns3server/schemas/controller/links.py b/gns3server/schemas/controller/links.py index dd4b4ce5c..853293f8c 100644 --- a/gns3server/schemas/controller/links.py +++ b/gns3server/schemas/controller/links.py @@ -152,7 +152,7 @@ class MarkerCreate(BaseModel): name: Optional[str] = Field( None, pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]*$", - max_length=128, + max_length=32, description='Unique marker name on the link. Auto-generated when absent.', ) bpf: str @@ -236,7 +236,7 @@ class MarkerDefinitionCreate(BaseModel): name: Optional[str] = Field( None, pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]*$", - max_length=128, + max_length=32, description="Unique definition name. Auto-generated when absent.", ) bpf: str diff --git a/tests/api/routes/controller/test_markers.py b/tests/api/routes/controller/test_markers.py index d9fc27b3b..53e57e7d9 100644 --- a/tests/api/routes/controller/test_markers.py +++ b/tests/api/routes/controller/test_markers.py @@ -105,6 +105,17 @@ class TestMarkerRoutes: ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async def test_create_marker_name_too_long_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = UDPLink(project) + project._links = {link.id: link} + + response = await client.post( + app.url_path_for("create_marker", project_id=project.id, link_id=link.id), + json={"name": "x" * 33, "bpf": "icmp"}, # max_length is 32 + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + async def test_get_markers(self, app: FastAPI, client: AsyncClient, project: Project) -> None: link = UDPLink(project)