Merge pull request #2990 from yueguobin/fix/cloud-hide-kernel-datapath-interfaces

Hide kernel-datapath interfaces from the cloud node
This commit is contained in:
Jeremy Grossmann 2026-10-14 05:55:34 +02:00 committed by GitHub
commit 149549e029
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 108 additions and 4 deletions

View File

@ -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(
{

View File

@ -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

View File

@ -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

View File

@ -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)