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:
YueGuobin committed 2026-10-06 00:21:08 +08:00
1 parent b1eb0984bc
commit d4b57d49fb
13 files changed
+1012 -108

No files matched your search

+84 -4
View File
@@ -26,6 +26,7 @@ from gns3server.config import Config
from gns3server.utils.packet_filter_validation import (
FilterValidationError,
filter_inactive_filters,
kernel_only_features,
validate_all_filters,
)
@@ -51,16 +52,22 @@ FILTERS = [
{
"type": "packet_loss",
"name": "Packet loss",
"description": "The percentage represents the chance for a packet to be lost",
"parameters": [{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"}],
"description": "The percentage represents the chance for a packet to be lost. The optional correlation "
"(kernel-datapath links only) makes consecutive losses dependent, like a bursty real network",
"parameters": [
{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Correlation", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "delay",
"name": "Delay",
"description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay",
"description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay, and "
"(kernel-datapath links only) shape the jitter with a distribution",
"parameters": [
{"name": "Latency", "minimum": 1, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Jitter (-/+)", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Distribution", "type": "str", "unit": "uniform|normal|pareto|paretonormal"},
],
},
{
@@ -75,6 +82,60 @@ FILTERS = [
"description": "This filter will drop any packet matching a BPF expression. Put one expression per line",
"parameters": [{"name": "Filters", "type": "text"}],
},
# Everything below runs on kernel-datapath links only (tc netem on the
# veth host end; the uBridge relay has no equivalent).
{
"type": "rate",
"name": "Bandwidth limit",
"description": "Shape the link to a maximum bandwidth, tc-style value (kernel-datapath links only)",
"parameters": [{"name": "Rate", "type": "str", "unit": "e.g. 512kbit, 10mbit"}],
},
{
"type": "reorder",
"name": "Reorder",
"description": "The percentage represents the chance for a packet to be reordered (held until a later "
"packet passes it). Requires the delay filter; kernel-datapath links only",
"parameters": [
{"name": "Reorder", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Correlation", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Gap", "minimum": 1, "maximum": 1000, "type": "int", "unit": "packets"},
],
},
{
"type": "gemodel",
"name": "Gilbert-Elliot loss",
"description": "Bursty loss model: p is the loss chance in the bad state, r in the good state, 1-h the "
"chance of moving from good to bad. Mutually exclusive with packet loss; kernel-datapath links only",
"parameters": [
{"name": "p (bad-state loss)", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "r (good-state loss)", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "1-h (good-to-bad)", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "duplicate",
"name": "Duplicate",
"description": "The percentage represents the chance for a packet to be duplicated; the optional "
"correlation makes consecutive duplicates dependent (kernel-datapath links only)",
"parameters": [
{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
{"name": "Correlation", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"},
],
},
{
"type": "seed",
"name": "Random seed",
"description": "Make the netem random draws (loss, duplication, jitter) reproducible for repeated "
"experiments (kernel-datapath links only)",
"parameters": [{"name": "Seed", "minimum": 0, "maximum": 4294967295, "type": "int", "unit": ""}],
},
{
"type": "limit",
"name": "Queue limit",
"description": "Queue depth of the impairment qdisc in packets — raise it above the 1000 default when "
"combining a low rate with a long delay (kernel-datapath links only)",
"parameters": [{"name": "Limit", "minimum": 1, "maximum": 1000000, "type": "int", "unit": "packets"}],
},
]
@@ -247,6 +308,21 @@ class Link:
", ".join(sorted(unsupported))
)
)
elif self._created:
# The reverse guard: netem-extension filters have no relay
# equivalent. Only enforced on created links — while loading a
# project the datapath is not decided yet (a link that will be
# wired on the kernel datapath must accept them), and the relay
# prepare path drops what it cannot run with a warning.
conflicts = kernel_only_features(new_filters)
if conflicts:
raise ControllerError(
"Packet filter(s) {} only run on a kernel-datapath link (tc netem on the "
"veth host end); this link is wired on the uBridge relay — delete and "
"recreate the link to switch it to the kernel datapath".format(
", ".join(sorted(conflicts))
)
)
if new_filters != self.filters:
self._filters = new_filters
@@ -652,7 +728,11 @@ class Link:
from gns3server.utils.packet_filter_validation import KERNEL_UNSUPPORTED_FILTERS
return [f for f in FILTERS if f["type"] not in KERNEL_UNSUPPORTED_FILTERS]
return FILTERS
# Relay links: hide the netem-extension types the relay cannot run
# (they would be rejected with 409 on update).
from gns3server.utils.packet_filter_validation import KERNEL_ONLY_FILTERS
return [f for f in FILTERS if f["type"] not in KERNEL_ONLY_FILTERS]
return []
def _get_filter_node(self):