mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
feat(docker): wire Docker-to-Docker links through a kernel bridge
Eligible links (both endpoints Docker, same compute, no filters/markers, stopped nodes, no unix-socket NIO containers) are wired as veth pairs enslaved into a per-link Linux kernel bridge (gns3 + 11 hex of the link id, filling IFNAMSIZ) instead of the uBridge UDP relay, so frames flow entirely in the kernel. - NIOBridge NIO type: the controller decides the datapath in UDPLink._prepare and both computes enslave their deterministic gv*/gc* veth pair via uBridge's docker/brctl modules - carrier/suspend = host-end admin state; both endpoints race-safely create/delete the bridge (EEXIST/EBUSY/ENOENT tolerated) - veth adapters can still attach relay (UDP) links on a running node: lazy relay bridge over AF_PACKET on the host end (add_nio_ethernet) - filters/markers/capture are rejected with explicit errors on this datapath (they live in the uBridge relay); vendor unix-socket containers are excluded; Server.enable_kernel_datapath is the kill-switch (default on) - host-end veths survive container death and are removed on stop plus swept by name on the next start (crash/stale recovery)
This commit is contained in:
13 files changed
+1031
-21
No files matched your search
@@ -260,12 +260,13 @@ async def duplicate_docker_node(
|
||||
@router.post(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
response_model=Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
)
|
||||
async def create_docker_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
|
||||
) -> schemas.UDPNIO:
|
||||
adapter_number: int, port_number: int, nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
node: DockerVM = Depends(dep_node)
|
||||
) -> Union[schemas.UDPNIO, schemas.BridgeNIO]:
|
||||
"""
|
||||
Add a NIO (Network Input/Output) to the node.
|
||||
The port number on the Docker node is always 0.
|
||||
@@ -279,12 +280,13 @@ async def create_docker_node_nio(
|
||||
@router.put(
|
||||
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=schemas.UDPNIO,
|
||||
response_model=Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
dependencies=[Depends(compute_authentication)],
|
||||
)
|
||||
async def update_docker_node_nio(
|
||||
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
|
||||
) -> schemas.UDPNIO:
|
||||
adapter_number: int, port_number: int, nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO],
|
||||
node: DockerVM = Depends(dep_node)
|
||||
) -> Union[schemas.UDPNIO, schemas.BridgeNIO]:
|
||||
"""
|
||||
Update a NIO (Network Input/Output) on the node.
|
||||
The port number on the Docker node is always 0.
|
||||
|
||||
@@ -41,6 +41,7 @@ from ..utils import force_unix_path
|
||||
from ..utils.images import default_images_directory, images_directories, list_images, md5sum, remove_checksum
|
||||
from .base_node import BaseNode
|
||||
from .error import ImageMissingError, NodeError
|
||||
from .nios.nio_bridge import NIOBridge
|
||||
from .nios.nio_ethernet import NIOEthernet
|
||||
from .nios.nio_tap import NIOTAP
|
||||
from .nios.nio_udp import NIOUDP
|
||||
@@ -358,6 +359,11 @@ class BaseManager:
|
||||
nio.filters = nio_settings.get("filters", {})
|
||||
nio.markers = nio_settings.get("markers", {})
|
||||
nio.suspend = nio_settings.get("suspend", False)
|
||||
elif nio_settings["type"] == "nio_bridge":
|
||||
nio = NIOBridge(nio_settings["bridge"])
|
||||
if nio_settings.get("filters") or nio_settings.get("markers"):
|
||||
raise ComputeError("Packet filters and markers are not supported on kernel-datapath links")
|
||||
nio.suspend = nio_settings.get("suspend", False)
|
||||
elif nio_settings["type"] == "nio_tap":
|
||||
tap_device = nio_settings["tap_device"]
|
||||
# if not is_interface_up(tap_device):
|
||||
|
||||
@@ -44,6 +44,7 @@ from gns3server.utils.hostname import is_rfc1123_hostname_valid
|
||||
from ..adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..base_node import BaseNode
|
||||
from ..error import ImageMissingError
|
||||
from ..nios.nio_bridge import NIOBridge
|
||||
from ..nios.nio_udp import NIOUDP
|
||||
from .docker_error import DockerError, DockerHttp404Error, DockerHttp409Error
|
||||
|
||||
@@ -169,6 +170,10 @@ class DockerVM(BaseNode):
|
||||
self._volumes = []
|
||||
# Keep a list of created bridge
|
||||
self._bridges = set()
|
||||
# (adapter_number, port_number) -> veth host-end name for adapters
|
||||
# wired on the kernel datapath (NIOBridge). The host end lives in the
|
||||
# root namespace and survives container death, unlike relay TAPs.
|
||||
self._kernel_veths = {}
|
||||
|
||||
if adapters is None:
|
||||
self.adapters = 1
|
||||
@@ -1422,6 +1427,9 @@ class DockerVM(BaseNode):
|
||||
self._console_websocket = None
|
||||
self._cleanup_console_resources()
|
||||
await self._clean_servers()
|
||||
# veth host ends live in the root namespace and survive container
|
||||
# death — remove them while the uBridge control channel is still up.
|
||||
await self._remove_kernel_veths()
|
||||
await self._stop_ubridge()
|
||||
|
||||
try:
|
||||
@@ -1745,6 +1753,10 @@ class DockerVM(BaseNode):
|
||||
f"Port {port_number} doesn't exist on adapter {adapter_number} of Docker container '{self.name}'"
|
||||
)
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
await self._add_ubridge_kernel_connection(nio, adapter_number, port_number)
|
||||
return
|
||||
|
||||
for index in range(4096):
|
||||
if f"tap-gns3-e{index}" not in psutil.net_if_addrs():
|
||||
adapter.host_ifc = f"tap-gns3-e{index!s}"
|
||||
@@ -1758,11 +1770,7 @@ class DockerVM(BaseNode):
|
||||
self._bridges.add(bridge_name)
|
||||
await self._ubridge_send(f"bridge add_nio_tap {bridge_name} {adapter.host_ifc} off")
|
||||
|
||||
mac_address = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number)
|
||||
custom_adapter = self._get_custom_adapter_settings(adapter_number)
|
||||
custom_mac_address = custom_adapter.get("mac_address")
|
||||
if custom_mac_address:
|
||||
mac_address = custom_mac_address
|
||||
mac_address = self._adapter_mac_address(adapter_number)
|
||||
|
||||
try:
|
||||
await self._ubridge_send(f"docker set_mac_addr {adapter.host_ifc} {mac_address}")
|
||||
@@ -1782,13 +1790,131 @@ class DockerVM(BaseNode):
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
|
||||
def _adapter_mac_address(self, adapter_number):
|
||||
"""
|
||||
MAC address for an adapter: the node base address plus the adapter
|
||||
number, unless the adapter has a custom MAC configured.
|
||||
"""
|
||||
|
||||
mac_address = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number)
|
||||
custom_adapter = self._get_custom_adapter_settings(adapter_number)
|
||||
custom_mac_address = custom_adapter.get("mac_address")
|
||||
if custom_mac_address:
|
||||
mac_address = custom_mac_address
|
||||
return mac_address
|
||||
|
||||
def _veth_names(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Deterministic veth pair names for a kernel-datapath adapter port.
|
||||
The host end stays in the root namespace (enslaved to the per-link
|
||||
kernel bridge); the guest end is moved into the container namespace
|
||||
and renamed. The ``gv``/``gc`` prefixes keep these out of the ``gns3``
|
||||
bridge/TAP name space and serve as search keys for stale-interface
|
||||
cleanup. 8 hex chars of the node id + adapter/port keep the names
|
||||
unique and within IFNAMSIZ (15).
|
||||
"""
|
||||
|
||||
suffix = f"{self._id.replace('-', '')[:8]}e{adapter_number}p{port_number}"
|
||||
return f"gv{suffix}", f"gc{suffix}"
|
||||
|
||||
async def _add_ubridge_kernel_connection(self, nio, adapter_number, port_number=0):
|
||||
"""
|
||||
Wire an adapter port on the kernel datapath (NIOBridge): a veth pair
|
||||
whose host end will be enslaved to the per-link kernel bridge, and
|
||||
whose guest end becomes the container interface (the role the TAP
|
||||
plays on the relay path). No uBridge relay bridge is created — frames
|
||||
flow entirely in the kernel, which is also why filters, markers and
|
||||
capture are unavailable on this datapath.
|
||||
"""
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} doesn't exist on Docker container '{name}'".format(
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
host_ifc, guest_ifc = self._veth_names(adapter_number, port_number)
|
||||
# The host end lives in the root namespace and survives container
|
||||
# death: best-effort removal of a stale pair from a previous run
|
||||
# (unclean stop or crash) before recreating it.
|
||||
for stale_ifc in (host_ifc, guest_ifc):
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'docker delete_veth "{stale_ifc}"')
|
||||
|
||||
mac_address = self._adapter_mac_address(adapter_number)
|
||||
try:
|
||||
await self._ubridge_send(f'docker create_veth "{host_ifc}" "{guest_ifc}"')
|
||||
# create_veth leaves the host end UP; carrier must be off until
|
||||
# a link attaches (mirrors "bridge add_nio_tap ... off").
|
||||
await self._ubridge_send(f'link set "{host_ifc}" down')
|
||||
try:
|
||||
await self._ubridge_send(f"docker set_mac_addr {guest_ifc} {mac_address}")
|
||||
except UbridgeError:
|
||||
log.warning(f"Could not set MAC address {mac_address} on interface {guest_ifc}")
|
||||
|
||||
ifname = self._get_container_ifname(adapter_number)
|
||||
log.debug(f"Move container {self.name} adapter {guest_ifc} -> {ifname} in ns {self._namespace}")
|
||||
try:
|
||||
await self._ubridge_send(f"docker move_to_ns {guest_ifc} {self._namespace} {ifname}")
|
||||
except UbridgeError as e:
|
||||
raise UbridgeNamespaceError(e)
|
||||
except UbridgeNamespaceError:
|
||||
raise
|
||||
except Exception:
|
||||
# Don't leave a half-created pair behind (the next start would
|
||||
# fail on create_veth EEXIST until the stale sweep runs).
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'docker delete_veth "{host_ifc}"')
|
||||
raise
|
||||
|
||||
adapter.host_ifc = host_ifc
|
||||
self._kernel_veths[(adapter_number, port_number)] = host_ifc
|
||||
log.debug(
|
||||
"Created kernel-datapath adapter {adapter_number} port {port_number} with MAC address {mac_address} in namespace {namespace}".format(
|
||||
adapter_number=adapter_number, port_number=port_number,
|
||||
mac_address=mac_address, namespace=self._namespace,
|
||||
)
|
||||
)
|
||||
|
||||
if nio:
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
|
||||
async def _remove_kernel_veths(self):
|
||||
"""
|
||||
Delete the root-namespace veth host ends. Unlike relay TAPs (which die
|
||||
with the container network namespace) the host end outlives the
|
||||
container and must be removed explicitly. Deleting an enslaved veth
|
||||
detaches it from its bridge automatically.
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
for host_ifc in self._kernel_veths.values():
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'docker delete_veth "{host_ifc}"')
|
||||
self._kernel_veths.clear()
|
||||
|
||||
async def _get_namespace(self):
|
||||
|
||||
result = await self.manager.query("GET", f"containers/{self._cid}/json")
|
||||
return int(result["State"]["Pid"])
|
||||
|
||||
async def _set_adapter_carrier(self, adapter_number, connected, port_number=0):
|
||||
"""Replicate a Docker adapter's connection state on its TAP device."""
|
||||
"""Replicate a Docker adapter's connection state on its host interface.
|
||||
|
||||
Kernel-datapath adapters toggle the veth host end's admin state (the
|
||||
container end loses carrier when its peer is down); relay adapters
|
||||
toggle the TAP carrier from the uBridge fd holder.
|
||||
"""
|
||||
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
if host_ifc is not None:
|
||||
state = "up" if connected else "down"
|
||||
await self._ubridge_send(f'link set "{host_ifc}" {state}')
|
||||
return
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
state = "on" if connected else "off"
|
||||
@@ -1796,8 +1922,45 @@ class DockerVM(BaseNode):
|
||||
|
||||
async def _connect_nio(self, adapter_number, nio, port_number=0):
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
if host_ifc is None:
|
||||
raise DockerError(
|
||||
"Adapter {adapter_number} port {port_number} of container '{name}' has no "
|
||||
"kernel-datapath interface (it was started on the relay datapath); "
|
||||
"restart the node to attach a kernel link to it".format(
|
||||
adapter_number=adapter_number, port_number=port_number, name=self.name
|
||||
)
|
||||
)
|
||||
# Both endpoints run this concurrently and either may create the
|
||||
# bridge first: treat an existing bridge as success instead of a
|
||||
# create race failure.
|
||||
try:
|
||||
await self._ubridge_send(f'brctl create "{nio.bridge}"')
|
||||
except UbridgeError:
|
||||
# Raises again if the bridge genuinely does not exist.
|
||||
await self._ubridge_send(f'brctl show "{nio.bridge}"')
|
||||
await self._ubridge_send(f'link set "{nio.bridge}" up')
|
||||
await self._ubridge_send(f'brctl addif "{nio.bridge}" "{host_ifc}"')
|
||||
return
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
await self._ubridge_send(f"bridge add_nio_udp {bridge_name} {nio.lport} {nio.rhost} {nio.rport}")
|
||||
if (adapter_number, port_number) in self._kernel_veths and bridge_name not in self._bridges:
|
||||
# A veth adapter attaching a UDP NIO (kernel link deleted and a
|
||||
# relay link created on the running node): relay through AF_PACKET
|
||||
# on the veth host end instead of a TAP.
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
self._bridges.add(bridge_name)
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_ethernet {bridge_name} "{hostif}"'.format(
|
||||
bridge_name=bridge_name, hostif=self._kernel_veths[(adapter_number, port_number)]
|
||||
)
|
||||
)
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {bridge_name} {lport} {rhost} {rport}".format(
|
||||
bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
if nio.capturing:
|
||||
await self._ubridge_send(f'bridge start_capture {bridge_name} "{nio.pcap_output_file}"')
|
||||
await self._ubridge_send(f"bridge start {bridge_name}")
|
||||
@@ -1840,6 +2003,16 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
if isinstance(nio, NIOBridge):
|
||||
if nio.filters or nio.markers:
|
||||
raise DockerError(
|
||||
"Packet filters and markers are not supported on kernel-datapath links; "
|
||||
"delete and recreate the link to use them"
|
||||
)
|
||||
if self.status == "started":
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
return
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if bridge_name in self._bridges:
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
@@ -1865,16 +2038,41 @@ class DockerVM(BaseNode):
|
||||
await self.stop_capture(adapter_number, port_number)
|
||||
if self.ubridge:
|
||||
nio = adapter.get_nio(port_number)
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if self.status == "started":
|
||||
await self._set_adapter_carrier(adapter_number, False, port_number)
|
||||
await self._ubridge_send(f"bridge stop {bridge_name}")
|
||||
await self._ubridge_send(f"bridge remove_nio_udp {bridge_name} {nio.lport} {nio.rhost} {nio.rport}")
|
||||
if isinstance(nio, NIOBridge):
|
||||
await self._remove_kernel_nio(nio, adapter_number, port_number)
|
||||
else:
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if self.status == "started":
|
||||
await self._set_adapter_carrier(adapter_number, False, port_number)
|
||||
await self._ubridge_send(f"bridge stop {bridge_name}")
|
||||
await self._ubridge_send(
|
||||
"bridge remove_nio_udp {bridge_name} {lport} {rhost} {rport}".format(
|
||||
bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
adapter.remove_nio(port_number)
|
||||
|
||||
log.debug(f"Docker VM '{self.name}' [{self.id}]: {adapter.host_ifc} removed from adapter {adapter_number}")
|
||||
|
||||
async def _remove_kernel_nio(self, nio, adapter_number, port_number=0):
|
||||
"""
|
||||
Detach a kernel-datapath adapter port from its per-link kernel bridge.
|
||||
Both endpoints run this concurrently: deleting a bridge that still has
|
||||
the peer's port enslaved fails with EBUSY, so the last endpoint to
|
||||
remove its port wins the deletion and the loser's failure is expected
|
||||
and suppressed.
|
||||
"""
|
||||
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
if self.status == "started" and host_ifc is not None:
|
||||
await self._set_adapter_carrier(adapter_number, False, port_number)
|
||||
if host_ifc is not None:
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'brctl delif "{nio.bridge}" "{host_ifc}"')
|
||||
with contextlib.suppress(UbridgeError):
|
||||
await self._ubridge_send(f'brctl delete "{nio.bridge}"')
|
||||
|
||||
def get_nio(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Gets an adapter NIO binding.
|
||||
@@ -1964,6 +2162,11 @@ class DockerVM(BaseNode):
|
||||
nio = self.get_nio(adapter_number, port_number)
|
||||
if nio.capturing:
|
||||
raise DockerError(f"Packet capture is already activated on adapter {adapter_number}")
|
||||
if isinstance(nio, NIOBridge):
|
||||
raise DockerError(
|
||||
"Packet capture is not supported on kernel-datapath links (there is no uBridge "
|
||||
"relay in the forwarding path); delete and recreate the link to capture it"
|
||||
)
|
||||
|
||||
nio.start_packet_capture(output_file)
|
||||
if self.status == "started" and self.ubridge:
|
||||
|
||||
@@ -36,6 +36,7 @@ import tempfile
|
||||
|
||||
from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
from gns3server.compute.nios.nio_bridge import NIOBridge
|
||||
from gns3server.utils.asyncio import wait_for_file_creation
|
||||
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||
|
||||
@@ -415,6 +416,12 @@ class VendorDockerVM(DockerVM):
|
||||
if not self._unix_socket_nio:
|
||||
return await super()._add_ubridge_connection(nio, adapter_number, port_number)
|
||||
|
||||
if isinstance(nio, NIOBridge):
|
||||
raise DockerError(
|
||||
f"Container '{self._name}' bridges its adapters through AF_UNIX sockets; "
|
||||
"kernel-datapath links are not supported for this container"
|
||||
)
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#
|
||||
# Copyright (C) 2026 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Interface for kernel-datapath bridge NIOs.
|
||||
|
||||
A NIO of this type instructs the node to enslave its adapter's veth host end
|
||||
into the named Linux kernel bridge instead of wiring a uBridge UDP relay.
|
||||
Frames then flow entirely in the kernel (veth -> bridge -> veth). Filters,
|
||||
markers and packet capture are not available on this datapath (they live in
|
||||
the uBridge relay); the controller must only emit this NIO type for links
|
||||
where those features are not in use.
|
||||
"""
|
||||
|
||||
from .nio import NIO
|
||||
|
||||
|
||||
class NIOBridge(NIO):
|
||||
|
||||
"""
|
||||
Kernel-bridge NIO.
|
||||
|
||||
:param bridge: name of the per-link Linux kernel bridge
|
||||
"""
|
||||
|
||||
def __init__(self, bridge):
|
||||
|
||||
super().__init__()
|
||||
self._bridge = bridge
|
||||
|
||||
@property
|
||||
def bridge(self):
|
||||
"""
|
||||
Returns the kernel bridge name.
|
||||
|
||||
:returns: bridge name
|
||||
"""
|
||||
|
||||
return self._bridge
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "NIO bridge"
|
||||
|
||||
def asdict(self):
|
||||
|
||||
return {
|
||||
"type": "nio_bridge",
|
||||
"bridge": self._bridge,
|
||||
"suspend": self._suspended,
|
||||
"filters": self._filters,
|
||||
"markers": self._markers,
|
||||
}
|
||||
@@ -111,6 +111,15 @@ class Link:
|
||||
"""
|
||||
return self._filters
|
||||
|
||||
@property
|
||||
def kernel_datapath(self):
|
||||
"""
|
||||
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, markers and capture are unavailable).
|
||||
"""
|
||||
return any(d.get("type") == "nio_bridge" for d in (getattr(self, "_link_data", None) or []))
|
||||
|
||||
@property
|
||||
def markers(self):
|
||||
"""
|
||||
@@ -224,6 +233,12 @@ 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 new_filters != self.filters:
|
||||
self._filters = new_filters
|
||||
if self._created:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.utils.packet_filter_validation import validate_bpf_syntax
|
||||
|
||||
from .controller_error import ControllerError, ControllerNotFoundError
|
||||
@@ -46,6 +47,19 @@ _MARKER_CAPABLE_TYPES = frozenset(
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_unix_socket_docker(node):
|
||||
"""
|
||||
Best-effort detection of vendor containers bridged through AF_UNIX
|
||||
socket pairs (GNS3_UNIX_SOCKET_NIO in their environment): they have no
|
||||
host-side interface to enslave into a kernel bridge. The compute side
|
||||
independently rejects kernel-datapath NIOs on these containers, so a
|
||||
missed detection here only surfaces as a clearer-late error.
|
||||
"""
|
||||
|
||||
environment = (node.properties or {}).get("environment") or ""
|
||||
return "GNS3_UNIX_SOCKET_NIO" in environment
|
||||
|
||||
|
||||
class UDPLink(Link):
|
||||
def __init__(self, project, link_id=None):
|
||||
super().__init__(project, link_id=link_id)
|
||||
@@ -110,6 +124,38 @@ class UDPLink(Link):
|
||||
"""
|
||||
return self._markers_for_node(node1), self._markers_for_node(node2)
|
||||
|
||||
def _kernel_datapath_eligible(self, node1, node2):
|
||||
"""
|
||||
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, markers
|
||||
and packet capture — which all live in the relay — disqualify it.
|
||||
It is host-local (same compute) and requires the adapter interfaces
|
||||
to be created as veths at container start, hence stopped nodes only.
|
||||
"""
|
||||
|
||||
if not Config.instance().settings.Server.enable_kernel_datapath:
|
||||
return False
|
||||
if node1.node_type != "docker" or node2.node_type != "docker":
|
||||
return False
|
||||
if node1.compute.id != node2.compute.id:
|
||||
return False
|
||||
if self.get_active_filters():
|
||||
return False
|
||||
if self._markers:
|
||||
return False
|
||||
# Project-level marker definitions are inherited by every link right
|
||||
# after creation (apply_defs_to_new_link) and need the relay datapath.
|
||||
if getattr(self._project, "_marker_definitions", None):
|
||||
return False
|
||||
# Running nodes already have relay TAPs wired into uBridge; the
|
||||
# kernel path needs the veth variant created at container start.
|
||||
if node1.status != "stopped" or node2.status != "stopped":
|
||||
return False
|
||||
if _is_unix_socket_docker(node1) or _is_unix_socket_docker(node2):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _prepare(self):
|
||||
"""
|
||||
Local-only link setup: resolve peer addresses, reserve UDP ports and
|
||||
@@ -135,6 +181,22 @@ class UDPLink(Link):
|
||||
adapter_number2 = self._nodes[1]["adapter_number"]
|
||||
port_number2 = self._nodes[1]["port_number"]
|
||||
|
||||
if self._kernel_datapath_eligible(node1, node2):
|
||||
# Kernel datapath: each endpoint enslaves its veth host end into
|
||||
# a per-link Linux bridge. The name derives from the link id so
|
||||
# both ends compute it independently (no coordinator) and crash
|
||||
# leftovers sweep deterministically; 11 hex chars fill IFNAMSIZ
|
||||
# (15), pushing name-collision probability to irrelevance.
|
||||
bridge_name = "gns3" + self._id.replace("-", "")[:11]
|
||||
self._link_data = [
|
||||
{"type": "nio_bridge", "bridge": bridge_name, "filters": {}, "markers": {}, "suspend": self._suspended},
|
||||
{"type": "nio_bridge", "bridge": bridge_name, "filters": {}, "markers": {}, "suspend": self._suspended},
|
||||
]
|
||||
return [
|
||||
(node1, adapter_number1, port_number1, self._link_data[0]),
|
||||
(node2, adapter_number2, port_number2, self._link_data[1]),
|
||||
]
|
||||
|
||||
# Get an IP allowing communication between both host
|
||||
try:
|
||||
(node1_host, node2_host) = await node1.compute.get_ip_on_same_subnet(node2.compute)
|
||||
@@ -472,6 +534,12 @@ class UDPLink(Link):
|
||||
if name in self._markers:
|
||||
raise ControllerError(f"Marker '{name}' already exists on link {self._id}")
|
||||
|
||||
if self.kernel_datapath:
|
||||
raise ControllerError(
|
||||
"Markers are not supported on kernel-datapath links (no uBridge relay in the "
|
||||
"forwarding path); delete and recreate the link to use markers"
|
||||
)
|
||||
|
||||
# Validate the BPF only for private per-link markers. An inherited copy
|
||||
# (``inherited_from`` set) fans out from a definition whose BPF was
|
||||
# already validated once at create/update (and on project load), so
|
||||
|
||||
@@ -101,7 +101,7 @@ from .compute.iou_nodes import IOU, IOUCreate, IOUStart, IOUUpdate
|
||||
from .compute.nat_nodes import NAT, NATCreate, NATUpdate
|
||||
|
||||
# Compute schemas
|
||||
from .compute.nios import TAPNIO, UDPNIO, BatchNIOCreate, BatchNIOEntry, EthernetNIO, MarkerRebuild, MarkerToggle
|
||||
from .compute.nios import TAPNIO, UDPNIO, BridgeNIO, BatchNIOCreate, BatchNIOEntry, EthernetNIO, MarkerRebuild, MarkerToggle
|
||||
from .compute.qemu_nodes import Qemu, QemuCreate, QemuUpdate
|
||||
from .compute.virtualbox_nodes import VirtualBox, VirtualBoxCreate, VirtualBoxUpdate
|
||||
from .compute.vmware_nodes import VMware, VMwareCreate, VMwareUpdate
|
||||
@@ -166,6 +166,7 @@ __all__ = [
|
||||
"AutoIdlePC",
|
||||
"BatchNIOCreate",
|
||||
"BatchNIOEntry",
|
||||
"BridgeNIO",
|
||||
"C1700DynamipsTemplate",
|
||||
"C1700DynamipsTemplateUpdate",
|
||||
"C2600DynamipsTemplate",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -65,6 +65,27 @@ class TAPNIO(BaseModel):
|
||||
tap_device: str = Field(..., description="TAP device name e.g. tap0")
|
||||
|
||||
|
||||
class BridgeNIOType(str, Enum):
|
||||
|
||||
bridge = "nio_bridge"
|
||||
|
||||
|
||||
class BridgeNIO(BaseModel):
|
||||
"""
|
||||
Kernel-datapath bridge Network Input/Output properties. The NIO tells the
|
||||
node to enslave its veth host end into the named kernel bridge instead of
|
||||
wiring a uBridge UDP relay. ``filters`` and ``markers`` must stay empty on
|
||||
this datapath; they are accepted (and rejected server-side) only because
|
||||
link update payloads always carry the full NIO dictionary.
|
||||
"""
|
||||
|
||||
type: BridgeNIOType
|
||||
bridge: str = Field(..., description="Kernel bridge name e.g. gns3a1b2c3d4e5")
|
||||
suspend: Optional[bool] = Field(None, description="Suspend the NIO")
|
||||
filters: Optional[dict] = Field(None, description="Packet filters (unsupported, must be empty)")
|
||||
markers: Optional[dict] = Field(None, description="Traffic-insight markers (unsupported, must be empty)")
|
||||
|
||||
|
||||
class MarkerToggle(BaseModel):
|
||||
"""
|
||||
Body for the per-marker enable/disable toggle endpoint: flips a running
|
||||
@@ -99,7 +120,7 @@ class BatchNIOEntry(BaseModel):
|
||||
node_id: str = Field(..., description="Node the NIO is attached to")
|
||||
adapter_number: int = Field(0, ge=0, description="Adapter number")
|
||||
port_number: int = Field(0, ge=0, description="Port number")
|
||||
nio: UDPNIO = Field(..., description="NIO settings")
|
||||
nio: Union[UDPNIO, BridgeNIO] = Field(..., description="NIO settings")
|
||||
|
||||
|
||||
class BatchNIOCreate(BaseModel):
|
||||
|
||||
@@ -183,6 +183,11 @@ class ServerSettings(BaseModel):
|
||||
description='Default symbol theme, e.g. "Classic" or "Affinity-square-blue"',
|
||||
)
|
||||
allow_raw_images: bool = Field(True, description="Allow raw images to be uploaded to the server")
|
||||
enable_kernel_datapath: bool = Field(
|
||||
True,
|
||||
description="Wire eligible Docker-to-Docker links (same compute, no filters/markers) through "
|
||||
"kernel veth/bridge interfaces instead of the uBridge UDP relay",
|
||||
)
|
||||
auto_discover_images: bool = Field(True, description="Automatically discover images in the images directory")
|
||||
image_sync_interval: int = Field(900, ge=10, description="Seconds between automatic image inventory scans")
|
||||
report_errors: bool = Field(True, description="Automatically send crash reports to the GNS3 team")
|
||||
|
||||
Reference in new issue
Block a user