mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 21:45:41 +03:00
docs(spec): record the delivered L2-only anchors and their residual
uBridge delivered `link l2only` (e2e3155) and the brctl delete refusal (83a84fb); verified from the server side -- anchors and per-link bridges report addrgenmode none, carry no addresses, and an idle anchor is silent once the one-shot membership burst has settled. That burst is the one thing the command cannot remove, so it is now recorded where the acceptance criteria live, with the measurement: enslaving a port makes the kernel announce the bridge's multicast memberships (90 B MLDv2 at +0.00 s, 54 B IGMPv3 at +0.09 s, 90 B MLDv2 at +0.73 s, then silence; no addresses involved, and IPv4 has no per-device IGMP switch). Decision: accepted and documented, not suppressed -- §E.2 measures idle silence after a settle window instead. Pins the other delivered behaviour with a test: a bridge now survives a single-sided node stop, so brctl create answers EEXIST on the common path rather than only in a create race -- the attach must not short-circuit there, because the restarted side has a brand-new anchor whose filters and markers still have to be re-applied.
This commit is contained in:
parent
a1fcd29bce
commit
81ad9b837f
@ -3,11 +3,17 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> Frozen requirements spec for the **uBridge** project. Status: **not yet
|
||||
> delivered.** Requested by gns3-server branch `feat/qemu-kernel-datapath`
|
||||
> (QEMU persistent-TAP anchors) and, retroactively, by the shipped Docker
|
||||
> kernel-datapath stack: today every host-side anchor device carries an IPv6
|
||||
> identity, so emulated links carry the *host's* IPv6 chatter.
|
||||
> Frozen requirements spec for the **uBridge** project. Status: **delivered**
|
||||
> (uBridge `e2e3155` — `link l2only` plus the creators that apply it) and
|
||||
> verified from the server side: anchors and per-link bridges report
|
||||
> `addrgenmode none`, `ip -4/-6 addr` are empty, and an idle anchor is silent
|
||||
> once the one-shot membership burst of §0 has settled.
|
||||
>
|
||||
> **Residual, accepted (measured after delivery):** enslaving a port still
|
||||
> makes the *bridge device* announce its multicast memberships — one IGMPv3
|
||||
> plus one or two MLDv2 reports in the first second — which floods into the
|
||||
> emulated segment. It is outside this command's reach by design (no addresses
|
||||
> are involved, and IPv4 has no per-device IGMP off switch); see §0.
|
||||
>
|
||||
> Scope guard: no wire format, no command grammar change to existing
|
||||
> commands — this adds one command and one internal step in four creators.
|
||||
@ -53,6 +59,32 @@ invariant we keep is *no addresses at all* (measured: `ip -4 addr` is empty on
|
||||
every anchor and per-link bridge on the deployment above, with NetworkManager
|
||||
reporting them `unmanaged`). IPv6 is the only protocol that lights itself up.
|
||||
|
||||
### The residual this command does not remove (measured after delivery)
|
||||
|
||||
With `l2only` in place (anchors and bridges report `addrgenmode none`, no
|
||||
addresses), **enslaving a port still makes the bridge device speak once** —
|
||||
the kernel announces the multicast memberships the enslavement brings to the
|
||||
bridge:
|
||||
|
||||
```
|
||||
+0.00 s 90 B IPv6 MLDv2 report dst 33:33:00:00:00:16 (ICMPv6 143)
|
||||
+0.09 s 54 B IPv4 IGMPv3 report dst 01:00:5e:00:00:16 0.0.0.0 → 224.0.0.22
|
||||
+0.73 s 90 B IPv6 MLDv2 report dst 33:33:00:00:00:16
|
||||
```
|
||||
|
||||
(Scratch tap pair + per-link bridge on the test host, unprivileged; source MAC
|
||||
= the bridge's. A 10 s window contains exactly those three frames, then silence.
|
||||
The same burst appears through the server: one 90 B IPv6 frame inside a 2 s
|
||||
window right after a link is created.)
|
||||
|
||||
It is outside `l2only`'s reach by design: no addresses are involved (the report
|
||||
sources are `::` and `0.0.0.0`), and IPv4 has no per-device "no IGMP" knob to
|
||||
set. **Decision: accepted and documented, not suppressed** — it is standard L2
|
||||
control traffic, one-shot per link creation or port change, and it stops. The
|
||||
acceptance test in §E.2 therefore measures after a settle window. Suppressing
|
||||
it (e.g. `disable_ipv6` on the bridge before enslaving, for the MLD half) would
|
||||
be a new, separate requirement and has not been asked for.
|
||||
|
||||
### Goal
|
||||
|
||||
Every host-side device uBridge creates for the data plane is **pure L2**: no
|
||||
@ -147,11 +179,12 @@ data plane hardens it, and the acceptance checks in §E apply to it.
|
||||
1. **No addresses.** After each creator: `ip -6 addr show dev <dev>` is empty
|
||||
(in particular no `fe80::`) and `ip -4 addr show dev <dev>` is empty; the
|
||||
same for the bridge created by `brctl create`.
|
||||
2. **Idle silence.** With the device UP and in its normal role (TAP with an open
|
||||
fd / veth with its peer up / bridge with two attached ports), nothing appears
|
||||
on it for 5 s — measured from the peer end, or via
|
||||
`capture start_kernel <dev> <pcap>` while idle and by asserting the pcap has
|
||||
zero packets. Baseline today: 6 frames / 2 s.
|
||||
2. **Idle silence, after the settle window.** Let the last port enslavement
|
||||
settle (≥ 2 s — that burst is expected, see §0), then with the device UP and
|
||||
in its normal role (TAP with an open fd / veth with its peer up / bridge with
|
||||
two attached ports) nothing appears on it for 5 s — measured from the peer
|
||||
end, or via `capture start_kernel <dev> <pcap>` while idle with the pcap
|
||||
asserted empty. Baseline before this work: 6 frames / 2 s, continuous.
|
||||
3. **Idempotency & revert.** `on` twice → `100` both times, state unchanged;
|
||||
`off` restores the kernel default (link-local returns after the device is
|
||||
cycled down/up).
|
||||
|
||||
@ -148,8 +148,16 @@ enable_kernel_datapath = True
|
||||
Anchors are host-side netdevs, so the kernel gives them an IPv6 link-local
|
||||
address and emits MLD/DAD from them; those frames flood into the emulated
|
||||
segment. The hardening requirement (anchors must be pure L2, no L3 identity)
|
||||
is specified for uBridge in `docs/design/ubridge-l2-anchor-spec.md` — it
|
||||
applies to QEMU TAPs and Docker veths alike.
|
||||
is specified for uBridge in `docs/design/ubridge-l2-anchor-spec.md` and is
|
||||
**delivered**: anchors and per-link bridges come up with `addrgenmode none`,
|
||||
no addresses, and an idle anchor is silent (asserted in the e2e after a 2.5 s
|
||||
settle).
|
||||
|
||||
One residual is accepted and documented there: enslaving a port makes the
|
||||
*kernel* announce the bridge's multicast memberships once — an IGMPv3 plus one
|
||||
or two MLDv2 reports in the first second — which floods into the segment like
|
||||
any L2 control frame. It stops; it is outside the L2-only command's reach (no
|
||||
addresses involved, IPv4 has no per-device IGMP switch).
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@ -291,6 +291,37 @@ async def test_connect_nio_kernel_tolerates_bridge_create_race(vm):
|
||||
vm._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{host_ifc}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_eexist_still_reapplies_everything(vm):
|
||||
"""
|
||||
EEXIST is a *common* path now: a bridge survives a single-sided node
|
||||
restart (the peer's still-enslaved port makes brctl delete answer EBUSY,
|
||||
which is suppressed), so the restarted side finds its bridge already
|
||||
there. The create-or-verify step must not short-circuit the rest of the
|
||||
attach — the restarted side has a brand-new anchor and needs its capture,
|
||||
markers and filters re-applied on it.
|
||||
"""
|
||||
|
||||
async def send(command):
|
||||
if "brctl create" in command:
|
||||
raise UbridgeError("Could not create bridge gns3a1b2c3d4e5f: File exists (EEXIST)")
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock(side_effect=send)
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
nio.filters = {"delay": [10]}
|
||||
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
vm._ubridge_send.assert_any_call(f'brctl show "{BRIDGE}"')
|
||||
vm._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{host_ifc}"')
|
||||
# ... and the port-level configuration of the fresh anchor goes on top of
|
||||
# it, unconditionally (never inside a "the bridge was created" branch).
|
||||
vm._ubridge_send.assert_any_call(f'tc reset "{host_ifc}"')
|
||||
vm._ubridge_send.assert_any_call(f'tc netem set "{host_ifc}" delay 10')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_create_failure_propagates(vm):
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user