mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 21:45:41 +03:00
The live suite asserted the datapath over IPv4 only. Give every guest a ULA IPv6 address next to its v4 one and twin every v4 assertion point, sharing the impairment window (one filter application, both families asserted) so the runtime grows by minutes, not double. - harness: configure_ios(eth_ipv6=), ping() dispatches to `ping ipv6`, docker_bring_up_ipv6 (the Docker daemon leaves eth0 with disable_ipv6=1 on v6-less bridge networks, so the interfaces-file route cannot work), family-aware docker_wait_address, pcap_records scans every record (a leading DAD/MLD frame must not mask the sampled ethertype), and pcap_icmp_rtt measures the wire round trip from capture timestamps; - scenarios: connectivity/delay/suspend/isolation/churn checks in both families, captures assert 86dd next to 0800, the libpcap bpf drops are asserted family-selective (icmp vs icmp6, each killing its own family and leaving the other alive), the eBPF classifier modes on both families, marker v6 twins with an echo-specific expression (bare icmp6 also matches DAD/MLD and would break the exact counts), the v6 sharkd replay, and the link-local matrix gains the 86dd all-nodes row; serial (HDLC) legs stay v4-only; - the dynamips-emulated classic IOS under-reports its `ping ipv6` RTT by ~40 % of a delayed round trip (pcap-verified: the wire carries the v6 echo exchange in the same ~230 ms as v4 while the console reports ~130 ms), so the dynamips-family scenarios assert the v6 delay from the capture's own timestamps — recorded in tests/e2e/README.md. Full suite: 17 passed in 58m50s.
342 lines
16 KiB
Python
342 lines
16 KiB
Python
#
|
|
# Copyright (C) 2026 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/>.
|
|
|
|
"""
|
|
Live end-to-end test for the Dynamips kernel datapath (pytest marker
|
|
``e2e``): two real c7200 routers on a real IOS image, one Ethernet link on
|
|
the kernel path and one serial link on the relay, driven through the
|
|
controller REST API and the IOS consoles. Real ICMP proves the datapath:
|
|
|
|
* the link reports ``kernel_datapath`` and the per-link kernel bridge on
|
|
this host has exactly the two anchor TAPs enslaved (kernel objects, not
|
|
server bookkeeping);
|
|
* a ``delay 100`` filter shows up as netem on the anchors and the measured
|
|
RTT rises by ~2x100 ms; clearing it resets the qdiscs and the RTT;
|
|
* suspend drives the anchor administratively down and the pings stop;
|
|
* capture writes a pcap full of ICMP frames;
|
|
* a serial link between the same routers stays on the relay and still
|
|
pings (the relay regression net);
|
|
* deleting / re-creating the link, and stopping / starting a router, leave
|
|
the wiring consistent;
|
|
* deleting the project leaves no anchors or bridges behind.
|
|
|
|
``test_dynamips_relay_control`` is the negative control: the same topology
|
|
on an isolated relay-configured instance wires nothing into the kernel — no
|
|
per-link bridge, nothing enslaved — and still pings, so the kernel objects
|
|
above can only come from the kernel datapath. (The anchors themselves exist
|
|
on both: they are the adapter's port interfaces, created by node start
|
|
whenever the tap module is available, exactly like QEMU's TAPs.)
|
|
|
|
Every Ethernet checkpoint runs dual-stack (the routers carry a ULA v6
|
|
address next to their v4 one and each checkpoint pings both families); the
|
|
serial leg stays v4-only — it is HDLC on the relay, not an Ethernet wire.
|
|
"""
|
|
|
|
import re
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from tests.e2e import harness
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
# c7200 slot 0 is the fixed IO slot; the Ethernet PA goes in slot 1 (two
|
|
# FE ports) and the serial PA in slot 2 (four ports).
|
|
SLOTS = {"slot1": "PA-2FE-TX", "slot2": "PA-4T+"}
|
|
ETH = (1, 0)
|
|
SERIAL = (2, 0)
|
|
|
|
# the dual-stack half of the Ethernet leg: a ULA mirroring 10.1.1.x. The
|
|
# serial leg stays v4-only (HDLC, not an Ethernet wire).
|
|
R1_IP6 = "fd00:1:1::1"
|
|
R2_IP6 = "fd00:1:1::2"
|
|
|
|
|
|
def _pick_image(server):
|
|
images = server.compute.dynamips_images()
|
|
image = next((name for name in images if name.startswith("c7200")), None)
|
|
if not image:
|
|
pytest.skip(f"no c7200 image available on this server (images: {images[:5]})")
|
|
return image
|
|
|
|
|
|
def _boot(server, project_id, node_id, timeout=360):
|
|
console = harness.ios_console(server, project_id, node_id)
|
|
console.boot_wait(timeout=timeout)
|
|
return console
|
|
|
|
|
|
def _eth_shutdown(console, shut):
|
|
console.run("conf t")
|
|
console.run("interface f1/0")
|
|
console.run("shutdown" if shut else "no shutdown")
|
|
console.run("end")
|
|
|
|
|
|
def test_dynamips_kernel_datapath():
|
|
server = harness.live_server(kernel=True)
|
|
compute = server.compute
|
|
if compute.capabilities().get("ubridge_tap") is not True:
|
|
pytest.skip("this compute's uBridge cannot create persistent TAPs (ubridge_tap != True)")
|
|
image = _pick_image(server)
|
|
idlepc = harness.dynamips_idlepc(server, image)
|
|
|
|
project = compute.create_project("dyn-e2e")
|
|
pid = project["project_id"]
|
|
t1 = t2 = None
|
|
try:
|
|
r1 = compute.create_dynamips_router(pid, "E2E-R1", image, SLOTS, idlepc=idlepc)
|
|
r2 = compute.create_dynamips_router(pid, "E2E-R2", image, SLOTS, idlepc=idlepc)
|
|
r1_id, r2_id = r1["node_id"], r2["node_id"]
|
|
t1 = harness.anchor_name(r1_id, *ETH)
|
|
t2 = harness.anchor_name(r2_id, *ETH)
|
|
|
|
# The link is created while both routers are stopped: the kernel NIO
|
|
# is bound with no anchors in existence yet and attached by node
|
|
# start (the deferred-wiring path).
|
|
link = compute.create_link(pid, (r1_id, *ETH), (r2_id, *ETH))
|
|
lid = link["link_id"]
|
|
bridge = harness.link_bridge_name(lid)
|
|
assert link["kernel_datapath"] is True, link
|
|
assert not harness.tap_exists(t1), "anchors must not exist before start"
|
|
|
|
print(".. starting routers and waiting for IOS boots")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r2_id}/start")
|
|
c1 = _boot(server, pid, r1_id)
|
|
c2 = _boot(server, pid, r2_id)
|
|
|
|
# NetworkManager can release an NM-managed TAP from its bridge after
|
|
# (re-)enslavement (docs/bugs/networkmanager-tap-release.md); take the
|
|
# anchors out of its reach for the rest of the run.
|
|
harness.unmanage_from_networkmanager(t1, t2)
|
|
|
|
# Kernel objects: anchors exist and the per-link bridge has exactly
|
|
# the two of them enslaved.
|
|
assert harness.tap_exists(t1) and harness.tap_exists(t2), (t1, t2)
|
|
assert harness.bridge_members(bridge) == sorted([t1, t2]), harness.bridge_members(bridge)
|
|
|
|
# L2-anchor spec §E.1: anchors and the per-link bridge carry no L3
|
|
# identity (skipped on a uBridge without `link l2only` — the spec's
|
|
# degradation), and both ports are FORWARDING — the silent-failure
|
|
# guard against a bridge device left DOWN.
|
|
if harness.l2only_supported():
|
|
harness.assert_pure_l2(t1)
|
|
harness.assert_pure_l2(t2)
|
|
harness.assert_pure_l2(bridge)
|
|
else:
|
|
print(".. uBridge without link l2only: skipping the §E.1 assertions")
|
|
harness.assert_forwarding(t1)
|
|
harness.assert_forwarding(t2)
|
|
|
|
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_ipv6=R1_IP6)
|
|
harness.configure_ios(c2, "R2", eth_ip="10.1.1.2", eth_ipv6=R2_IP6)
|
|
out = c1.run("show ip int brief")
|
|
assert re.search(r"FastEthernet1/0\s+10\.1\.1\.1\s+\S+\s+\S+\s+up\s+up", out), out
|
|
out = c1.run("show ipv6 interface brief")
|
|
# IOS prints v6 addresses upper-case (and on their own line): match
|
|
# the literal case-insensitively
|
|
assert R1_IP6.upper() in out.upper(), out
|
|
print(".. both routers configured, pinging over the kernel link")
|
|
baseline = harness.wait_ping(c1, "10.1.1.2")
|
|
assert baseline["success"] == 100, baseline["raw"]
|
|
baseline6 = harness.wait_ping(c1, R2_IP6)
|
|
assert baseline6["success"] == 100, baseline6["raw"]
|
|
|
|
# L2-anchor spec §E.2: shut the guest interfaces (silencing IOS's own
|
|
# CDP/keepalives — the only legitimate speakers on the segment) and
|
|
# demand the host side stays silent for 5 s: without the hardening
|
|
# the host's own MLD/DAD noise floods the link (6 frames / 2 s).
|
|
if harness.l2only_supported():
|
|
for console in (c1, c2):
|
|
_eth_shutdown(console, shut=True)
|
|
harness.assert_idle_silence(t1, t2, bridge)
|
|
for console in (c1, c2):
|
|
_eth_shutdown(console, shut=False)
|
|
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6, attempts=5)["success"] == 100
|
|
|
|
# delay 100: netem on both anchors (each impairs one direction),
|
|
# one-way ~100 ms => RTT grows by ~200 ms — netem is family-blind,
|
|
# both families ride the same qdisc
|
|
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}})
|
|
assert "netem" in harness.qdiscs(t1) and "netem" in harness.qdiscs(t2), (
|
|
harness.qdiscs(t1),
|
|
harness.qdiscs(t2),
|
|
)
|
|
delayed = harness.wait_ping(c1, "10.1.1.2")
|
|
assert delayed["success"] == 100, delayed["raw"]
|
|
delayed6 = harness.wait_ping(c1, R2_IP6)
|
|
assert delayed6["success"] == 100, delayed6["raw"]
|
|
print(f".. delay 100: v4 {baseline['avg']}->{delayed['avg']} ms, v6 {baseline6['avg']}->{delayed6['avg']} ms")
|
|
assert delayed["avg"] >= 150, (baseline, delayed)
|
|
# The v6 twin is asserted from the capture's own timestamps, not from
|
|
# the guest's report: the dynamips-emulated classic IOS under-reports
|
|
# its ping ipv6 RTT by ~40 % of a delayed round trip (reproducible:
|
|
# report ~125-135 ms while a pcap on the anchor shows the v6 echo
|
|
# request->reply taking the same ~230 ms as v4 — the wire is correct,
|
|
# the reporter is not; the pings' order does not matter).
|
|
capture = compute.call("POST", f"/projects/{pid}/links/{lid}/capture/start", {"data_link_type": "DLT_EN10MB"})
|
|
harness.ping(c1, R2_IP6)
|
|
time.sleep(1)
|
|
compute.call("POST", f"/projects/{pid}/links/{lid}/capture/stop")
|
|
wire6 = harness.pcap_icmp_rtt(capture["capture_file_path"], "86dd")
|
|
print(f".. delay 100 (capture): v6 wire round trip {wire6:.0f} ms")
|
|
assert wire6 is not None and wire6 >= 60, wire6
|
|
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
|
|
assert "netem" not in harness.qdiscs(t1), harness.qdiscs(t1)
|
|
fast = harness.wait_ping(c1, "10.1.1.2")
|
|
assert fast["success"] == 100 and fast["avg"] < harness.FAST_RTT_MS, fast
|
|
fast6 = harness.wait_ping(c1, R2_IP6)
|
|
assert fast6["success"] == 100 and fast6["avg"] < harness.FAST_RTT_MS, fast6
|
|
|
|
# suspend: anchor admin-down, traffic dead; resume restores it
|
|
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"suspend": True})
|
|
assert not harness.tap_up(t1)
|
|
dead = harness.ping(c1, "10.1.1.2", repeat=3)
|
|
assert dead["success"] == 0, dead["raw"]
|
|
dead6 = harness.ping(c1, R2_IP6, repeat=3)
|
|
assert dead6["success"] == 0, dead6["raw"]
|
|
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"suspend": False})
|
|
assert harness.tap_up(t1)
|
|
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6)["success"] == 100
|
|
|
|
# capture: AF_PACKET on the anchor writes a real pcap — both
|
|
# families inside the window, both ethertypes on the wire
|
|
capture = compute.call("POST", f"/projects/{pid}/links/{lid}/capture/start", {"data_link_type": "DLT_EN10MB"})
|
|
harness.ping(c1, "10.1.1.2")
|
|
harness.ping(c1, R2_IP6)
|
|
time.sleep(1)
|
|
compute.call("POST", f"/projects/{pid}/links/{lid}/capture/stop")
|
|
path = capture["capture_file_path"]
|
|
count, ethertypes = harness.pcap_records(path)
|
|
assert count >= 8, f"{path}: {count} records"
|
|
assert "0800" in ethertypes, (path, ethertypes)
|
|
assert "86dd" in ethertypes, (path, ethertypes)
|
|
|
|
# serial link between the same routers: per-port exclusion keeps it
|
|
# on the relay, where the real traffic still crosses
|
|
serial = compute.create_link(pid, (r1_id, *SERIAL), (r2_id, *SERIAL))
|
|
assert serial["kernel_datapath"] is False, serial
|
|
harness.configure_ios(c1, "R1", serial_ip="10.2.2.1", clock=True)
|
|
harness.configure_ios(c2, "R2", serial_ip="10.2.2.2")
|
|
relay_ping = harness.wait_ping(c1, "10.2.2.2")
|
|
assert relay_ping["success"] == 100, relay_ping["raw"]
|
|
compute.call("DELETE", f"/projects/{pid}/links/{serial['link_id']}")
|
|
|
|
# delete / re-create the kernel link: anchors survive, the bridge
|
|
# goes and comes back
|
|
compute.call("DELETE", f"/projects/{pid}/links/{lid}")
|
|
assert harness.bridge_members(bridge) is None
|
|
assert harness.tap_exists(t1) and harness.tap_exists(t2)
|
|
link = compute.create_link(pid, (r1_id, *ETH), (r2_id, *ETH))
|
|
lid = link["link_id"]
|
|
bridge = harness.link_bridge_name(lid)
|
|
assert link["kernel_datapath"] is True, link
|
|
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6, attempts=5)["success"] == 100
|
|
|
|
# stop / start keeps the whole wiring (the Dynamips hypervisor
|
|
# outlives ``vm stop``); the restarted router just reboots and is
|
|
# reconfigured
|
|
print(".. stopping and restarting R1")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/stop")
|
|
assert harness.tap_exists(t1)
|
|
assert harness.bridge_members(bridge) == sorted([t1, t2])
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
|
|
c1.close()
|
|
c1 = _boot(server, pid, r1_id)
|
|
assert harness.bridge_members(bridge) == sorted([t1, t2])
|
|
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_ipv6=R1_IP6)
|
|
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
|
|
assert harness.wait_ping(c1, R2_IP6, attempts=5)["success"] == 100
|
|
|
|
c1.close()
|
|
c2.close()
|
|
except BaseException:
|
|
harness.release(server, pid, failed=True)
|
|
raise
|
|
harness.release(server, pid, failed=False)
|
|
|
|
# deleting the project closes the nodes: anchors and bridges go with them
|
|
assert harness.wait_until(lambda: not harness.tap_exists(t1) and not harness.tap_exists(t2), timeout=15), (
|
|
t1,
|
|
t2,
|
|
)
|
|
assert harness.wait_until(lambda: harness.bridge_members(bridge) is None, timeout=10), bridge
|
|
|
|
|
|
def test_dynamips_relay_control():
|
|
"""Negative control: the same topology with ``enable_kernel_datapath =
|
|
false`` (an isolated instance) wires nothing into the kernel — no
|
|
per-link bridge, no enslavement — and still pings over the relay.
|
|
|
|
Anchors DO exist here (a Dynamips router creates its port TAPs at start
|
|
whenever the uBridge tap module is available — they are the adapter's
|
|
interfaces, exactly like QEMU's); the server configuration only decides
|
|
whether links are attached to them."""
|
|
server = harness.live_server(kernel=False)
|
|
compute = server.compute
|
|
image = _pick_image(server)
|
|
idlepc = harness.dynamips_idlepc(server, image)
|
|
|
|
project = compute.create_project("dyn-e2e-relay")
|
|
pid = project["project_id"]
|
|
t1 = None
|
|
try:
|
|
r1 = compute.create_dynamips_router(pid, "E2E-R1", image, SLOTS, idlepc=idlepc)
|
|
r2 = compute.create_dynamips_router(pid, "E2E-R2", image, SLOTS, idlepc=idlepc)
|
|
r1_id, r2_id = r1["node_id"], r2["node_id"]
|
|
t1 = harness.anchor_name(r1_id, *ETH)
|
|
|
|
link = compute.create_link(pid, (r1_id, *ETH), (r2_id, *ETH))
|
|
lid = link["link_id"]
|
|
assert link["kernel_datapath"] is False, link
|
|
|
|
print(".. starting routers and waiting for IOS boots (relay)")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
|
|
compute.call("POST", f"/projects/{pid}/nodes/{r2_id}/start")
|
|
c1 = _boot(server, pid, r1_id)
|
|
c2 = _boot(server, pid, r2_id)
|
|
|
|
# no per-link kernel bridge on the relay: nothing is enslaved, the
|
|
# anchor (if present) is just an idle port interface
|
|
assert harness.bridge_members(harness.link_bridge_name(lid)) is None
|
|
if harness.tap_exists(t1):
|
|
assert not harness.tap_up(t1), "an unattached relay anchor should sit down"
|
|
if harness.l2only_supported():
|
|
# the L2 hardening is a creation-time property, not a
|
|
# datapath one: relay anchors are pure L2 too
|
|
harness.assert_pure_l2(t1)
|
|
|
|
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1", eth_ipv6=R1_IP6)
|
|
harness.configure_ios(c2, "R2", eth_ip="10.1.1.2", eth_ipv6=R2_IP6)
|
|
relay_ping = harness.wait_ping(c1, "10.1.1.2")
|
|
assert relay_ping["success"] == 100, relay_ping["raw"]
|
|
relay_ping6 = harness.wait_ping(c1, R2_IP6)
|
|
assert relay_ping6["success"] == 100, relay_ping6["raw"]
|
|
|
|
c1.close()
|
|
c2.close()
|
|
except BaseException:
|
|
harness.release(server, pid, failed=True)
|
|
raise
|
|
harness.release(server, pid, failed=False)
|
|
|
|
assert harness.wait_until(lambda: not harness.tap_exists(t1), timeout=15), t1
|