mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
docs: add builtin Ethernet Switch uBridge brctl implementation document
This commit is contained in:
parent
18a123e0b9
commit
7c320d6100
262
docs/features/builtin-ethernet-switch-ubridge.md
Normal file
262
docs/features/builtin-ethernet-switch-ubridge.md
Normal file
@ -0,0 +1,262 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> 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`.
|
||||
|
||||
### `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**:
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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.
|
||||
Loading…
x
Reference in New Issue
Block a user