From c0568dc25f79bf727599de54204800617d8479ae Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 14 Oct 2026 00:14:41 +0800 Subject: [PATCH] fix: hide kernel-datapath interfaces from the cloud node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cloud node offered the kernel datapath's host-side interfaces — node anchors (gv/gq/gi/gd/gx/gp), switch cascade veth ends (gs) and cross-compute VXLAN devices (gvl) — in its interface list and even pre-filled them as ports on a freshly created cloud: none of them is flagged special and only "gns3*" names were filtered. These interfaces belong to the link or node that created them for the project's whole lifetime, so bridging a cloud to one would race its owner. Add is_kernel_datapath_interface() next to the naming contract in utils/kernel_anchor.py: exact-shape matching (prefix plus its fixed-length id-derived tail), so a user interface that merely starts with "gv" is never swallowed. The cloud keeps managed names out of its host-interface list and default ports mapping, while saved mappings keep working (the existence checks still see the full interface list). --- gns3server/compute/builtin/nodes/cloud.py | 11 +++++-- gns3server/utils/kernel_anchor.py | 34 +++++++++++++++++++++ tests/compute/builtin/nodes/test_cloud.py | 30 ++++++++++++++++++ tests/utils/test_kernel_anchor.py | 37 ++++++++++++++++++++++- 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/builtin/nodes/cloud.py b/gns3server/compute/builtin/nodes/cloud.py index c1cfdc251..2ffeda579 100644 --- a/gns3server/compute/builtin/nodes/cloud.py +++ b/gns3server/compute/builtin/nodes/cloud.py @@ -21,6 +21,7 @@ import sys import gns3server.utils.asyncio import gns3server.utils.interfaces from gns3server.compute.ubridge.ubridge_error import UbridgeError +from gns3server.utils.kernel_anchor import is_kernel_datapath_interface from ...base_node import BaseNode from ...error import NodeError @@ -52,7 +53,9 @@ class Cloud(BaseNode): if not ports or len(ports) == 0: self._ports_mapping = [] for interface in self._interfaces(): - if not interface["special"]: + # kernel-datapath interfaces (node anchors, cascade veths, VXLAN + # devices) belong to their creating link or node, never the cloud + if not interface["special"] and not is_kernel_datapath_interface(interface["name"]): self._ports_mapping.append( { "interface": interface["name"], @@ -80,8 +83,10 @@ class Cloud(BaseNode): host_interfaces = [] network_interfaces = gns3server.utils.interfaces.interfaces() for interface in network_interfaces: - # Hide GNS3 internal bridges (e.g. EthernetSwitch kernel bridges) - if interface["name"].lower().startswith("gns3"): + # Hide GNS3 internal interfaces: kernel bridges ("gns3*" names) and + # the kernel-datapath devices (node anchors, cascade veths, VXLAN + # devices) a link or node of some project owns + if interface["name"].lower().startswith("gns3") or is_kernel_datapath_interface(interface["name"]): continue host_interfaces.append( { diff --git a/gns3server/utils/kernel_anchor.py b/gns3server/utils/kernel_anchor.py index b5f39b784..7ca223f85 100644 --- a/gns3server/utils/kernel_anchor.py +++ b/gns3server/utils/kernel_anchor.py @@ -36,6 +36,7 @@ is what makes links to stopped nodes wireable. """ import logging +import re log = logging.getLogger(__name__) @@ -113,3 +114,36 @@ def kernel_cascade_names(link_id): stem = str(link_id).replace("-", "")[:10] return f"gs{stem}0", f"gs{stem}1" + + +# Host-side interfaces the kernel datapath owns outright, recognised by +# exact shape — the prefix plus its fixed-length id-derived tail — never by +# bare prefix, so a user interface that merely starts with "gv" or "gs" is +# not swallowed. Consumers keep these out of human-facing interface lists: +# each belongs to the link or node that created it for as long as the +# project exists (the names are deterministic, valid whether it runs or +# not), so nothing else may bridge to it. +_MANAGED_ANCHOR_PREFIXES = frozenset(ANCHOR_PREFIX.values()) +_MANAGED_ANCHOR_SUFFIX_SHAPE = re.compile(r"[0-9a-f]{8}e\d{1,2}p\d{1,2}") +_MANAGED_CASCADE_SHAPE = re.compile(r"gs[0-9a-f]{10}[01]") +# a cross-compute VXLAN device: "gvl" + 8 hex of the link id + 4 hex of the +# endpoint node id (the cross-compute VXLAN naming keeps its devices out of +# the anchor and "gns3" namespaces the same deterministic way) +_MANAGED_VXLAN_SHAPE = re.compile(r"gvl[0-9a-f]{12}") + + +def is_kernel_datapath_interface(name): + """ + Whether *name* is a host interface the kernel datapath manages by name — + a node anchor, a switch cascade veth end, or a cross-compute VXLAN + device. True for exactly the names the naming contract above can + produce: the cloud node (and any other interface picker) uses this to + refuse offering them, since bridging to one would race whatever link or + node already owns it. + """ + + # "gvl" begins with the docker anchor prefix "gv", so an anchor miss must + # still fall through to the VXLAN shape + if name[:2] in _MANAGED_ANCHOR_PREFIXES and _MANAGED_ANCHOR_SUFFIX_SHAPE.fullmatch(name[2:]): + return True + return _MANAGED_CASCADE_SHAPE.fullmatch(name) is not None or _MANAGED_VXLAN_SHAPE.fullmatch(name) is not None diff --git a/tests/compute/builtin/nodes/test_cloud.py b/tests/compute/builtin/nodes/test_cloud.py index 14cb7633d..180ab167d 100644 --- a/tests/compute/builtin/nodes/test_cloud.py +++ b/tests/compute/builtin/nodes/test_cloud.py @@ -156,6 +156,36 @@ async def test_json_without_ports(on_gns3vm, compute_project, manager): } +async def test_kernel_datapath_interfaces_are_hidden(linux_platform, compute_project, manager): + """ + Kernel-datapath interfaces (node anchors, cascade veths, VXLAN devices) + are neither pre-filled as cloud ports nor listed as host interfaces, + while look-alike user interfaces survive both. + """ + + host_interfaces = [ + {"name": "eth0", "special": False, "type": "ethernet"}, + {"name": "gv00010203e0p0", "special": False, "type": "ethernet"}, # docker anchor + {"name": "gq00010203e0p0", "special": False, "type": "ethernet"}, # qemu anchor + {"name": "gi00010203e0p0", "special": False, "type": "ethernet"}, # iou anchor + {"name": "gd00010203e0p0", "special": False, "type": "ethernet"}, # dynamips anchor + {"name": "gx00010203e0p0", "special": False, "type": "ethernet"}, # iol runner anchor + {"name": "gp00010203e0p0", "special": False, "type": "ethernet"}, # vpcs anchor + {"name": "gs1a2b3c4d5e0", "special": False, "type": "ethernet"}, # cascade veth end + {"name": "gvl1a2b3c4d0001", "special": False, "type": "ethernet"}, # VXLAN device + {"name": "gv0", "special": False, "type": "ethernet"}, # look-alike user interface + {"name": "gs1a2b3c4d5e", "special": False, "type": "ethernet"}, # look-alike (no side digit) + {"name": "gvl1a2b3c4d5e", "special": False, "type": "ethernet"}, # look-alike (short tail) + ] + + with patch("gns3server.utils.interfaces.interfaces", return_value=host_interfaces): + cloud = Cloud("cloud1", str(uuid.uuid4()), compute_project, manager, ports=None) + interfaces = [interface["name"] for interface in cloud.asdict()["interfaces"]] + + assert [port["interface"] for port in cloud.ports_mapping] == ["eth0", "gv0", "gs1a2b3c4d5e", "gvl1a2b3c4d5e"] + assert interfaces == ["eth0", "gv0", "gs1a2b3c4d5e", "gvl1a2b3c4d5e"] + + async def test_update_port_mappings(on_gns3vm, compute_project): """ We don't allow an empty interface in the middle of port list diff --git a/tests/utils/test_kernel_anchor.py b/tests/utils/test_kernel_anchor.py index dda820b56..c9a298d23 100644 --- a/tests/utils/test_kernel_anchor.py +++ b/tests/utils/test_kernel_anchor.py @@ -21,7 +21,13 @@ Ethernet-switch link), plus the cascade veth naming for switch-to-switch links. """ -from gns3server.utils.kernel_anchor import anchor_suffix, kernel_anchor_name, kernel_anchor_type, kernel_cascade_names +from gns3server.utils.kernel_anchor import ( + anchor_suffix, + is_kernel_datapath_interface, + kernel_anchor_name, + kernel_anchor_type, + kernel_cascade_names, +) NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f" LINK_ID = "1a2b3c4d-5e6f-4a5b-8c9d-0e1f2a3b4c5d" @@ -86,3 +92,32 @@ def test_cascade_names_stay_out_of_the_node_anchor_namespace(): # cascade end is never mistaken for another node's anchor for end in kernel_cascade_names(LINK_ID): assert not any(end.startswith(prefix) for prefix in ("gv", "gq", "gi", "gd", "gx")) + + +def test_every_name_the_contract_produces_is_a_managed_interface(): + for node_type in ("docker", "qemu", "iou", "dynamips", "iol_docker", "vpcs"): + assert is_kernel_datapath_interface(kernel_anchor_name(node_type, NODE_ID, 0, 0)) + for end in kernel_cascade_names(LINK_ID): + assert is_kernel_datapath_interface(end) + # a cross-compute VXLAN device: "gvl" + 8 hex of the link id + 4 hex of the node id + assert is_kernel_datapath_interface("gvl" + LINK_ID.replace("-", "")[:8] + NODE_ID.replace("-", "")[:4]) + + +def test_lookalike_interfaces_are_not_managed(): + # recognition is by exact shape, not bare prefix: user interfaces that + # merely start with a managed prefix stay visible to the cloud node + for name in ( + "eth0", + "g", + "gs", + "gvl", + "gv0", + "gp1", + "gv00010203e0p", # anchor without the port + "gv00010203Xe0p0", # non-hex node id + "gs1a2b3c4d5e", # cascade without the side digit + "gs1a2b3c4d5e2", # side digit other than 0/1 + "gvl1a2b3c4d5e", # VXLAN with a short tail + "GV00010203E0P0", # the contract only ever produces lowercase + ): + assert not is_kernel_datapath_interface(name)