mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 20:40:13 +03:00
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:
parent
210103058f
commit
72dc10a669
@ -85,7 +85,11 @@ bridge start {node_id}-{port}
|
||||
```
|
||||
|
||||
Captures and marker signals are applied via the existing `_ubridge_apply_filters`
|
||||
and `_ubridge_apply_markers` helpers from `BaseNode`.
|
||||
and `_ubridge_apply_markers` helpers from `BaseNode`. The controller therefore allows
|
||||
packet filters and traffic-insight markers on switch links (including switch-to-switch):
|
||||
`ethernet_switch` is a marker/filter-capable node type, and the compute API exposes the
|
||||
matching NIO-update and per-marker endpoints. The `ethernet_hub` — still Dynamips-hosted,
|
||||
no uBridge — remains excluded.
|
||||
|
||||
### `remove_nio(port_number)`
|
||||
|
||||
|
||||
@ -97,8 +97,8 @@ IOU runs a single `IOL-BRIDGE` per node shared by every interface, so `bridge`+`
|
||||
identical across that node's links. uBridge keeps a separate filter list **per port
|
||||
(bay/unit)** within the bridge, so each interface gets its own `global-{name}` filter, its own
|
||||
pcap file, and its own `link=`. The shared bridge name is irrelevant to attribution. Other
|
||||
capable node types (`qemu`, `docker`, `vpcs`, `cloud`) already use one bridge per link; `link`
|
||||
applies uniformly to all of them.
|
||||
capable node types (`qemu`, `docker`, `vpcs`, `cloud`, `ethernet_switch`) already use one
|
||||
bridge per link (the switch's per-port relay); `link` applies uniformly to all of them.
|
||||
|
||||
## Direction
|
||||
|
||||
@ -137,7 +137,7 @@ pass `capture_node_id` on marker **create**:
|
||||
```
|
||||
|
||||
The value must be one of the link's two endpoints and a marker-capable type (`vpcs`, `qemu`,
|
||||
`docker`, `iou`, `dynamips`, `cloud`); any other id is rejected with `409`. Omit it to keep
|
||||
`docker`, `iou`, `dynamips`, `cloud`, `ethernet_switch`); any other id is rejected with `409`. Omit it to keep
|
||||
the auto-pick. The chosen id is echoed back as `capture_node_id` in the marker entry and in
|
||||
each `MARK` signal's `node=<id>`, so the Web UI always knows the observer regardless of who
|
||||
picked it.
|
||||
@ -357,7 +357,9 @@ direction relative to the capture node; see [Direction](#direction).
|
||||
runs one `tcpdump -d` rather than *N*. (uBridge still runs `pcap_compile` itself at
|
||||
install time, so an invalid expression can never slip through.)
|
||||
- **Supported node types.** A marker needs a uBridge bridge: `vpcs`, `qemu`, `docker`,
|
||||
`iou`, `dynamips`, `cloud` (one capable endpoint suffices). Types without a uBridge are
|
||||
`iou`, `dynamips`, `cloud`, `ethernet_switch` (one capable endpoint suffices). The
|
||||
`ethernet_switch` hosts markers on its per-port uBridge relays (brctl backend); the
|
||||
`ethernet_hub` is still Dynamips-hosted and has no uBridge. Types without a uBridge are
|
||||
silently skipped by the inheritance fan-out. IOU uses one shared `IOL-BRIDGE` per node but
|
||||
keeps filters, pcap files, and `link=` ids per port, so multi-interface nodes are handled
|
||||
(see [Per-link attribution](#per-link-attribution)).
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -382,6 +382,7 @@ class TestEthernetSwitchNodesRoutes:
|
||||
response = await compute_client.delete(url)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# the port's relay bridge and TAP are released from the kernel bridge
|
||||
br = node._bridge_name
|
||||
tap = f"{br}-0"
|
||||
relay = f"{node.id}-0"
|
||||
@ -390,6 +391,92 @@ class TestEthernetSwitchNodesRoutes:
|
||||
call(f"bridge delete {relay}"),
|
||||
])
|
||||
|
||||
async def test_ethernet_switch_update_nio(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
compute_project: Project,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
params = self._udp_params()
|
||||
params["filters"] = {"delay": [10, 0]}
|
||||
response = await compute_client.post(url, json=params)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._ubridge_send.reset_mock()
|
||||
|
||||
params["filters"] = {"packet_loss": [10]}
|
||||
params["markers"] = {}
|
||||
url = app.url_path_for(
|
||||
"compute:update_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
response = await compute_client.put(url, json=params)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["filters"] == {"packet_loss": [10]}
|
||||
|
||||
# update_nio re-applies the filters on the port's uBridge relay
|
||||
relay = node._ubridge_bridge_name(0)
|
||||
node._ubridge_send.assert_any_call(f"bridge reset_packet_filters {relay}")
|
||||
|
||||
async def test_ethernet_switch_toggle_marker(
|
||||
self,
|
||||
app: FastAPI,
|
||||
compute_client: AsyncClient,
|
||||
compute_project: Project,
|
||||
ethernet_switch: dict
|
||||
) -> None:
|
||||
|
||||
# a marker installed via the NIO registers in the node's filter-bridge map
|
||||
url = app.url_path_for(
|
||||
"compute:create_ethernet_switch_nio",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
adapter_number="0",
|
||||
port_number="0"
|
||||
)
|
||||
params = self._udp_params()
|
||||
params["markers"] = {"icmp": {"bpf": "icmp", "link_id": "link-1", "enabled": True}}
|
||||
response = await compute_client.post(url, json=params)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
node = compute_project.get_node(ethernet_switch["node_id"])
|
||||
node._ubridge_send.reset_mock()
|
||||
|
||||
url = app.url_path_for(
|
||||
"compute:toggle_ethernet_switch_marker",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
marker_name="icmp"
|
||||
)
|
||||
response = await compute_client.put(url, json={"enabled": False})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"marker_name": "icmp", "enabled": False}
|
||||
relay = node._ubridge_bridge_name(0)
|
||||
node._ubridge_send.assert_any_call(f"bridge enable_packet_filter {relay} icmp off")
|
||||
|
||||
# toggling an unknown marker is a 404
|
||||
url = app.url_path_for(
|
||||
"compute:toggle_ethernet_switch_marker",
|
||||
project_id=ethernet_switch["project_id"],
|
||||
node_id=ethernet_switch["node_id"],
|
||||
marker_name="nope"
|
||||
)
|
||||
response = await compute_client.put(url, json={"enabled": True})
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
async def test_ethernet_switch_start_capture(
|
||||
self,
|
||||
app: FastAPI,
|
||||
|
||||
@ -378,9 +378,9 @@ async def test_available_filters(project, compute):
|
||||
link.create = AsyncioMagicMock()
|
||||
assert link.available_filters() == []
|
||||
|
||||
# Ethernet switch is not supported should return 0 filters
|
||||
# The brctl Ethernet switch hosts filters on its per-port uBridge relays
|
||||
await link.add_node(node1, 0, 4)
|
||||
assert link.available_filters() == []
|
||||
assert len(link.available_filters()) > 0
|
||||
|
||||
node2 = Node(project, compute, "node2", node_type="vpcs")
|
||||
node2._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||
|
||||
@ -56,20 +56,21 @@ def _valid_bpf():
|
||||
return stack
|
||||
|
||||
|
||||
async def _make_link(project, port_cls=EthernetPort):
|
||||
"""Build a created UDPLink between two VPCS nodes on a mocked compute.
|
||||
async def _make_link(project, port_cls=EthernetPort, node_types=("vpcs", "vpcs")):
|
||||
"""Build a created UDPLink between two nodes on a mocked compute.
|
||||
|
||||
``port_cls`` defaults to EthernetPort; pass SerialPort for a serial link
|
||||
(the link's link_type follows the port).
|
||||
(the link's link_type follows the port). ``node_types`` overrides the
|
||||
endpoint node types (e.g. a switch-to-switch link).
|
||||
"""
|
||||
|
||||
compute = MagicMock()
|
||||
compute.id = "local"
|
||||
compute.host = "example.com"
|
||||
|
||||
node1 = Node(project, compute, "n1", node_type="vpcs")
|
||||
node1 = Node(project, compute, "n1", node_type=node_types[0])
|
||||
node1._ports = [port_cls("E0", 0, 0, 0)]
|
||||
node2 = Node(project, compute, "n2", node_type="vpcs")
|
||||
node2 = Node(project, compute, "n2", node_type=node_types[1])
|
||||
node2._ports = [port_cls("E0", 0, 0, 1)]
|
||||
|
||||
async def subnet(_other):
|
||||
@ -112,6 +113,23 @@ async def test_start_marker_stores_entry(project):
|
||||
assert entry["highlight_duration"] == 800
|
||||
assert entry["enabled"] is True
|
||||
assert entry["capture_node_id"] in {n["node"].id for n in link._nodes}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_marker_on_ethernet_switch_link(project):
|
||||
"""A switch-to-switch link can host a marker: the brctl Ethernet switch
|
||||
runs a per-port uBridge relay the `mark` filter attaches to."""
|
||||
|
||||
with _valid_bpf():
|
||||
link = await _make_link(project, node_types=("ethernet_switch", "ethernet_switch"))
|
||||
await link.start_marker("icmp", "icmp")
|
||||
|
||||
entry = link.markers["icmp"]
|
||||
switch_ids = {n["node"].id for n in link._nodes}
|
||||
assert entry["capture_node_id"] in switch_ids
|
||||
# the marker rides exactly one side's NIO and is pushed via update()
|
||||
carrying = [d for d in link._link_data if "icmp" in d["markers"]]
|
||||
assert len(carrying) == 1
|
||||
assert "inherited_from" not in entry
|
||||
|
||||
|
||||
|
||||
@ -451,6 +451,55 @@ async def test_update(project):
|
||||
}, timeout=120)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_ethernet_switch_nio(project):
|
||||
"""
|
||||
Link updates must reach an Ethernet switch endpoint: the brctl switch has
|
||||
a PUT NIO route, so only the Dynamips-hosted hub side stays skipped.
|
||||
"""
|
||||
|
||||
compute1 = MagicMock()
|
||||
|
||||
node_vpcs = Node(project, compute1, "node1", node_type="vpcs")
|
||||
node_vpcs._ports = [EthernetPort("E0", 0, 0, 4)]
|
||||
node_switch = Node(project, compute1, "node2", node_type="ethernet_switch")
|
||||
node_switch._ports = [EthernetPort("E0", 0, 3, 1)]
|
||||
|
||||
async def subnet_callback(compute2):
|
||||
return ("192.168.1.1", "192.168.1.2")
|
||||
|
||||
compute1.get_ip_on_same_subnet.side_effect = subnet_callback
|
||||
|
||||
async def compute1_callback(path, data={}, **kwargs):
|
||||
if "/ports/udp" in path:
|
||||
response = MagicMock()
|
||||
response.json = {"udp_port": 1024}
|
||||
return response
|
||||
|
||||
compute1.post.side_effect = compute1_callback
|
||||
compute1.put = AsyncioMagicMock()
|
||||
compute1.host = "example.com"
|
||||
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node_vpcs, 0, 4)
|
||||
await link.add_node(node_switch, 3, 1)
|
||||
assert link.created
|
||||
|
||||
await link.update_filters({"delay": [10, 0]})
|
||||
compute1.put.assert_any_call(
|
||||
"/projects/{}/ethernet_switch/nodes/{}/adapters/3/ports/1/nio".format(project.id, node_switch.id),
|
||||
data={
|
||||
"lport": 1024,
|
||||
"rhost": "192.168.1.1",
|
||||
"rport": 1024,
|
||||
"type": "nio_udp",
|
||||
"suspend": False,
|
||||
"markers": {},
|
||||
"filters": {}
|
||||
}, timeout=221
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_suspend(project):
|
||||
compute1 = MagicMock()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user