mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-05 23:01:15 +02:00
feat(cloud): attach TAP interfaces to existing Linux bridges
This commit is contained in:
1 parent
e57acfb7b1
commit
a9f5306f16
5 files changed
+253
-9
No files matched your search
@@ -297,11 +297,12 @@ class BaseManager:
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def has_privileged_access(executable):
|
||||
def has_privileged_access(executable, *, require_net_admin=False):
|
||||
"""
|
||||
Check if an executable have the right to attach to Ethernet and TAP adapters.
|
||||
|
||||
:param executable: executable path
|
||||
:param require_net_admin: also require CAP_NET_ADMIN for network configuration
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
@@ -324,8 +325,8 @@ class BaseManager:
|
||||
try:
|
||||
if sys.platform.startswith("linux") and "security.capability" in os.listxattr(executable):
|
||||
caps = os.getxattr(executable, "security.capability")
|
||||
# test the 2nd byte and check if the 13th bit (CAP_NET_RAW) is set
|
||||
if struct.unpack("<IIIII", caps)[1] & 1 << 13:
|
||||
required = (1 << 13) | ((1 << 12) if require_net_admin else 0)
|
||||
if struct.unpack("<IIIII", caps)[1] & required == required:
|
||||
return True
|
||||
except (AttributeError, OSError) as e:
|
||||
log.error(f"Could not determine if CAP_NET_RAW capability is set for {executable}: {e}")
|
||||
|
||||
@@ -15,12 +15,15 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import gns3server.utils.asyncio
|
||||
import gns3server.utils.interfaces
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.schemas.compute.cloud_nodes import LINUX_TAP_NAME_PATTERN
|
||||
from gns3server.utils.kernel_anchor import is_kernel_datapath_interface
|
||||
|
||||
from ...base_node import BaseNode
|
||||
@@ -69,7 +72,7 @@ class Cloud(BaseNode):
|
||||
for port in ports:
|
||||
port["port_number"] = port_number
|
||||
port_number += 1
|
||||
self._ports_mapping = ports
|
||||
self._ports_mapping = self._normalize_ports_mapping(ports)
|
||||
|
||||
@property
|
||||
def nios(self):
|
||||
@@ -101,6 +104,9 @@ class Cloud(BaseNode):
|
||||
}
|
||||
)
|
||||
|
||||
if sys.platform.startswith("linux") and gns3server.utils.interfaces.is_interface_bridge(interface["name"]):
|
||||
host_interfaces[-1]["is_bridge"] = True
|
||||
|
||||
return {
|
||||
"name": self.name,
|
||||
"usage": self.usage,
|
||||
@@ -206,6 +212,11 @@ class Cloud(BaseNode):
|
||||
|
||||
return self._ports_mapping
|
||||
|
||||
@staticmethod
|
||||
def _normalize_ports_mapping(ports):
|
||||
# API responses include bridge=None; legacy projects omit it.
|
||||
return [{key: value for key, value in port.items() if key != "bridge" or value is not None} for port in ports]
|
||||
|
||||
@ports_mapping.setter
|
||||
def ports_mapping(self, ports):
|
||||
"""
|
||||
@@ -214,6 +225,7 @@ class Cloud(BaseNode):
|
||||
:param ports: ports info
|
||||
"""
|
||||
|
||||
ports = self._normalize_ports_mapping(ports)
|
||||
if ports != self._ports_mapping:
|
||||
if len(self._nios) > 0:
|
||||
raise NodeError("Cannot modify a cloud that is already connected.")
|
||||
@@ -314,7 +326,9 @@ class Cloud(BaseNode):
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
await self._ubridge_apply_markers(bridge_name, nio)
|
||||
if port_info["type"] in ("ethernet", "tap"):
|
||||
if not self.manager.has_privileged_access(self.ubridge_path):
|
||||
if not self.manager.has_privileged_access(
|
||||
self.ubridge_path, require_net_admin=bool(port_info.get("bridge"))
|
||||
):
|
||||
raise NodeError(
|
||||
"uBridge requires root access or the capability to interact with Ethernet and TAP adapters"
|
||||
)
|
||||
@@ -334,9 +348,7 @@ class Cloud(BaseNode):
|
||||
await self._add_windows_ethernet(port_info, bridge_name)
|
||||
|
||||
elif port_info["type"] == "tap":
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_tap {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"])
|
||||
)
|
||||
await self._add_tap_interface(port_info, bridge_name)
|
||||
|
||||
elif port_info["type"] == "udp":
|
||||
await self._ubridge_send(
|
||||
@@ -350,6 +362,32 @@ class Cloud(BaseNode):
|
||||
|
||||
await self._ubridge_send(f"bridge start {bridge_name}")
|
||||
|
||||
async def _add_tap_interface(self, port_info, bridge_name):
|
||||
"""Open a TAP and restore its optional host bridge assignment."""
|
||||
target_bridge = port_info.get("bridge")
|
||||
if target_bridge:
|
||||
self._validate_tap_bridge(port_info["interface"], target_bridge)
|
||||
await self._ubridge_send(
|
||||
'bridge add_nio_tap {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"])
|
||||
)
|
||||
|
||||
if target_bridge:
|
||||
await self._ubridge_send(f'brctl addif "{target_bridge}" "{port_info["interface"]}"')
|
||||
|
||||
def _validate_tap_bridge(self, tap, target_bridge):
|
||||
"""Validate host resources before creating or attaching a Cloud TAP."""
|
||||
if not sys.platform.startswith("linux"):
|
||||
raise NodeError("TAP bridge assignments are only supported on Linux")
|
||||
if not all(re.fullmatch(LINUX_TAP_NAME_PATTERN, name) for name in (target_bridge, tap)):
|
||||
raise NodeError("Invalid TAP or bridge interface name")
|
||||
if target_bridge not in {
|
||||
interface["name"] for interface in self._interfaces()
|
||||
} or not gns3server.utils.interfaces.is_interface_bridge(target_bridge):
|
||||
raise NodeError(f"Bridge '{target_bridge}' is missing or not allowed on this server")
|
||||
master_path = os.path.join("/sys/class/net", tap, "master")
|
||||
if os.path.exists(master_path) and os.path.basename(os.path.realpath(master_path)) != target_bridge:
|
||||
raise NodeError(f"TAP '{tap}' already belongs to another bridge")
|
||||
|
||||
async def _add_linux_ethernet(self, port_info, bridge_name):
|
||||
"""
|
||||
Connects an Ethernet interface on Linux using raw sockets.
|
||||
|
||||
@@ -14,15 +14,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from ..common import NodeStatus
|
||||
from ..update import PartialUpdateModel
|
||||
|
||||
LINUX_TAP_NAME_PATTERN = r"^[a-zA-Z0-9_.:-]{1,15}$"
|
||||
|
||||
|
||||
class HostInterfaceType(str, Enum):
|
||||
ethernet = "ethernet"
|
||||
@@ -57,6 +60,7 @@ class HostInterface(BaseModel):
|
||||
name: str = Field(..., description="Interface name")
|
||||
type: HostInterfaceType = Field(..., description="Interface type")
|
||||
special: bool = Field(..., description="Whether the interface is non standard")
|
||||
is_bridge: bool = Field(False, description="Whether this is a Linux bridge")
|
||||
ip_addresses: List[HostInterfaceIPAddress] = Field(
|
||||
default_factory=list, description="All IPv4 and IPv6 addresses on this interface"
|
||||
)
|
||||
@@ -94,6 +98,19 @@ class TAPPort(BaseModel):
|
||||
port_number: int
|
||||
type: TAPType
|
||||
interface: str
|
||||
bridge: Optional[str] = Field(
|
||||
None,
|
||||
min_length=1,
|
||||
max_length=15,
|
||||
pattern=LINUX_TAP_NAME_PATTERN,
|
||||
description="Existing Linux bridge to attach the TAP to",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_bridged_interface(self):
|
||||
if self.bridge and not re.fullmatch(LINUX_TAP_NAME_PATTERN, self.interface):
|
||||
raise ValueError("A bridged TAP name must be 1-15 letters, digits, underscores, dots, colons or hyphens")
|
||||
return self
|
||||
|
||||
|
||||
class UDPType(str, Enum):
|
||||
|
||||
Reference in new issue
Block a user