From 17403afeb453f2cc89cdb6ad7e017c1c90fbbc17 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 4 Nov 2024 14:36:35 +0100 Subject: [PATCH 01/10] test: pcap: Avoid creating dumpcap zombies Story time: In the days of yore, we generated PCAPs using tcpdump, which will flush any buffered packets to its output file on reception of SIGUSR2. Therefore we sent a USR2 before terminating the capture. At some point, we switched over to use tshark to capture packets for us, but the SIGUSR2 sayed in place. It turns out, tshark will just die on USR2, abandoning the dumpcap child it has created, which means the enclosing network namespace can't be properly torn down. Therefore: Remove skip sending USR2 and just send TERM directly. --- test/infamy/netns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index 52e9590e..caf327f0 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -253,7 +253,6 @@ class Pcap: # terminating the capture. time.sleep(sleep) - self.proc.send_signal(signal.SIGUSR2) self.proc.terminate() try: _, stderr = self.proc.communicate(5) From f9e430e41dc89736778bcb17165cf5c0763e8926 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 1 Nov 2024 13:46:29 +0100 Subject: [PATCH 02/10] test: netns: Generalize IsolatedMacVlan to support multiple interfaces This is needed to create namespaces with MACVLANs from different parent interfaces. The first consumer will be a Two-Port Mac Relay (TPMR), so that we can control the flow of packets between two nodes. --- test/infamy/netns.py | 110 +++++++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index caf327f0..c5827428 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -18,31 +18,53 @@ def setns(fd, nstype): __NR_setns = 308 __libc.syscall(__NR_setns, fd, nstype) -class IsolatedMacVlan: - """Create an isolated interface on top of a PC interface.""" - def __init__(self, parent, ifname="iface", lo=True): +class IsolatedMacVlans: + """A network namespace containing a multiple MACVLANs + + Stacks a MACVLAN on top of each specificed controller interface, + and moves those interfaces to a separate namespace, isolating it + from all others. + + NOTE: For the simple case when only one interface needs to be + mapped, see IsolatedMacVlan below. + + Example: + + netns = IsolatedMacVlans({ "eth2": "a", "eth3": "b" }) + + netns: + .--------. + | a b | (MACVLANs) + '-+----+-' + | | + eth0 eth1 eth2 eth3 + + """ + + def __init__(self, ifmap, lo=True): self.sleeper = None - self.parent, self.ifname, self.lo = parent, ifname, lo + self.ifmap, self.lo = ifmap, lo self.ping_timeout = env.ENV.attr("ping_timeout", 5) - def __enter__(self): + def start(self): self.sleeper = subprocess.Popen(["unshare", "-r", "-n", "sh", "-c", "echo && exec sleep infinity"], stdout=subprocess.PIPE) self.sleeper.stdout.readline() try: - subprocess.run(["ip", "link", "add", - "dev", self.ifname, - "link", self.parent, - "address", self._stable_mac(), - "netns", str(self.sleeper.pid), - "type", "macvlan"], check=True) - self.runsh(f""" - while ! ip link show dev {self.ifname}; do - sleep 0.1 - done - """) + for parent, ifname in self.ifmap.items(): + subprocess.run(["ip", "link", "add", + "dev", ifname, + "link", parent, + "address", self._stable_mac(parent), + "netns", str(self.sleeper.pid), + "type", "macvlan"], check=True) + self.runsh(f""" + while ! ip link show dev {ifname}; do + sleep 0.1 + done + """) except Exception as e: self.__exit__(None, None, None) raise e @@ -56,12 +78,18 @@ class IsolatedMacVlan: return self - def __exit__(self, val, typ, tb): + def stop(self): self.sleeper.kill() self.sleeper.wait() time.sleep(0.5) - def _stable_mac(self): + def __enter__(self): + return self.start() + + def __exit__(self, val, typ, tb): + return self.stop() + + def _stable_mac(self, parent): """Generate address for MACVLAN By default, the kernel will assign a random address. This @@ -75,7 +103,7 @@ class IsolatedMacVlan: the resource exhaustion issue. """ - random.seed(self.parent) + random.seed(parent) a = list(random.randbytes(6)) a[0] |= 0x02 a[0] &= ~0x01 @@ -146,13 +174,13 @@ class IsolatedMacVlan: ip -{p} route add {subnet}{prefix_length} via {nexthop} """, check=True) - def addip(self, addr, prefix_length=24, proto="ipv4"): + def addip(self, ifname, addr, prefix_length=24, proto="ipv4"): p=proto[3] self.runsh(f""" set -ex ip link set iface up - ip -{p} addr add {addr}/{prefix_length} dev iface + ip -{p} addr add {addr}/{prefix_length} dev {ifname} """, check=True) @@ -188,8 +216,7 @@ class IsolatedMacVlan: raise Exception(res) - def must_receive(self, expr, timeout=None, ifname=None, must=True): - ifname = ifname if ifname else self.ifname + def must_receive(self, expr, ifname, timeout=None, must=True): timeout = timeout if timeout else self.ping_timeout tshark = self.run(["tshark", "-nl", f"-i{ifname}", @@ -207,10 +234,43 @@ class IsolatedMacVlan: def must_not_receive(self, *args, **kwargs): self.must_receive(*args, **kwargs, must=False) - def pcap(self, expr, ifname=None): - ifname = ifname if ifname else self.ifname + def pcap(self, expr, ifname): return Pcap(self, ifname, expr) +class IsolatedMacVlan(IsolatedMacVlans): + """A network namespace containing a single MACVLAN + + Stacks a MACVLAN on top of an interface on the controller, and + moves that interface to a separate namespace, isolating it from + all other interfaces. + + Example: + + netns = IsolatedMacVlan("eth3") + + netns: + .-------. + | iface | (MACVLAN) + '---+---' + | + eth0 eth1 eth2 eth3 + + """ + def __init__(self, parent, ifname="iface", lo=True): + self._ifname = ifname + return super().__init__(ifmap={ parent: ifname }, lo=lo) + + def addip(self, addr, prefix_length=24, proto="ipv4"): + return super().addip(ifname=self._ifname, addr=addr, prefix_length=prefix_length, proto=proto) + + def must_receive(self, expr, timeout=None, ifname=None, must=True): + ifname = ifname if ifname else self._ifname + return super().must_receive(expr=expr, ifname=ifname, timeout=timeout, must=must) + + def pcap(self, expr, ifname=None): + ifname = ifname if ifname else self._ifname + return super().pcap(expr=expr, ifname=ifname) + class Pcap: def __init__(self, netns, ifname, expr): self.netns, self.ifname, self.expr = netns, ifname, expr From 3031bba0475fb6c0d9f9a9a35f2a16463a2dda30 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 1 Nov 2024 16:40:41 +0100 Subject: [PATCH 03/10] test: netns: Use passthru mode for MACVLANs This mode let's us receive all packets from the parent device, which is automatically set in promiscuous mode, and let's the MACVLAN transmit packets with any SA, which we need to implement a TPMR (or a bridge or anything else we might dream up) --- test/infamy/netns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index c5827428..54e9e7c9 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -59,7 +59,7 @@ class IsolatedMacVlans: "link", parent, "address", self._stable_mac(parent), "netns", str(self.sleeper.pid), - "type", "macvlan"], check=True) + "type", "macvlan", "mode", "passthru"], check=True) self.runsh(f""" while ! ip link show dev {ifname}; do sleep 0.1 From 2a6e4402f80151916100c15956b93a8275d2e3ed Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 1 Nov 2024 13:48:50 +0100 Subject: [PATCH 04/10] test: netns: Clean up lingering namespaces on test exit This gets rid of annoying "Address already in use" errors when running tests repeatedly from a test-sh. --- test/infamy/netns.py | 7 +++++++ test/infamy/tap.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index 54e9e7c9..dfffe114 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -41,6 +41,11 @@ class IsolatedMacVlans: """ + Instances = [] + def Cleanup(): + for ns in list(IsolatedMacVlans.Instances): + ns.stop() + def __init__(self, ifmap, lo=True): self.sleeper = None self.ifmap, self.lo = ifmap, lo @@ -76,11 +81,13 @@ class IsolatedMacVlans: self.__exit__(None, None, None) raise e + self.Instances.append(self) return self def stop(self): self.sleeper.kill() self.sleeper.wait() + self.Instances.remove(self) time.sleep(0.5) def __enter__(self): diff --git a/test/infamy/tap.py b/test/infamy/tap.py index dc2e462e..42a2ed61 100644 --- a/test/infamy/tap.py +++ b/test/infamy/tap.py @@ -4,6 +4,8 @@ import subprocess import sys import traceback +import infamy.netns + class Test: def __init__(self, output=sys.stdout): self.out = output @@ -24,6 +26,8 @@ class Test: self.out.write(f"# Exiting ({now})\n") self.out.flush() + infamy.netns.IsolatedMacVlans.Cleanup() + if not e: self._not_ok("Missing explicit test result\n") else: From a4a4a49eaeb007b159e5bdfb8c9fcb664db52e03 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 4 Nov 2024 10:31:57 +0100 Subject: [PATCH 05/10] test: netns: Guard against "double free" when failing to setup netns When used as a context manager, __exit__ will be called even though __enter__ might throw an exception half way through. So there is no guarantee that the instance is on the cleanup list when we get to stop(). --- test/infamy/netns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index dfffe114..e7de7d87 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -87,7 +87,8 @@ class IsolatedMacVlans: def stop(self): self.sleeper.kill() self.sleeper.wait() - self.Instances.remove(self) + if self in self.Instances: + self.Instances.remove(self) time.sleep(0.5) def __enter__(self): From be828d46c951bee50ada22273abcf41a52798f42 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 4 Nov 2024 12:28:26 +0100 Subject: [PATCH 06/10] test: netns: Poll for MACVLAN removal completion Now that we're using the `passthru` mode of MACVLANs, it seems we can rely on the parent interface's promiscuity going down to 0 as an indication that the kernel has completed the removal of the interface, and will accept the creation of a new one. --- test/infamy/netns.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index e7de7d87..a6e97756 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -1,4 +1,5 @@ import ctypes +import json import multiprocessing import os import random @@ -87,9 +88,29 @@ class IsolatedMacVlans: def stop(self): self.sleeper.kill() self.sleeper.wait() + + for n in range(100): + promisc = False + for parent in self.ifmap.keys(): + iplink = subprocess.run(f"ip -d -j link show dev {parent}".split(), + stdout=subprocess.PIPE, check=True) + link = json.loads(iplink.stdout)[0] + if link["promiscuity"]: + # Use promisc as a substitute for an indicator + # of whether the kernel has actually removed + # the passthru MACVLAN yet or not + promisc = True + break + + if not promisc: + break + + time.sleep(.1) + else: + raise TimeoutError("Lingering MACVLAN") + if self in self.Instances: self.Instances.remove(self) - time.sleep(0.5) def __enter__(self): return self.start() From dc92db4e9faafa8a7488452cacd1dcf7e51f830a Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 1 Nov 2024 13:50:26 +0100 Subject: [PATCH 07/10] test: netns: Add Two-Port MAC Relay (TPMR) This is a specialized namespace, containing two ports, which (hopefully) acts as completely transparent "bump-on-the-wire" bridge. Useful in scenarios where you want to test what happens when the flow of packets between two nodes is disrupted. E.g., for testing fail-over behavior in dynamic routing protocols, L2 redundancy, VRRP etc. --- test/infamy/netns.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index a6e97756..59a1f6af 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -360,3 +360,39 @@ class Pcap: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=True) return tcpdump.stdout + +class TPMR(IsolatedMacVlans): + def __init__(self, a, b): + super().__init__(ifmap={ a: "a", b: "b" }, lo=False) + + def start(self, forward=True): + ret = super().start() + + for dev in ("a", "b"): + self.run(f"ip link set dev {dev} promisc on up".split()) + self.run(f"tc qdisc add dev {dev} clsact".split()) + + if forward: + self.forward() + + return ret + + def _clear_ingress(self, iface): + return self.run(f"tc filter del dev {iface} ingress".split()) + + def _add_redir(self, frm, to): + cmd = \ + "tc filter add dev".split() \ + + [frm] \ + + "ingress matchall action mirred egress redirect dev".split() \ + + [to] + return self.run(cmd) + + def forward(self): + for iface in ("a", "b"): + self._clear_ingress(iface) + self._add_redir(iface, "a" if iface == "b" else "b") + + def block(self): + for iface in ("a", "b"): + self._clear_ingress(iface) From 7f2f3768db739a0c659c59c78d254b12a80ceb40 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Fri, 1 Nov 2024 17:10:01 +0100 Subject: [PATCH 08/10] test: ospf_bfd: Verify fast fail-over using BFD --- test/case/ietf_routing/Readme.adoc | 2 + test/case/ietf_routing/ietf_routing.yaml | 3 + test/case/ietf_routing/ospf_bfd/Readme.adoc | 35 ++++ test/case/ietf_routing/ospf_bfd/test.py | 183 +++++++++++++++++++ test/case/ietf_routing/ospf_bfd/topology.dot | 39 ++++ test/case/ietf_routing/ospf_bfd/topology.svg | 95 ++++++++++ 6 files changed, 357 insertions(+) create mode 100644 test/case/ietf_routing/ospf_bfd/Readme.adoc create mode 100755 test/case/ietf_routing/ospf_bfd/test.py create mode 100644 test/case/ietf_routing/ospf_bfd/topology.dot create mode 100644 test/case/ietf_routing/ospf_bfd/topology.svg diff --git a/test/case/ietf_routing/Readme.adoc b/test/case/ietf_routing/Readme.adoc index 539fcdaf..99234e34 100644 --- a/test/case/ietf_routing/Readme.adoc +++ b/test/case/ietf_routing/Readme.adoc @@ -11,3 +11,5 @@ include::ospf_basic/Readme.adoc[] include::ospf_unnumbered_interface/Readme.adoc[] include::ospf_multiarea/Readme.adoc[] + +include::ospf_bfd/Readme.adoc[] diff --git a/test/case/ietf_routing/ietf_routing.yaml b/test/case/ietf_routing/ietf_routing.yaml index 633f3a11..45228205 100644 --- a/test/case/ietf_routing/ietf_routing.yaml +++ b/test/case/ietf_routing/ietf_routing.yaml @@ -10,3 +10,6 @@ - name: ospf_multiarea case: ospf_multiarea/test.py + +- name: ospf_bfd + case: ospf_bfd/test.py diff --git a/test/case/ietf_routing/ospf_bfd/Readme.adoc b/test/case/ietf_routing/ospf_bfd/Readme.adoc new file mode 100644 index 00000000..ae2b34d5 --- /dev/null +++ b/test/case/ietf_routing/ospf_bfd/Readme.adoc @@ -0,0 +1,35 @@ +=== OSPF BFD +==== Description +Verify that a router running OSPF, with Bidirectional Forwarding +Detection (BFD) enabled, will detect link faults even when the +physical layer is still operational. + +This can typically happen when one logical link, from OSPF's +perspective, is made up of multiple physical links containing media +converters without link fault forwarding. + +==== Topology +ifdef::topdoc[] +image::../../test/case/ietf_routing/ospf_bfd/topology.svg[OSPF BFD topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::ospf_bfd/topology.svg[OSPF BFD topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.svg[OSPF BFD topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUTs +. Setup TPMR between R1fast and R2fast +. Configure R1 and R2 +. Setup IP addresses and default routes on h1 and h2 +. Wait for R1 and R2 to peer +. Verify connectivity from PC:src to PC:dst via fast link +. Disable forwarding between R1fast and R2fast to trigger fail-over +. Verify connectivity from PC:src to PC:dst via slow link + + +<<< + diff --git a/test/case/ietf_routing/ospf_bfd/test.py b/test/case/ietf_routing/ospf_bfd/test.py new file mode 100755 index 00000000..31740f4c --- /dev/null +++ b/test/case/ietf_routing/ospf_bfd/test.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + +""" +OSPF BFD + +Verify that a router running OSPF, with Bidirectional Forwarding +Detection (BFD) enabled, will detect link faults even when the +physical layer is still operational. + +This can typically happen when one logical link, from OSPF's +perspective, is made up of multiple physical links containing media +converters without link fault forwarding. +""" + +import time + +import infamy +import infamy.route as route +from infamy.netns import TPMR +from infamy.util import until, parallel + +def config(target, params): + name = params["name"] + dif, fif, sif = \ + params["link"]["data"], \ + params["link"]["fast"], \ + params["link"]["slow"] + rid, daddr, faddr, saddr = \ + params["addr"]["rid"], \ + params["addr"]["data"], \ + params["addr"]["fast"], \ + params["addr"]["slow"] + + def ifconfig(name, addr, plen): + return { + "name": name, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": addr, + "prefix-length": plen, + }]} + } + + target.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [ + ifconfig("lo", rid, 32), + + ifconfig(dif, daddr, 24), + ifconfig(fif, faddr, 30), + ifconfig(sif, saddr, 30), + ] + } + }) + + target.put_config_dict("ietf-system", { + "system": { + "hostname": name, + } + }) + + target.put_config_dict("ietf-routing", { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ospfv2", + "name": "default", + "ospf": { + "areas": { + "area": [{ + "area-id": "0.0.0.0", + "interfaces": + { + "interface": [{ + "bfd": { + "enabled": True + }, + "name": fif, + "hello-interval": 1, + "dead-interval": 10, + "cost": 100, + }, + { + "bfd": { + "enabled": True + }, + "name": sif, + "hello-interval": 1, + "dead-interval": 10, + "cost": 200, + }, { + "name": dif, + "passive": True, + }] + }, + }] + } + } + }] + } + } + }) + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUTs"): + env = infamy.Env() + R1 = env.attach("R1", "mgmt") + R2 = env.attach("R2", "mgmt") + + with test.step("Setup TPMR between R1fast and R2fast"): + breaker = TPMR(env.ltop.xlate("PC", "R1fast")[1], + env.ltop.xlate("PC", "R2fast")[1]).start() + + with test.step("Configure R1 and R2"): + r1cfg = { + "name": "R1", + "addr": { + "rid": "192.168.1.1", + + "data": "192.168.10.1", + "fast": "192.168.100.1", + "slow": "192.168.200.1", + }, + "link": { + "data": env.ltop.xlate("R1", "h1")[1], + "fast": env.ltop.xlate("R1", "fast")[1], + "slow": env.ltop.xlate("R1", "slow")[1], + } + } + r2cfg = { + "name": "R2", + "addr": { + "rid": "192.168.1.2", + + "data": "192.168.20.1", + "fast": "192.168.100.2", + "slow": "192.168.200.2", + }, + "link": { + "data": env.ltop.xlate("R2", "h2")[1], + "fast": env.ltop.xlate("R2", "fast")[1], + "slow": env.ltop.xlate("R2", "slow")[1], + } + } + + parallel(config(R1, r1cfg), config(R2, r2cfg)) + + with test.step("Setup IP addresses and default routes on h1 and h2"): + _, h1 = env.ltop.xlate("PC", "h1") + _, h2 = env.ltop.xlate("PC", "h2") + + h1net = infamy.IsolatedMacVlan(h1).start() + h1net.addip("192.168.10.2") + h1net.addroute("default", "192.168.10.1") + + h2net = infamy.IsolatedMacVlan(h2).start() + h2net.addip("192.168.20.2") + h2net.addroute("default", "192.168.20.1") + + with test.step("Wait for R1 and R2 to peer"): + print("Waiting for R1 and R2 to peer") + until(lambda: route.ipv4_route_exist(R1, "192.168.20.0/24", proto="ietf-ospf:ospfv2"), attempts=200) + until(lambda: route.ipv4_route_exist(R2, "192.168.10.0/24", proto="ietf-ospf:ospfv2"), attempts=200) + + with test.step("Verify connectivity from PC:src to PC:dst via fast link"): + h1net.must_reach("192.168.20.2") + hops = [row[1] for row in h1net.traceroute("192.168.20.2")] + assert "192.168.100.2" in hops, f"Path to h2 ({repr(hops)}), does not use fast link" + + with test.step("Disable forwarding between R1fast and R2fast to trigger fail-over"): + breaker.block() + print("Give BFD some time to detect the bad link, " + + "but not enough for the OSPF dead interval expire") + time.sleep(1) + + with test.step("Verify connectivity from PC:src to PC:dst via slow link"): + h1net.must_reach("192.168.20.2") + hops = [row[1] for row in h1net.traceroute("192.168.20.2")] + assert "192.168.200.2" in hops, f"Path to h2 ({repr(hops)}), does not use slow link" + + test.succeed() diff --git a/test/case/ietf_routing/ospf_bfd/topology.dot b/test/case/ietf_routing/ospf_bfd/topology.dot new file mode 100644 index 00000000..f6f59985 --- /dev/null +++ b/test/case/ietf_routing/ospf_bfd/topology.dot @@ -0,0 +1,39 @@ +graph "ospf-bfd" { + layout="neato"; + overlap="false"; + esep="+20"; + size=10 + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + R1 [ + label=" { { R1 | slow } | { mgmt |

h1 | fast } }", + pos="0,6!", + + kind="infix", + ]; + R2 [ + label="{ { slow | R2 } | { fast |

h2 | mgmt } }", + pos="18,6!", + + kind="infix", + ]; + + PC [ + label="{ { R1mgmt |

h1 | R1fast | R2fast |

h2 | R2mgmt } | PC }", + pos="9,0!", + kind="controller", + ]; + + PC:R1mgmt -- R1:mgmt [kind=mgmt, color="lightgray"] + PC:R2mgmt -- R2:mgmt [kind=mgmt, color="lightgray"] + + PC:h1 -- R1:h1 + PC:h2 -- R2:h2 + + R1:fast -- PC:R1fast [color="lightgreen", taillabel="Cost: 100"] + R2:fast -- PC:R2fast [color="lightgreen"] + + R1:slow -- R2:slow [color="crimson", taillabel="Cost: 200"] +} diff --git a/test/case/ietf_routing/ospf_bfd/topology.svg b/test/case/ietf_routing/ospf_bfd/topology.svg new file mode 100644 index 00000000..ca1ec0a6 --- /dev/null +++ b/test/case/ietf_routing/ospf_bfd/topology.svg @@ -0,0 +1,95 @@ + + + + + + +ospf-bfd + + + +R1 + +R1 + +slow + +mgmt + +h1 + +fast + + + +R2 + +slow + +R2 + +fast + +h2 + +mgmt + + + +R1:slow--R2:slow + +Cost: 200 + + + +PC + +R1mgmt + +h1 + +R1fast + +R2fast + +h2 + +R2mgmt + +PC + + + +R1:fast--PC:R1fast + +Cost: 100 + + + +R2:fast--PC:R2fast + + + + +PC:R1mgmt--R1:mgmt + + + + +PC:h1--R1:h1 + + + + +PC:R2mgmt--R2:mgmt + + + + +PC:h2--R2:h2 + + + + From 96d200fe70f7559de72ad3c600d9922c442b452c Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 4 Nov 2024 10:34:25 +0100 Subject: [PATCH 09/10] test: vlan_ping: Improve test step description and structure Initially done as a workaround for the MACVLAN create/remove race. But even though the old version of this test now works, this refactor provides better test step descriptions, making it easier to follow for anyone looking to manually verify the functionality. --- .../ietf_interfaces/vlan_ping/Readme.adoc | 8 ++-- test/case/ietf_interfaces/vlan_ping/test.py | 40 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/test/case/ietf_interfaces/vlan_ping/Readme.adoc b/test/case/ietf_interfaces/vlan_ping/Readme.adoc index 75cdde9d..b976123c 100644 --- a/test/case/ietf_interfaces/vlan_ping/Readme.adoc +++ b/test/case/ietf_interfaces/vlan_ping/Readme.adoc @@ -16,10 +16,12 @@ endif::testgroup[] endif::topdoc[] ==== Test sequence . Set up topology and attach to target DUT +. Set up VLAN interface on host:data with IP 10.0.0.1 . Configure VLAN 10 interface on target:data with IP 10.0.0.2 -. Waiting for links to come up -. Ping 10.0.0.2 from VLAN 10 on host:data with IP 10.0.0.1 -. Remove VLAN interface from target:data, and test again (should not be able to ping) +. Wait for links to come up +. Verify that host:data can reach 10.0.0.2 +. Remove VLAN interface from target:data +. Verify that host:data can no longer reach 10.0.0.2 <<< diff --git a/test/case/ietf_interfaces/vlan_ping/test.py b/test/case/ietf_interfaces/vlan_ping/test.py index 8639f332..db2060b2 100755 --- a/test/case/ietf_interfaces/vlan_ping/test.py +++ b/test/case/ietf_interfaces/vlan_ping/test.py @@ -10,31 +10,21 @@ import infamy.iface as iface from infamy import until -def test_ping(hport, should_pass): - with infamy.IsolatedMacVlan(hport) as ns: - try: - ns.runsh(""" - set -ex - ip link set iface up - ip link add dev vlan10 link iface up type vlan id 10 - ip addr add 10.0.0.1/24 dev vlan10 - """) - - if should_pass: - ns.must_reach("10.0.0.2") - else: - ns.must_not_reach("10.0.0.2") - - except Exception as e: - print(f"An error occurred during the VLAN setup or ping test: {e}") - raise - with infamy.Test() as test: with test.step("Set up topology and attach to target DUT"): env = infamy.Env() target = env.attach("target", "mgmt") _, tport = env.ltop.xlate("target", "data") + with test.step("Set up VLAN interface on host:data with IP 10.0.0.1"): + _, hport = env.ltop.xlate("host", "data") + datanet = infamy.IsolatedMacVlan(hport).start() + datanet.runsh(""" + set -ex + ip link set iface up + ip link add dev vlan10 link iface up type vlan id 10 + ip addr add 10.0.0.1/24 dev vlan10 + """) with test.step("Configure VLAN 10 interface on target:data with IP 10.0.0.2"): target.put_config_dict("ietf-interfaces", { @@ -64,16 +54,18 @@ with infamy.Test() as test: } }) - with test.step("Waiting for links to come up"): + with test.step("Wait for links to come up"): until(lambda: iface.get_param(target, tport, "oper-status") == "up") - with test.step("Ping 10.0.0.2 from VLAN 10 on host:data with IP 10.0.0.1"): + with test.step("Verify that host:data can reach 10.0.0.2"): _, hport = env.ltop.xlate("host", "data") - test_ping(hport,True) + datanet.must_reach("10.0.0.2") - with test.step("Remove VLAN interface from target:data, and test again (should not be able to ping)"): + with test.step("Remove VLAN interface from target:data"): target.delete_xpath(f"/ietf-interfaces:interfaces/interface[name='{tport}.10']") + + with test.step("Verify that host:data can no longer reach 10.0.0.2"): _, hport = env.ltop.xlate("host", "data") - test_ping(hport,False) + datanet.must_not_reach("10.0.0.2") test.succeed() From 56abfde8ec72b47f074a96ca0197485a69d7d909 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Tue, 5 Nov 2024 15:43:28 +0100 Subject: [PATCH 10/10] test: netns: Add missing class documentation to describe TPMRs [skip-ci] --- test/infamy/netns.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/infamy/netns.py b/test/infamy/netns.py index 59a1f6af..3d66ffe1 100644 --- a/test/infamy/netns.py +++ b/test/infamy/netns.py @@ -362,6 +362,19 @@ class Pcap: return tcpdump.stdout class TPMR(IsolatedMacVlans): + """Two-Port MAC Relay + + Creates a network namespace containing two controller interfaces + (`a` and `b`). By default, tc rules are setup to copy all frames + ingressing on `a` to egress on `b`, and vice versa. + + These rules can be removed and reinserted dynamically using the + `block()` and `forward()` methods, respectively. + + This is useful to verify the correctness of fail-over behavior in + various protocols. See ospf_bfd for a usage example. + """ + def __init__(self, a, b): super().__init__(ifmap={ a: "a", b: "b" }, lo=False)