mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51: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
@@ -116,8 +116,9 @@ class Link:
|
||||
"""
|
||||
Whether this link is wired on the kernel datapath (veth pairs
|
||||
enslaved into a per-link Linux bridge — no uBridge relay in the
|
||||
forwarding path, so filters and markers are unavailable; capture is
|
||||
served by uBridge's AF_PACKET module).
|
||||
forwarding path). Impairment filters run as tc netem on the veth
|
||||
host end; markers and capture are served by uBridge's AF_PACKET
|
||||
module.
|
||||
"""
|
||||
return any(d.get("type") == "nio_bridge" for d in (getattr(self, "_link_data", None) or []))
|
||||
|
||||
@@ -234,11 +235,18 @@ class Link:
|
||||
except FilterValidationError as e:
|
||||
raise ControllerError(f"Invalid packet filter parameters: {e!s}")
|
||||
|
||||
if new_filters and self.kernel_datapath:
|
||||
raise ControllerError(
|
||||
"Packet filters are not supported on kernel-datapath links (no uBridge relay "
|
||||
"in the forwarding path); delete and recreate the link to use filters"
|
||||
)
|
||||
if self.kernel_datapath:
|
||||
from gns3server.utils.packet_filter_validation import KERNEL_UNSUPPORTED_FILTERS
|
||||
|
||||
unsupported = KERNEL_UNSUPPORTED_FILTERS.intersection(new_filters or {})
|
||||
if unsupported:
|
||||
raise ControllerError(
|
||||
"Packet filter(s) {} cannot run on a kernel-datapath link (no uBridge relay "
|
||||
"in the forwarding path; delay, packet loss and corrupt are served by tc "
|
||||
"netem on the veth); delete and recreate the link to use them".format(
|
||||
", ".join(sorted(unsupported))
|
||||
)
|
||||
)
|
||||
|
||||
if new_filters != self.filters:
|
||||
self._filters = new_filters
|
||||
@@ -637,6 +645,13 @@ class Link:
|
||||
"""
|
||||
filter_node = self._get_filter_node()
|
||||
if filter_node:
|
||||
if self.kernel_datapath:
|
||||
# Kernel-datapath links serve filters via tc netem on the
|
||||
# veth host end; types without a netem equivalent (which
|
||||
# only run in the uBridge relay) are hidden from the picker.
|
||||
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
|
||||
return []
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.utils.packet_filter_validation import validate_bpf_syntax
|
||||
from gns3server.utils.packet_filter_validation import (
|
||||
KERNEL_UNSUPPORTED_FILTERS,
|
||||
validate_bpf_syntax,
|
||||
)
|
||||
|
||||
from .controller_error import ControllerError, ControllerNotFoundError
|
||||
from .link import _UNSET, Link
|
||||
@@ -82,19 +85,32 @@ class UDPLink(Link):
|
||||
"""Whether NIO creation is waiting for missing node images."""
|
||||
return self._deferred
|
||||
|
||||
def _get_node_filters(self, node1, node2):
|
||||
def _get_node_filters(self, node1, node2, kernel=None):
|
||||
"""
|
||||
Determine which node gets the active filters applied.
|
||||
|
||||
:param kernel: explicit datapath decision for the caller's context.
|
||||
``_prepare`` must pass it: it clears ``_link_data`` (the source
|
||||
of the ``kernel_datapath`` property) before rebuilding the NIO
|
||||
specs, so deriving the datapath there would read the previous
|
||||
run's. Callers operating on established link data (``update``)
|
||||
derive it from the property.
|
||||
|
||||
:returns: Tuple of (node1_filters, node2_filters)
|
||||
"""
|
||||
# Kernel-datapath links implement suspend natively (interface carrier
|
||||
# driven by the NIO suspend flag). The synthetic frequency_drop filter
|
||||
# that get_active_filters() injects for suspended links is a
|
||||
# relay-datapath emulation mechanism — sending it would trip the
|
||||
# compute-side kernel guard, so kernel links always push empty filters.
|
||||
if self.kernel_datapath:
|
||||
return {}, {}
|
||||
# relay-datapath emulation mechanism, so kernel links push the *real*
|
||||
# filters straight from storage. Both endpoints receive them: each
|
||||
# end's compute attaches one tc netem qdisc to its veth host end
|
||||
# (egress = traffic entering that container), 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.
|
||||
if kernel is None:
|
||||
kernel = self.kernel_datapath
|
||||
if kernel:
|
||||
return self._filters, self._filters
|
||||
filter_node = self._get_filter_node()
|
||||
return (
|
||||
self.get_active_filters() if filter_node == node1 else {},
|
||||
@@ -135,9 +151,11 @@ class UDPLink(Link):
|
||||
"""
|
||||
Whether this link can be wired on the kernel datapath (veth pairs
|
||||
enslaved into a per-link Linux bridge) instead of the uBridge UDP
|
||||
relay. The kernel path has no userspace relay, so filters — which
|
||||
live in the relay — disqualify it. Markers and capture are served
|
||||
by uBridge's AF_PACKET modules on the veth host end.
|
||||
relay. Impairment filters with a tc netem equivalent (delay,
|
||||
packet_loss, corrupt) are served on the veth host end; the others
|
||||
(frequency_drop, bpf) only exist in the uBridge relay and disqualify
|
||||
the kernel path. Capture and markers are served by uBridge's
|
||||
AF_PACKET modules on the veth host end.
|
||||
|
||||
Docker adapters are born as veth pairs (unified interface), so the
|
||||
datapath is a runtime decision — links attach to running containers
|
||||
@@ -152,7 +170,11 @@ class UDPLink(Link):
|
||||
return False
|
||||
if node1.compute.id != node2.compute.id:
|
||||
return False
|
||||
if self.get_active_filters():
|
||||
# Look at the *stored* filters, not get_active_filters(): a suspended
|
||||
# link reports the synthetic frequency_drop emulation, which is
|
||||
# relay-only mechanics and must not flip a kernel link to the relay
|
||||
# on reopen/reset.
|
||||
if KERNEL_UNSUPPORTED_FILTERS.intersection(self._filters or {}):
|
||||
return False
|
||||
if _is_unix_socket_docker(node1) or _is_unix_socket_docker(node2):
|
||||
return False
|
||||
@@ -192,11 +214,27 @@ class UDPLink(Link):
|
||||
# Markers ride the NIO like on the relay datapath, routed by
|
||||
# capture node; they attach to the veth host end via uBridge's
|
||||
# AF_PACKET marker module instead of a relay `mark` filter.
|
||||
# Filters (delay/packet_loss/corrupt — eligibility guarantees no
|
||||
# other type is present) become one tc netem qdisc per veth host
|
||||
# end, pushed to both endpoints.
|
||||
bridge_name = "gns3" + self._id.replace("-", "")[:11]
|
||||
node1_filters, node2_filters = self._get_node_filters(node1, node2, kernel=True)
|
||||
node1_markers, node2_markers = self._get_node_markers(node1, node2)
|
||||
self._link_data = [
|
||||
{"type": "nio_bridge", "bridge": bridge_name, "filters": {}, "markers": node1_markers, "suspend": self._suspended},
|
||||
{"type": "nio_bridge", "bridge": bridge_name, "filters": {}, "markers": node2_markers, "suspend": self._suspended},
|
||||
{
|
||||
"type": "nio_bridge",
|
||||
"bridge": bridge_name,
|
||||
"filters": node1_filters,
|
||||
"markers": node1_markers,
|
||||
"suspend": self._suspended,
|
||||
},
|
||||
{
|
||||
"type": "nio_bridge",
|
||||
"bridge": bridge_name,
|
||||
"filters": node2_filters,
|
||||
"markers": node2_markers,
|
||||
"suspend": self._suspended,
|
||||
},
|
||||
]
|
||||
return [
|
||||
(node1, adapter_number1, port_number1, self._link_data[0]),
|
||||
@@ -223,7 +261,7 @@ class UDPLink(Link):
|
||||
_allocate_port(node1.compute), _allocate_port(node2.compute)
|
||||
)
|
||||
|
||||
node1_filters, node2_filters = self._get_node_filters(node1, node2)
|
||||
node1_filters, node2_filters = self._get_node_filters(node1, node2, kernel=False)
|
||||
node1_markers, node2_markers = self._get_node_markers(node1, node2)
|
||||
|
||||
# Build the tunnel specs for both sides. Index 0 is always node1 so
|
||||
@@ -331,7 +369,7 @@ class UDPLink(Link):
|
||||
self._link_data[1]["suspend"] = self._suspended
|
||||
if node2.node_type != "ethernet_hub":
|
||||
await node2.put(
|
||||
f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=221
|
||||
f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=120
|
||||
)
|
||||
|
||||
async def delete(self):
|
||||
|
||||
Reference in new issue
Block a user