gns3-server/docs/features/builtin-ethernet-switch-ubridge.md
YueGuobin 72dc10a669
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.
2026-08-21 21:41:51 +08:00

11 KiB

This documentation is AI-generated with reference to actual code and verified against real-kernel testing (Linux 7.1.2-1-default). AI can make mistakes — please verify against the source code when in doubt.

Builtin Ethernet Switch — uBridge brctl Backend

Overview

The historical GNS3 Ethernet Switch was an emulated L2 device inside Dynamips (ethsw). This implementation replaces it with a real Linux kernel bridge driven through uBridge's brctl module — one bridge per switch node. The migration makes the switch a first-class builtin node (no Dynamips dependency) and enables native-kernel-speed L2 switching with VLAN filtering and QinQ.

Old (Dynamips ethsw) New (uBridge brctl)
Switching engine Dynamips user-space emulation Linux kernel bridge (netlink)
VLAN model ethsw ACL per port Kernel VLAN filtering + PVID/untagged
QinQ 0x8100/0x88A8/0x9100/0x9200 0x8100 (802.1Q) / 0x88A8 (802.1ad)
Data path Node NIO ↔ ethsw NIO (Dynamips) Node NIO ↔ uBridge relay ↔ TAP ↔ kernel bridge
Console Inactive (reserved TCP port) None (console_type=none)
Node type dynamips-routed builtin (always-on)

Architecture

  ┌───────────┐     ┌──────────────┐     ┌───────────────┐     ┌───────────┐
  │  Peer A   │     │   uBridge    │     │  Kernel       │     │  Peer B   │
  │ (Dynamips │◄───►│  per-port    │◄───►│  Bridge       │◄───►│ (IOU /    │
  │  / IOU /  │ UDP │  relay       │ TAP │  gns3{id[:6]}│ TAP │ QEMU / …) │
  │  QEMU)   │     │  nio_tap↔udp │     │  vlan_filter  │     │           │
  └───────────┘     └──────────────┘     └──────┬────────┘     └───────────┘
                                                │
                                          ┌─────┴─────┐
                                          │ ... more   │
                                          │ ports      │
                                          └───────────┘

Each switch port is a dual-role TAP — uBridge holds the file descriptor as a nio_tap relay endpoint, and the same TAP is enslaved to the kernel bridge via brctl addif. This is the same pattern the Cloud node already uses for host bridges (cloud.py:_add_linux_ethernet). uBridge is only the per-port UDP transport; the kernel bridge performs the actual MAC learning, forwarding, and VLAN filtering.

Component map

File Role
compute/builtin/nodes/ethernet_switch.py Node implementation
api/routes/compute/ethernet_switch_nodes.py REST endpoints (repointed to Builtin)
schemas/compute/ethernet_switch_nodes.py Request/response models (unchanged)
controller/udp_link.py Link creation — pushes NIO to switch via standard adapter endpoint

Lifecycle

create()start()

  1. _start_ubridge(require_privileged_access=True) — launch uBridge instance
  2. _ensure_bridge():
    • Derive deterministic bridge name: gns3 + first 6 hex chars of self.id
    • brctl delete (best-effort — crash recovery, cleans stale interfaces)
    • brctl create
    • link set … up (bridge is DOWN after create)
    • brctl vlanfiltering … on

add_nio(nio, port_number)

Per port, one uBridge relay bridge {node_id}-{port} is wired:

bridge create {node_id}-{port}
bridge add_nio_tap {node_id}-{port} "{tap}"          ← uBridge holds TAP fd
brctl addif "{bridge}" "{tap}"                       ← enslave to kernel bridge
brctl vlan_del/vlan_add …                            ← apply port VLAN mode
bridge add_nio_udp {node_id}-{port} lport rhost rport
bridge reset_packet_filters {node_id}-{port}          ← from _ubridge_apply_filters
bridge start {node_id}-{port}

Captures and marker signals are applied via the existing _ubridge_apply_filters 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)

brctl delif "{bridge}" "{tap}"
bridge delete {node_id}-{port}
release_udp_port(nio.lport)

close()

for each port: release UDP port
brctl delete "{self._bridge_name}"     ← kernel bridge teardown
_stop_ubridge()                        ← destroys remaining TAPs

Cleanup paths:

Scenario Bridge cleanup TAP cleanup
Normal project close close()brctl delete uBridge stops → TAP fd closed → kernel destroys
gns3server crash / kill Next _ensure_bridge()brctl delete before create uBridge dies → TAP fd closed by kernel
Manual project-file deletion after crash Leaked (no GNS3 record of gns3{id[:6]}) Leaked (same — but uBridge probably dead, TAPs gone with it)

Port mode → VLAN translation

All VLAN operations ride on the brctl hypervisor module (../ubridge/doc/brctl.md). The kernel bridge must have vlan_filtering on before any vlan_* call.

access VLAN N

brctl vlan_del {br} {tap} 1          ← remove default PVID 1
brctl vlan_add {br} {tap} N pvid untagged

dot1q trunk (native VLAN V)

A dot1q trunk in ESW is "admit all VLANs tagged, native VLAN PVID + untagged":

