mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
feat(docker): expose netem-extension packet filters on kernel links
Surface the netem extensions delivered in uBridge feature/tc-netem-ext as GNS3 filter types: rate (tc-style bandwidth string), reorder (needs delay), gemodel (Gilbert-Elliot bursty loss, exclusive with packet_loss), duplicate, seed and limit, plus the delay jitter distribution and the loss/dup correlation parameters. All are kernel-datapath only — the relay's packet-filter registry has no equivalent, so available_filters hides them on relay links, setting one on a created relay link returns 409, and the project-load path drops them with a warning (update_filters runs before the datapath is decided, so it cannot reject there). Validation mirrors the tc grammar: reorder requires delay, gemodel and packet_loss are mutually exclusive, a distribution requires jitter > 0, rate is integer+unit capped at 100gbit. The translation in DockerVM._netem_command_parts emits the frozen grammar order and gates extension keywords on the 'tc capabilities' netem token list (probed once per uBridge process; a plain delay/loss/corrupt/dup filter never probes, keeping old uBridge builds untouched; the correl suffix has no token and gates on 'rate' as the extension-build marker). Reconcile became reset-before-set: the kernel's netem change merges optional attributes (rate, correlation, reorder, corrupt, gemodel, distribution, seed keep their previous value when the new message omits them), so removing a parameter needs an explicit 'tc reset' — the bpf_drop filters are re-added right after in the same flow, so the clsact churn is invisible. The capability check runs before the reset so a failed update leaves the previous qdisc in place. e2e (isolated instance, kernel link): 24/24 — rate 512kbit adds exactly 2x the per-packet serialization to the RTT (45.5 ms for 1400-byte pings), paretonormal jitter + rate-cleared leak check, reorder gap 5, gemodel 50% bursty loss, duplicate, loss 30% correl 50%, seed 42 + limit 5000 visible in the qdisc dump, suspend/resume and node restart keep the filters, relay link rejects rate with 409, teardown leaves no netem. Full unit suite: 1916 passed.
This commit is contained in:
13 files changed
+1012
-108
No files matched your search
@@ -4,14 +4,29 @@ See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> Frozen requirements spec for the **uBridge** project. Delivery status:
|
||||
> parts **A** (netem keyword extensions, uBridge `feature/tc-netem-ext`) and
|
||||
> **C + D** (cBPF match-drop, full-restore idempotent `tc reset`,
|
||||
> `tc capabilities` with `cbpf` probe — uBridge `feature/tc-bpf-drop`) are
|
||||
> **delivered and integrated** on gns3-server branch
|
||||
> `feat/docker-kernel-bpf-drop`. Part **B** (eBPF stateful classifier) is
|
||||
> still pending. The originally shipped netem surface (delay/jitter/loss/
|
||||
> dup/corrupt) exists in uBridge 1.2.3+ and is verified against gns3-server
|
||||
> branch `feat/docker-kernel-filters`.
|
||||
> parts **A** (netem keyword extensions, uBridge `feature/tc-netem-ext`), **C**
|
||||
> (cBPF match-drop) and **D + E** (full-restore idempotent `tc reset`,
|
||||
> `tc capabilities` — uBridge `feature/tc-bpf-drop`) are **delivered and
|
||||
> integrated** on gns3-server branches `feat/docker-kernel-bpf-drop` (C/D/E)
|
||||
> and `feat/docker-kernel-netem-ext` (A: rate/reorder/gemodel/duplicate/
|
||||
> seed/limit/distributions/correlation exposed as GNS3 filter types).
|
||||
> Part **B** (eBPF stateful classifier) is delivered in the uBridge
|
||||
> `feature/tc-precision` branch but **not yet consumed** by gns3-server.
|
||||
> The originally shipped netem surface (delay/jitter/loss/dup/corrupt)
|
||||
> exists in uBridge 1.2.3+ and is verified against gns3-server branch
|
||||
> `feat/docker-kernel-filters`.
|
||||
>
|
||||
> **Deviation found during integration (part A/D):** the delivered
|
||||
> `tc netem set` uses NLM_F_REPLACE, but the *kernel's* netem change merges
|
||||
> optional attributes — rate, correlation, reorder, corrupt, gemodel,
|
||||
> distribution and seed keep their previous value when the new message
|
||||
> omits them, so "netem set atomically replaces" (D's reconcile
|
||||
> assumption) does not hold for parameter *removals*. gns3-server works
|
||||
> around it by sending `tc reset` before every `netem set` (the bpf_drop
|
||||
> filters are re-added right after in the same flow, so clsact churn is
|
||||
> invisible). If uBridge later emits explicit zero/absent clears for every
|
||||
> optional attribute, the server-side reset-before-set can be dropped; the
|
||||
> two behaviours are compatible either way.
|
||||
|
||||
# uBridge kernel impairment: tc netem extensions + eBPF classifiers
|
||||
|
||||
@@ -272,11 +287,17 @@ corresponding filter types per capability.
|
||||
|
||||
## gns3-server alignment (informational — not uBridge scope)
|
||||
|
||||
* P6a types map 1:1 onto new GNS3 filter entries (rate, reorder, gemodel…),
|
||||
translated in `DockerVM._ubridge_apply_netem`.
|
||||
* `frequency_drop` / `bpf` move out of `KERNEL_UNSUPPORTED_FILTERS` once
|
||||
`tc capabilities` reports `ebpf=1` / `cbpf=1` (per-compute probe, cached);
|
||||
without the capability the link stays relay-wired exactly as today.
|
||||
* **Done** (`feat/docker-kernel-netem-ext`): the P6a types map 1:1 onto new
|
||||
GNS3 filter entries (rate, reorder, gemodel, duplicate, seed, limit, plus
|
||||
the delay `distribution` and loss/dup `correlation` parameters), translated
|
||||
in `DockerVM._ubridge_apply_netem`; extension keywords are gated on the
|
||||
`tc capabilities` netem token list (old uBridge builds are never probed
|
||||
for the original surface).
|
||||
* **Done** (`feat/docker-kernel-bpf-drop`): `bpf` runs as cBPF match-drop
|
||||
(`KERNEL_UNSUPPORTED_FILTERS` shrank to `frequency_drop`).
|
||||
* `frequency_drop` moves out of `KERNEL_UNSUPPORTED_FILTERS` once the eBPF
|
||||
classifier (part B, uBridge `feature/tc-precision`) is consumed on the
|
||||
server side, keyed on `ebpf=1` per compute.
|
||||
* Every veth end owns one qdisc + its filters: per-direction impairment is
|
||||
an architectural freebie to expose later (API `direction` field), aligned
|
||||
with marker `dir` semantics.
|
||||
@@ -24,13 +24,13 @@ Frames never leave the kernel.
|
||||
| Link | uBridge bridge, TAP fd ↔ UDP, userspace copy | per-link kernel bridge `gns3{link_id[:11]}`, zero-copy |
|
||||
| Relay latency | ~0.3 ms RTT | ~0.05 ms RTT (measured) |
|
||||
| Link attach at runtime | yes (relay attaches the pre-existing TAP) | yes (brctl addif / add_nio_ethernet — interface untouched) |
|
||||
| Packet filters | uBridge userspace filters | tc netem on the veth host end (delay / packet_loss / corrupt); bpf as cls_bpf match-drop classifiers; frequency_drop stays relay-only (eBPF classifier pending) |
|
||||
| Packet filters | uBridge userspace filters | tc netem on the veth host end (delay / packet_loss / corrupt / duplicate + the netem extensions rate, reorder, gemodel, seed, limit, jitter distributions, loss correlation); bpf as cls_bpf match-drop classifiers; frequency_drop stays relay-only (eBPF classifier pending) |
|
||||
| Capture / markers | relay bridge | uBridge AF_PACKET modules on the veth host end |
|
||||
|
||||
The work landed in six stages on branch stack `feat/docker-kernel-datapath` →
|
||||
The work landed in seven stages on branch stack `feat/docker-kernel-datapath` →
|
||||
`feat/docker-kernel-capture` → `feat/docker-kernel-markers` →
|
||||
`feat/docker-veth-everywhere` → `feat/docker-kernel-filters` →
|
||||
`feat/docker-kernel-bpf-drop`:
|
||||
`feat/docker-kernel-bpf-drop` → `feat/docker-kernel-netem-ext`:
|
||||
|
||||
1. **Kernel datapath** — NIOBridge NIO type, per-link bridges, carrier-based
|
||||
suspend, crash-safe reconciliation.
|
||||
@@ -49,6 +49,14 @@ The work landed in six stages on branch stack `feat/docker-kernel-datapath` →
|
||||
uBridge pcap-compiles the line to classic BPF, the kernel migrates it to
|
||||
eBPF internally — no CAP_BPF needed). Requires a uBridge reporting
|
||||
`cbpf=1` via `tc capabilities`; older builds get a clear upgrade error.
|
||||
7. **netem extensions** — the netem-extension surface delivered in uBridge
|
||||
`feature/tc-netem-ext` is exposed as GNS3 filter types: `rate`,
|
||||
`reorder`, `gemodel`, `duplicate`, `seed`, `limit`, plus the delay
|
||||
`distribution` and loss/dup `correlation` parameters. Kernel-datapath
|
||||
links only (the relay has no equivalent); capability-gated per uBridge
|
||||
process. Reconcile became reset-before-set: the kernel's netem replace
|
||||
merges optional attributes, so removing a parameter needs the explicit
|
||||
reset.
|
||||
|
||||
## Adapter interface types
|
||||
|
||||
@@ -171,9 +179,15 @@ ends (uBridge `tc netem set`, raw netlink — no `tc` binary needed):
|
||||
|
||||
| GNS3 filter | kernel implementation | Notes |
|
||||
|---|---|---|
|
||||
| `delay [ms, jitter]` | netem `delay X jitter Y` | jitter 0 omitted |
|
||||
| `packet_loss [%]` | netem `loss P` | per direction (see below) |
|
||||
| `delay [ms, jitter, dist]` | netem `delay X jitter Y [distribution D]` | jitter 0 omitted; dist ∈ uniform/normal/pareto/paretonormal (kernel-only parameter) |
|
||||
| `packet_loss [%, correl]` | netem `loss P [correl C]` | per direction (see below); correlation is a kernel-only parameter |
|
||||
| `corrupt [%]` | netem `corrupt P` | |
|
||||
| `duplicate [%, correl]` | netem `dup P [correl C]` | kernel-only type |
|
||||
| `rate ["512kbit"]` | netem `rate B` | tc-style integer+unit (bit/kbit/mbit/gbit/bps/kbps/mbps, ≤100gbit); kernel-only type |
|
||||
| `reorder [%, correl, gap]` | netem `reorder P [correl C] [gap G]` | requires `delay`; kernel-only type |
|
||||
| `gemodel [p, r, 1-h]` | netem `loss gemodel P R H` | Gilbert-Elliot bursty loss; mutually exclusive with `packet_loss`; kernel-only type |
|
||||
| `seed [u32]` | netem `seed N` | reproducible random draws; kernel-only type |
|
||||
| `limit [pkts]` | netem `limit N` | queue depth above the 1000 default (rate+delay BDP); kernel-only type |
|
||||
| `bpf` (one line per expression, OR) | `tc bpf_drop add <if> <prio> "<expr>"` — cls_bpf on clsact egress + gact drop | needs uBridge `cbpf=1` (`tc capabilities`, probed once per uBridge process); a line that fails to compile on the compute is skipped with a warning, mirroring the relay |
|
||||
| `frequency_drop` | — not yet (eBPF stateful classifier spec'd: [ubridge-kernel-impairment-spec](../design/ubridge-kernel-impairment-spec.md), part B pending) | 409 on kernel links; relay fallback |
|
||||
|
||||
@@ -184,32 +198,45 @@ Semantics:
|
||||
so every direction of the link is impaired exactly once — the same net
|
||||
effect as the relay, where both directions cross the single filtered
|
||||
bridge. A `delay 100` link measures ≈200 ms RTT; `packet_loss 30`
|
||||
measures ≈51 % round-trip (1 − 0.7²).
|
||||
* **Reconcile = full re-apply.** `netem set` is an atomic replace (NLM_F_REPLACE),
|
||||
so every NIO update just rebuilds the qdisc from the current filters; no
|
||||
per-parameter diffing. An empty filter set detaches it (`tc reset`,
|
||||
ENOENT-tolerated).
|
||||
measures ≈51 % round-trip (1 − 0.7²); `rate 512kbit` adds ≈2× the
|
||||
per-packet serialization time to the RTT.
|
||||
* **Reconcile = reset + full re-apply.** The kernel's netem replace MERGES
|
||||
optional attributes (rate, correlation, reorder, corrupt, gemodel,
|
||||
distribution: an absent attr keeps its previous value), so re-applying a
|
||||
filter set with a parameter *removed* would silently keep the old value.
|
||||
Every apply therefore starts with `tc reset` (also drops clsact and its
|
||||
bpf_drop filters — re-added right after in the same flow) followed by one
|
||||
`netem set` built from the current filters; no per-parameter diffing. An
|
||||
empty filter set is the reset alone (ENOENT-tolerated).
|
||||
* **Extension gating.** The netem-extension keywords (rate, reorder,
|
||||
gemodel, dist, seed, limit, and the correl suffixes) probe
|
||||
`tc capabilities` once per uBridge process and require the tokens;
|
||||
a plain delay/loss/corrupt/dup filter never probes — an old uBridge
|
||||
serves the original surface untouched.
|
||||
* **kernel-only vs relay-only.** The extension types have no uBridge relay
|
||||
equivalent: `available_filters` hides them on relay links, setting one on
|
||||
a created relay link returns 409, and a project loaded with such filters
|
||||
on a link that cannot be kernel-wired drops them with a warning (invalid
|
||||
filters are dropped the same way at load). Symmetrically,
|
||||
`frequency_drop` stays relay-only.
|
||||
* **Validation** mirrors the tc grammar: `reorder` requires `delay`,
|
||||
`gemodel` and `packet_loss` are mutually exclusive (both map to the netem
|
||||
loss keyword), `distribution` requires jitter > 0, rate must be integer +
|
||||
unit ≤ 100gbit.
|
||||
* **Node restart** restores the qdisc from the NIO (like capture and markers);
|
||||
**link deletion** detaches it explicitly — the veth survives the link and an
|
||||
orphaned qdisc would keep impairing the next one.
|
||||
* **Suspend** keeps the qdisc (carrier-driven loss); the first packet after
|
||||
resume takes one extra delay interval (known netem idle-baseline behaviour)
|
||||
and steady state is exact.
|
||||
* A link carrying `frequency_drop` is wired on the **relay** (its eBPF
|
||||
stateful classifier is not delivered yet); `available_filters` hides it on
|
||||
kernel links and setting it returns 409 with a clear message. The remaining
|
||||
kernel-side roadmap is frozen in
|
||||
[docs/design/ubridge-kernel-impairment-spec.md](../design/ubridge-kernel-impairment-spec.md)
|
||||
(eBPF stateful classifier for `frequency_drop` + quota/window/flow modes,
|
||||
plus netem keyword extensions: rate, reorder, gemodel loss, jitter
|
||||
distributions — parts C and D of that spec are delivered).
|
||||
* **bpf reconcile = flush + re-add** (`tc bpf_drop flush`, then one add per
|
||||
line at priorities 10, 11, …). `tc reset` (link delete / filter clear) is
|
||||
the full restore in uBridge: filters → clsact → root qdisc, idempotent.
|
||||
* **Observability caveat:** clsact drops happen before the AF_PACKET tap
|
||||
points — markers/captures on the same veth do not observe cls_bpf-dropped
|
||||
frames (on the relay, filter-vs-mark ordering determined visibility
|
||||
instead).
|
||||
instead). Also note `tc qdisc show` cannot print a distribution's name
|
||||
(the kernel stores only the sampled table).
|
||||
|
||||
## uBridge command surface
|
||||
|
||||
@@ -222,7 +249,9 @@ brctl create / delete / addif / delif / show
|
||||
link set <if> up|down
|
||||
capture start_kernel / stop_kernel
|
||||
marker add_kernel / delete_kernel / enable_kernel
|
||||
tc netem set <if> [delay <ms>] [jitter <ms>] [loss <%>] [dup <%>] [corrupt <%>]
|
||||
tc netem set <if> [delay <ms>] [jitter <ms>] [loss <%> [correl <%>] | loss gemodel <p> <r> <1-h>]
|
||||
[dup <%> [correl <%>]] [corrupt <%>] [reorder <%> [correl <%>] [gap <n>]]
|
||||
[rate <bw>] [limit <pkts>] [distribution uniform|normal|pareto|paretonormal] [seed <u32>]
|
||||
tc bpf_drop add <if> <prio 10-99> "<expr>" / tc bpf_drop flush <if>
|
||||
tc reset <if> # full restore: filters -> clsact -> root qdisc, idempotent
|
||||
tc capabilities # "netem=<kw,...>;ebpf=0|1;cbpf=0|1", probed once per process
|
||||
@@ -266,3 +295,23 @@ recreate leaves no residual filters; `frequency_drop` still 409;
|
||||
`available_filters` shows `bpf` and hides `frequency_drop`. Unit tests:
|
||||
`tests/compute/docker/test_docker_kernel_datapath.py`,
|
||||
`tests/controller/test_kernel_datapath_link.py`.
|
||||
|
||||
netem extensions (same setup, 24/24 checks): `rate 512kbit` on both veths,
|
||||
1400-byte pings measure 45.5 ms RTT = baseline + 2× the 22.2 ms
|
||||
serialization (exact); `delay 100 20 paretonormal` applies delay+jitter and
|
||||
clears the previous rate (leak detector for the kernel's merge-on-replace);
|
||||
`reorder 25 gap 5` visible in the qdisc dump and traffic passes;
|
||||
`gemodel 100/0/30` = 50 % bursty round-trip loss with the previous reorder
|
||||
cleared; `duplicate 50` link alive; `packet_loss 30 correl 50` shows
|
||||
`loss 30% 50%`; `seed 42` + `limit 5000` both visible in the dump, RTT
|
||||
exactly 2×50 ms; suspend/resume and node restart keep the filters; a
|
||||
docker↔ethernet-switch relay link rejects `rate` with 409 and its
|
||||
`available_filters` hides the kernel-only types while the kernel link shows
|
||||
them all (minus frequency_drop); `reorder` without `delay` and
|
||||
`gemodel`+`packet_loss` are 409s; teardown leaves no netem on either veth.
|
||||
Also verified live: a relay attach (`bridge add_nio_ethernet`) on the
|
||||
admin-down veth host end fails in libpcap — the relay path now brings the
|
||||
interface up before attaching. Unit tests:
|
||||
`tests/utils/test_packet_filter_validation.py` (P6a class),
|
||||
`tests/compute/docker/test_docker_kernel_datapath.py`,
|
||||
`tests/controller/test_kernel_datapath_link.py`.
|
||||
Reference in new issue
Block a user