mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-13 14:05:36 +03:00
Switch-to-switch links were the last shape left on the relay: the switch fast path works by absorbing the *peer's* anchor, and a switch has none — its ports are its own bridge, and a kernel interface belongs to exactly one bridge, so two bridges cannot be joined by enslaving anything. Join them with a veth pair instead: one end per bridge, each side enslaving its own end and applying its own port mode to it — an access port on one side and a trunk on the other behaves like the real cable it models. uBridge's docker create_veth/delete_veth (the primitive every Docker adapter is already born from) creates and destroys the pair; the names derive from the link id (utils.kernel_anchor.kernel_cascade_names), so each side computes its own end and the names survive a restart. The pair belongs to the link, and the two ends are wired by two independent uBridge processes — the project-open batch drives different nodes concurrently — so the protocol is order-free: both sides create the pair when their own end is missing (one create wins; the loser's failure is re-checked against the kernel and "the pair is there" is not an error), and either side's teardown destroys it (deleting one veth end takes the other with it, so both converge on the same gone pair and the second delif answers ENOENT). A pre-existing end — this switch restarted, or a leftover from a previous life of the link id — is reused rather than recreated, because the other end may still be a live port of the peer switch; a stale tc qdisc is all a reused end can carry. No new wire format was needed: a cascade is an anchor NIO on both ends, the absorbed-anchor case plus the peer end's name. That also corrects the filter routing — the single-sided rule exists because an absorbed anchor is one interface, while a cascade has one per switch, so impairments go to both ends like any other kernel link. Tests: the wire format, the eligibility flip and the both-ends re-push on node start (controller); the naming contract (utils); the switch-side lifecycle — create, reuse, lost create race, delete destroys the pair (compute routes). Drive-by: the relay filter test's compute mock carried no awaitable `put`, so tests/controller/test_kernel_datapath_link.py failed when run alone while an argument-less full run passed; callers now get one.
182 lines
6.4 KiB
Python
182 lines
6.4 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
|
|
bridge: Optional[str] = Field(
|
|
None,
|
|
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")
|