brctl vlan_del {br} {tap} 1
brctl vlan_add {br} {tap} 1 vid 4094     ← admit all VIDs tagged
brctl vlan_add {br} {tap} V pvid untagged ← override native

qinq (outer VLAN O, ethertype 0x88A8)

Bridge-level (once):

brctl setvlanproto {br} 0x88a8          ← switch to 802.1ad (outer S-tag)

Port-level:

brctl vlan_del {br} {tap} 1
brctl vlan_add {br} {tap} O pvid untagged  ← S-tag push for untagged ingress

Ethertype 0x8100 qinq ports are treated as plain access ports (the bridge defaults to 0x8100; no setvlanproto needed). Ethertype 0x9100/0x9200 are not supported by the kernel bridge — see § Limitations.

Runtime reconfiguration (update_port_settings)

On ports_mapping update, existing port VLANs are reset before re-apply:

brctl delif {br} {tap}                  ← release from bridge (clears VLAN state)
brctl addif {br} {tap}                  ← re-enslave (resets to default PVID 1)
brctl vlan_del/vlan_add …               ← apply new mode

This prevents stale VLAN membership from a previous mode leaking into the new configuration (e.g., access→trunk transition leaving old access VLAN behind).

Bridge naming

Deterministic from the switch's UUID: gns3 + first 6 hex chars (no dashes).

gns3a1b2c3          ← bridge (10 chars, ≤ 15 IFNAMSIZ limit)
gns3a1b2c3-0        ← tap for port 0 (12 chars)
gns3a1b2c3-1        ← tap for port 1 (12 chars)
  • 6 hex = 48 bits of entropy — collision risk is astronomically low even with thousands of switches on the same host.
  • Crash recovery: brctl delete (best-effort, ignore if not found) then brctl create — stale interfaces from a previous abnormal shutdown are reclaimed automatically when the switch is re-created.

Controller integration

No controller or API contract changes are required. The migration is entirely compute-internal:

  • node_types.BUILTIN_NODE_TYPES already classified ethernet_switch as a builtin, always-running node.
  • udp_link.create() pushes the NIO to the switch via the standard POST /adapters/0/ports/{p}/nio endpoint (same as Dynamips).
  • The REST API paths, request/response schemas, and port model (EthernetSwitchPort: type/vlan/ethertype) are unchanged.
  • /start, /stop, /suspend, /reload return 405 (switch is always-on).

The sole observable difference: the console field in the response is now null (the switch has no console; console_type="none" makes BaseNode skip TCP port reservation). The old Dynamips ethsw returned an unused TCP port number. Both are valid under Optional[int].

Known limitations

Default PVID 1 must be deleted explicitly

A port freshly enslaved to a vlan_filtering bridge inherits default PVID 1 (PVID + Egress Untagged). Access/trunk mode application must issue vlan_del … 1 first — vlan_add … pvid moves the PVID but does not remove the old PVID's membership. This matches iproute2 semantics and is documented in ../ubridge/doc/brctl.md#limitations.

QinQ is outer-tag (S-VLAN) only

With setvlanproto 0x88a8 the bridge filters on the outer S-tag; the inner C-tag passes through transparently. Selective QinQ (inner-VLAN classification or remapping) requires IFLA_BRIDGE_VLAN_TUNNEL_INFO which is not implemented. Documented in ../ubridge/doc/brctl.md#limitations.

Ethertype 0x9100 / 0x9200

The GNS3 schema allows legacy QinQ ethertypes 0x9100 and 0x9200, but the Linux kernel bridge only supports 0x8100 (802.1Q) and 0x88a8 (802.1ad). Configuring these on a qinq port produces a NodeError at creation/update time. Handling policy (map to 0x88A8 + warn vs. reject with error) is pending per design discussion.

No FDB read/write

The brctl module exposes no fdb_show/fdb_flush. The kernel bridge learns and ages MAC entries autonomously; uBridge has never exposed MAC-table access and gns3-server does not consume it. Consumers that need the FDB (e.g., a WebUI switch view) should read /sys/class/net/<br>/brforward or bridge fdb show dev <br> directly, without uBridge involvement.

Troubleshooting

Docker iptables: FORWARD chain DROP

Docker sets the iptables FORWARD chain default policy to DROP when the Docker daemon starts. This blocks all forwarded traffic through kernel bridges on the host, including gns3* bridges.

Symptoms: nodes can send frames into the bridge (visible in tcpdump -i gns3*) but never receive unicast replies. ARP and multicast may work because they flood, but unicast forwarding silently fails.

Fix:

sudo iptables -P FORWARD ACCEPT

Bridge left DOWN after creation

brctl create creates the bridge but leaves it administratively DOWN. The node now sends link set … up after brctl create. If forwarding is not working, verify:

ip -d link show gns3* | grep -E "state|vlan_filtering"

Kernel version differences

This implementation has been tested on Linux 7.1.2-1-default (x86_64) with uBridge installed via make install (cap_net_admin,cap_net_raw=ep). The ubridge brctl module has a 168-test suite covering kernel-side VLAN behaviour on this kernel.