mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
feat(docker): packet filters on kernel-datapath links via tc netem
Impairment filters with a netem equivalent (delay, packet_loss, corrupt) now run on the kernel datapath as one tc netem qdisc per veth host end, pushed through uBridge's netlink tc module (tc netem set / tc reset). This removes the relay delay filter's nanosleep bottleneck (upstream Both link endpoints carry the filters: each end's qdisc egress covers the traffic entering that container, so every direction is impaired exactly once -- the same net effect as the relay, where both directions cross the single filtered bridge. - controller: eligibility keys on relay-only types (frequency_drop, bpf) instead of any filter; kernel links push the real filters (the synthetic suspend frequency_drop no longer flips a suspended link to relay on reopen/reset); update_filters 409s only relay-only types; available_filters hides them on kernel links - compute: create_nio whitelists netem-capable filter types; the NIO update path keeps its own guard (PUT mutates the NIO in place); _ubridge_apply_netem translates the filter dict into one atomic netem-set (full re-apply IS the reconcile); node restart restores the qdisc, link deletion detaches it (the veth survives the link) - e2e (27/27): delay 100 -> 200.2 ms RTT, atomic replace 50 -> 100.2 ms, clear -> baseline, packet_loss 30 -> 48 % round-trip (expected 51 %), suspend/resume and node restart keep the qdisc, link delete leaves no residual impairment
This commit is contained in:
11 files changed
+459
-95
No files matched your search
@@ -88,12 +88,46 @@ async def test_kernel_datapath_not_eligible_across_computes(project):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_filters(project):
|
||||
async def test_kernel_datapath_eligible_with_netem_filters(project):
|
||||
"""
|
||||
Impairment filters with a tc netem equivalent (delay, packet_loss,
|
||||
corrupt) no longer disqualify the kernel datapath — they become one
|
||||
netem qdisc per veth host end.
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._filters = {"delay": [10, 0]}
|
||||
link._filters = {"delay": [10, 0], "packet_loss": [5], "corrupt": [1]}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_relay_only_filters(project):
|
||||
"""
|
||||
frequency_drop and bpf only exist in the uBridge userspace relay — a
|
||||
link carrying one stays on the relay.
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._filters = {"delay": [10, 0], "frequency_drop": [7]}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
link._filters = {"bpf": ["icmp"]}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_eligible_when_suspended(project):
|
||||
"""
|
||||
A suspended link reports the synthetic frequency_drop emulation through
|
||||
get_active_filters() — that is relay-only mechanics and must not flip a
|
||||
kernel link to the relay on reopen/reset (suspend is carrier-driven).
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._suspended = True
|
||||
assert link.get_active_filters() == {"frequency_drop": [-1]}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_eligible_with_markers(project):
|
||||
@@ -191,7 +225,7 @@ async def test_prepare_relay_link_allocates_udp_ports(project):
|
||||
|
||||
compute1.get_ip_on_same_subnet.side_effect = subnet_callback
|
||||
|
||||
async def port_callback(path, data={}, **kwargs):
|
||||
async def port_callback(path, data=None, **kwargs):
|
||||
response = MagicMock()
|
||||
response.json = {"udp_port": 1024}
|
||||
return response
|
||||
@@ -212,13 +246,29 @@ async def test_prepare_relay_link_allocates_udp_ports(project):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_filters_rejected_on_kernel_link(project):
|
||||
async def test_update_netem_filters_accepted_on_kernel_link(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
await link._prepare()
|
||||
|
||||
await link.update_filters({"delay": [10, 0], "packet_loss": [5]})
|
||||
assert link.filters == {"delay": [10, 0], "packet_loss": [5]}
|
||||
# both endpoints carry the filters (one netem qdisc per veth host end)
|
||||
by_node = {entry[0].id: entry[3] for entry in (await link._prepare())}
|
||||
assert by_node[node1.id]["filters"] == {"delay": [10, 0], "packet_loss": [5]}
|
||||
assert by_node[node2.id]["filters"] == {"delay": [10, 0], "packet_loss": [5]}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_relay_only_filters_rejected_on_kernel_link(project):
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
await link._prepare()
|
||||
|
||||
with pytest.raises(ControllerError, match="kernel-datapath"):
|
||||
await link.update_filters({"delay": [10, 0]})
|
||||
await link.update_filters({"frequency_drop": [10]})
|
||||
with pytest.raises(ControllerError, match="kernel-datapath"):
|
||||
await link.update_filters({"bpf": ["icmp"]})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -565,7 +565,7 @@ async def test_update_ethernet_switch_nio(project):
|
||||
"markers": {},
|
||||
"filters": {},
|
||||
},
|
||||
timeout=221,
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user