controller: allow markers and packet filters on Ethernet switch links

The brctl Ethernet switch runs a per-port uBridge relay, so it can host
the mark filter and packet filters like any uBridge-backed node. Add
ethernet_switch to _MARKER_CAPABLE_TYPES and _get_filter_node, narrow the
UDPLink.update() NIO-PUT skip down to the Dynamips-hosted ethernet_hub,
and expose the matching compute endpoints: PUT nio (filter/marker
reapply) plus the per-marker toggle/pause/resume/delete/rebuild routes.

The ethernet_hub keeps its exclusion: its routes still wire into the
Dynamips hub, which has no uBridge of its own.
This commit is contained in:
YueGuobin committed 2026-08-21 21:41:51 +08:00
1 parent 210103058f
commit 72dc10a669
9 files changed
+282 -17

No files matched your search

@@ -192,6 +192,33 @@ async def create_ethernet_switch_nio(
return nio.asdict()
@router.put(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
)
async def update_ethernet_switch_nio(
*,
adapter_number: int = Path(..., ge=0, le=0),
port_number: int,
nio_data: schemas.UDPNIO,
node: EthernetSwitch = Depends(dep_node)
) -> schemas.UDPNIO:
"""
Update a NIO (Network Input/Output) on the node: re-apply the packet
filters and traffic-insight markers carried by the NIO onto the port's
uBridge relay. The adapter number on the switch is always 0.
"""
nio = node.get_nio(port_number)
nio.filters.clear()
if nio_data.filters:
nio.filters = nio_data.filters
nio.markers = nio_data.markers or {}
await node.update_nio(port_number, nio)
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
async def delete_ethernet_switch_nio(
*,
@@ -257,3 +284,75 @@ async def stream_pcap_file(
nio = node.get_nio(port_number)
stream = Builtin.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.put("/{node_id}/markers/{marker_name}")
async def toggle_ethernet_switch_marker(
marker_name: str,
toggle_data: schemas.MarkerToggle,
node: EthernetSwitch = Depends(dep_node)
) -> dict:
"""
Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2).
"""
if not any(n == marker_name for (n, lid) in node._marker_filter_bridges):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Marker '{marker_name}' is not installed on this node",
)
await node._ubridge_set_marker_filter_state(marker_name, toggle_data.enabled)
return {"marker_name": marker_name, "enabled": toggle_data.enabled}
@router.post("/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT)
async def pause_ethernet_switch_markers(node: EthernetSwitch = Depends(dep_node)) -> None:
await node._ubridge_marker_pause()
@router.post("/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT)
async def resume_ethernet_switch_markers(node: EthernetSwitch = Depends(dep_node)) -> None:
await node._ubridge_marker_resume()
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_ethernet_switch_marker_capture(
*,
marker_name: str,
adapter_number: int = Path(..., ge=0, le=0),
port_number: int,
link_id: str = "",
node: EthernetSwitch = Depends(dep_node)
) -> None:
"""
Delete a marker's capture pcap (called by the controller when the marker is
removed) so the file is cleaned up even with the switch stopped. Also drops
the marker from the port NIO's cached spec so a switch restart won't
reinstall it (and recreate an empty pcap). The adapter number is always 0.
"""
nio = node.get_nio(port_number)
await node.delete_marker_capture(marker_name, link_id, nio)
@router.put("/{node_id}/markers/{marker_name}/rebuild")
async def rebuild_ethernet_switch_marker(
marker_name: str,
rebuild_data: schemas.MarkerRebuild,
node: EthernetSwitch = Depends(dep_node)
) -> dict:
"""
Re-install a single marker filter with new BPF/tag/direction (delete + add,
no bridge reset) so sibling markers' pcaps stay open.
"""
await node.rebuild_marker_filter(
marker_name, rebuild_data.link_id, rebuild_data.bpf,
rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled,
)
return {"marker_name": marker_name}
+2
View File
@@ -624,6 +624,8 @@ class Link:
"nat",
"virtualbox",
"docker",
# the brctl Ethernet switch applies filters on its per-port uBridge relays
"ethernet_switch",
):
return node["node"]
return None
+9 -5
View File
@@ -24,12 +24,13 @@ from .link import Link, _UNSET
from .node_types import BUILTIN_NODE_TYPES
from gns3server.utils.packet_filter_validation import validate_bpf_syntax, FilterValidationError
# Node types without a uBridge bridge — a marker filter has nothing to attach to.
# Node types that can host a marker (have a uBridge bridge to attach the
# `mark` filter to). Mirrors _get_filter_node in link.py, minus "nat"
# (which has no uBridge).
# (which has no uBridge) and "ethernet_hub" (still Dynamips-hosted, no
# uBridge of its own). "ethernet_switch" hosts markers on the per-port
# uBridge relays of its brctl kernel-bridge backend.
_MARKER_CAPABLE_TYPES = frozenset({
"vpcs", "qemu", "docker", "iou", "dynamips", "cloud",
"vpcs", "qemu", "docker", "iou", "dynamips", "cloud", "ethernet_switch",
})
@@ -234,7 +235,10 @@ class UDPLink(Link):
self._link_data[0]["filters"] = node1_filters
self._link_data[0]["markers"] = node1_markers
self._link_data[0]["suspend"] = self._suspended
if node1.node_type not in ("ethernet_switch", "ethernet_hub"):
# The Ethernet hub is still Dynamips-hosted (no uBridge of its own and
# no PUT NIO route) — keep skipping its side. Every other node type,
# including the brctl Ethernet switch, re-applies via the NIO update.
if node1.node_type != "ethernet_hub":
await node1.put(
f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=self._link_data[0], timeout=120
)
@@ -244,7 +248,7 @@ class UDPLink(Link):
self._link_data[1]["filters"] = node2_filters
self._link_data[1]["markers"] = node2_markers
self._link_data[1]["suspend"] = self._suspended
if node2.node_type not in ("ethernet_switch", "ethernet_hub"):
if node2.node_type != "ethernet_hub":
await node2.put(
f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=221
)