feat: migrate builtin EthernetSwitch from Dynamips ethsw to ubridge brctl

Replace the builtin EthernetSwitch stub with a Linux kernel bridge backed
by uBridge's brctl module. Each switch node creates one kernel bridge
(gns3br{N}) with VLAN filtering; each port is a persistent TAP enslaved to
the bridge, relayed by a per-port uBridge bridge (nio_tap <-> nio_udp).

- Access/dot1q/qinq port modes translated to brctl vlan primitives
- Compute router repointed from Dynamips to Builtin manager
- Tests updated: 21 router-level tests + 215 surrounding tests pass
- Real-kernel e2e verified: access 100 PVID untagged, dot1q trunk
  VIDs 1-4094 + native 1, qinq 802.1ad proto + 200 PVID

Ethertype 0x9100/0x9200 handling and port-count guard relaxation are
deferred pending resolution.
This commit is contained in:
YueGuobin committed 2026-07-18 00:21:19 +08:00
1 parent 22a1026633
commit fd4ac4460d
3 files changed
+652 -254

No files matched your search

@@ -16,6 +16,10 @@
"""
API routes for Ethernet switch nodes.
The Ethernet switch is a builtin node backed by a Linux kernel bridge driven
through uBridge's ``brctl`` module (see
``gns3server.compute.builtin.nodes.ethernet_switch``).
"""
import os
@@ -25,8 +29,8 @@ from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.ethernet_switch import EthernetSwitch
from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.ethernet_switch import EthernetSwitch
from gns3server import schemas
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}}
@@ -39,8 +43,8 @@ def dep_node(project_id: UUID, node_id: UUID) -> EthernetSwitch:
Dependency to retrieve a node.
"""
dynamips_manager = Dynamips.instance()
node = dynamips_manager.get_node(str(node_id), project_id=str(project_id))
builtin_manager = Builtin.instance()
node = builtin_manager.get_node(str(node_id), project_id=str(project_id))
return node
@@ -55,10 +59,9 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
Create a new Ethernet switch.
"""
# Use the Dynamips Ethernet switch to simulate this node
dynamips_manager = Dynamips.instance()
builtin_manager = Builtin.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
node = await dynamips_manager.create_node(
node = await builtin_manager.create_node(
node_data.pop("name"),
str(project_id),
node_data.get("node_id"),
@@ -67,7 +70,7 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
node_type="ethernet_switch",
ports=node_data.get("ports_mapping"),
)
node.usage = node_data.get("usage", "")
return node.asdict()
@@ -86,7 +89,7 @@ async def duplicate_ethernet_switch(
Duplicate an Ethernet switch.
"""
new_node = await Dynamips.instance().duplicate_node(node.id, str(destination_node_id))
new_node = await Builtin.instance().duplicate_node(node.id, str(destination_node_id))
return new_node.asdict()
@@ -101,7 +104,9 @@ async def update_ethernet_switch(
node_data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]:
await node.set_name(node_data["name"])
node.name = node_data["name"]
if "usage" in node_data:
node.usage = node_data["usage"]
if "ports_mapping" in node_data:
node.ports_mapping = node_data["ports_mapping"]
await node.update_port_settings()
@@ -117,7 +122,7 @@ async def delete_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> No
Delete an Ethernet switch.
"""
await Dynamips.instance().delete_node(node.id)
await Builtin.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@@ -182,7 +187,7 @@ async def create_ethernet_switch_nio(
node: EthernetSwitch = Depends(dep_node)
) -> schemas.UDPNIO:
nio = await Dynamips.instance().create_nio(node, jsonable_encoder(nio_data, exclude_unset=True))
nio = Builtin.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
await node.add_nio(nio, port_number)
return nio.asdict()
@@ -199,8 +204,7 @@ async def delete_ethernet_switch_nio(
The adapter number on the switch is always 0.
"""
nio = await node.remove_nio(port_number)
await nio.delete()
await node.remove_nio(port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@@ -251,5 +255,5 @@ async def stream_pcap_file(
"""
nio = node.get_nio(port_number)
stream = Dynamips.instance().stream_pcap_file(nio, node.project.id)
stream = Builtin.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")