YueGuobin c3c025ab2a
fix: resolve the code-review findings on the kernel datapath
Compute:
- Track the anchor that owns uBridge's single AF_PACKET capture slot: a
  second kernel port's capture is refused with a clear error (its flag
  rolled back), a failed start no longer wedges the port, and only the
  owning port's stop issues the process-wide `capture stop_kernel`
  (docker/qemu/iou/dynamips/switch, via the mixin's slot helpers).
- Unwind failed starts instead of leaking uBridge and the anchor TAPs:
  QEMU's stop body runs under try/finally, QEMU and IOU start the datapath
  inside the launch try, Dynamips undoes a half-attached port (the retry
  used to be short-circuited into a silent black hole), and Docker deletes
  the veth pair when the namespace move fails.
- Restore the relay filter guard on IOU (kernel-only filters raise
  NodeError, an uncompilable BPF line degrades to a warning), validate the
  eBPF/quota/window/bpf filter shapes before touching the bridge (short or
  non-numeric values surfaced as 500s after partial state), and refuse a
  second NIO on an occupied Docker/QEMU port (the guard IOU and Dynamips
  already had).
- Probe fixes: `_probe_bridge_tap` survives a failing uBridge start, and
  the probe caches key on ctime too, so a chmod/setcap remedy re-probes.

Controller:
- Parse GNS3_UNIX_SOCKET_NIO like the compute does (line-anchored, truthy
  value) and mirror the compute's class selection in kernel_anchor_type.
- Read partial capability reports defensively; make BridgeNIO.bridge
  required-but-nullable so an omitted key is a 422 rather than a KeyError.
- Strip the packet_loss correlation slot even when it is zero: the relay
  filter takes exactly one argument and rejected `packet_loss 10 0`.

Ethernet switch: skip ports whose absorbed anchor does not exist yet (the
join is deferred by design) and roll the ports_mapping back when the VLAN
reconcile fails, so a retry still diffs against the old mapping.

Agent: reject device config entries without a device_name instead of
silently dropping them (a malformed batch used to execute partially while
reporting success).

Also split the netem builder to clear C901 and drop the two per-file ruff
ignores the PR had added, keeping the remove-only policy.
2026-10-06 19:55:21 +08:00

185 lines
6.6 KiB
Python

#
# Copyright (C) 2020 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/>.
from enum import Enum
from typing import List, Optional, Union
from pydantic import BaseModel, Field
class UDPNIOType(str, Enum):
udp = "nio_udp"
class UDPNIO(BaseModel):
"""
UDP Network Input/Output properties.
"""
type: UDPNIOType
lport: int = Field(..., gt=0, le=65535, description="Local port")
rhost: str = Field(..., description="Remote host")
rport: int = Field(..., gt=0, le=65535, description="Remote port")
suspend: Optional[bool] = Field(None, description="Suspend the NIO")
filters: Optional[dict] = Field(
None,
description="Packet filters (relay-supported types only: delay/packet_loss/corrupt/frequency_drop/bpf — "
"the netem extensions and the quota mode need a kernel-datapath NIO)",
)
markers: Optional[dict] = Field(None, description="Traffic-insight markers")
class EthernetNIOType(str, Enum):
ethernet = "nio_ethernet"
class EthernetNIO(BaseModel):
"""
Generic Ethernet Network Input/Output properties.
"""
type: EthernetNIOType
ethernet_device: str = Field(..., description="Ethernet device name e.g. eth0")
class TAPNIOType(str, Enum):
tap = "nio_tap"
class TAPNIO(BaseModel):
"""
TAP Network Input/Output properties.
"""
type: TAPNIOType
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. Impairment filters run on the veth host end:
delay/packet_loss/corrupt and the netem extensions (rate, reorder,
gemodel, duplicate, seed, limit, jitter distributions, loss correlation)
as one tc netem qdisc, bpf as cls_bpf match-drop classifiers (needs a
uBridge reporting cbpf), frequency_drop and quota as the eBPF stateful
classifier (needs ebpf). Markers and packet capture are served by
uBridge's AF_PACKET modules on the veth host end.
``bridge`` may be None: the anchor is then bridged by an Ethernet switch
(its own kernel bridge absorbed it) and the node applies only carrier,
markers and impairments — never bridge membership.
"""
type: BridgeNIOType
# Required-but-nullable: the NIO factories read nio_settings["bridge"],
# and the create/update routes drop unset fields — an omitted key would
# pass validation and then surface as a KeyError (500) during binding.
bridge: Optional[str] = Field(
...,
description="Kernel bridge name e.g. gns3a1b2c3d4e5; None when an Ethernet switch "
"owns the anchor's bridge membership (absorbed link)",
)
suspend: Optional[bool] = Field(None, description="Suspend the NIO")
filters: Optional[dict] = Field(
None,
description="Packet filters served on the veth host end (netem surface + extensions via one tc netem qdisc, "
"bpf via cls_bpf, frequency_drop/quota via the eBPF classifier)",
)
markers: Optional[dict] = Field(None, description="Traffic-insight markers (attached to the veth host end)")
class AnchorNIOType(str, Enum):
anchor = "nio_anchor"
class AnchorNIO(BaseModel):
"""
Anchor-absorbing Network Input/Output properties, for a node that owns a
kernel bridge (the Ethernet switch): the named foreign interface — the
peer's kernel-datapath anchor — is joined to the node's own bridge with
the node's port settings applied to it. The mirror of BridgeNIO; filters,
markers and capture ride the NIO and attach to the named anchor.
"""
type: AnchorNIOType
anchor: str = Field(..., description="Host interface to absorb e.g. gv00010203e0p0")
peer: Optional[str] = Field(
None,
description="Switch-to-switch cascade: the other end of the veth pair this port is one end of. Present on "
"exactly one side (the owner, which creates and destroys the pair); absent for an absorbed anchor",
)
suspend: Optional[bool] = Field(None, description="Suspend the NIO")
filters: Optional[dict] = Field(
None,
description="Packet filters served on the absorbed anchor (netem surface + extensions via one tc netem "
"qdisc, bpf via cls_bpf, frequency_drop/quota via the eBPF classifier)",
)
markers: Optional[dict] = Field(None, description="Traffic-insight markers (attached to the absorbed anchor)")
class MarkerToggle(BaseModel):
"""
Body for the per-marker enable/disable toggle endpoint: flips a running
uBridge marker filter with ``enable_packet_filter on|off`` (no NIO rebuild,
so the pcap identity and emitted counter are preserved).
"""
enabled: bool
class MarkerRebuild(BaseModel):
"""
Body for the per-marker rebuild endpoint: re-install a single uBridge marker
filter with new BPF/tag/direction via ``delete_packet_filter`` + add (NOT a
bridge-wide reset), so sibling markers keep their pcaps open. The marker's
own pcap is reopened by uBridge on re-add (new capture session for the new
BPF), which is expected.
"""
bpf: str
tag: Optional[int] = None
direction: Optional[str] = None
enabled: bool = True
link_id: str = ""
class BatchNIOEntry(BaseModel):
"""
A single NIO binding to create as part of a project-wide batch.
"""
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: Union[UDPNIO, BridgeNIO, AnchorNIO] = Field(..., description="NIO settings")
class BatchNIOCreate(BaseModel):
"""
Body for the project-wide batch NIO endpoint: create many NIO bindings in a
single request (used during project open) to avoid one HTTP round-trip per
NIO between controller and compute.
"""
nios: List[BatchNIOEntry] = Field(..., description="NIO bindings to create")