From 0a491ffff534434de6f0edb83c1e4c5c6435d894 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 4 Dec 2025 19:10:14 +0100 Subject: [PATCH] test: add new tests for RIP Signed-off-by: Joachim Wiberg --- test/case/routing/Readme.adoc | 22 +- test/case/routing/all.yaml | 12 + test/case/routing/rip_basic/Readme.adoc | 1 + test/case/routing/rip_basic/test.adoc | 28 ++ test/case/routing/rip_basic/test.py | 215 ++++++++++++ test/case/routing/rip_basic/topology.dot | 34 ++ test/case/routing/rip_basic/topology.svg | 89 +++++ test/case/routing/rip_multihop/Readme.adoc | 1 + test/case/routing/rip_multihop/test.py | 314 ++++++++++++++++++ test/case/routing/rip_multihop/topology.dot | 41 +++ test/case/routing/rip_multihop/topology.svg | 118 +++++++ .../routing/rip_passive_interface/Readme.adoc | 1 + .../routing/rip_passive_interface/test.adoc | 29 ++ .../routing/rip_passive_interface/test.py | 156 +++++++++ .../rip_passive_interface/topology.dot | 33 ++ .../rip_passive_interface/topology.svg | 71 ++++ .../case/routing/rip_redistribute/Readme.adoc | 1 + test/case/routing/rip_redistribute/test.adoc | 37 +++ test/case/routing/rip_redistribute/test.py | 286 ++++++++++++++++ .../routing/rip_redistribute/topology.dot | 39 +++ .../routing/rip_redistribute/topology.svg | 93 ++++++ 21 files changed, 1620 insertions(+), 1 deletion(-) create mode 120000 test/case/routing/rip_basic/Readme.adoc create mode 100644 test/case/routing/rip_basic/test.adoc create mode 100755 test/case/routing/rip_basic/test.py create mode 100644 test/case/routing/rip_basic/topology.dot create mode 100644 test/case/routing/rip_basic/topology.svg create mode 120000 test/case/routing/rip_multihop/Readme.adoc create mode 100755 test/case/routing/rip_multihop/test.py create mode 100644 test/case/routing/rip_multihop/topology.dot create mode 100644 test/case/routing/rip_multihop/topology.svg create mode 120000 test/case/routing/rip_passive_interface/Readme.adoc create mode 100644 test/case/routing/rip_passive_interface/test.adoc create mode 100755 test/case/routing/rip_passive_interface/test.py create mode 100644 test/case/routing/rip_passive_interface/topology.dot create mode 100644 test/case/routing/rip_passive_interface/topology.svg create mode 120000 test/case/routing/rip_redistribute/Readme.adoc create mode 100644 test/case/routing/rip_redistribute/test.adoc create mode 100755 test/case/routing/rip_redistribute/test.py create mode 100644 test/case/routing/rip_redistribute/topology.dot create mode 100644 test/case/routing/rip_redistribute/topology.svg diff --git a/test/case/routing/Readme.adoc b/test/case/routing/Readme.adoc index 84302db6..68bfc2d7 100644 --- a/test/case/routing/Readme.adoc +++ b/test/case/routing/Readme.adoc @@ -7,9 +7,13 @@ Tests verifying standard routing protocols and configuration: - OSPF basic neighbor discovery and LSA exchange - OSPF unnumbered interface configuration - OSPF multi-area configuration and inter-area routing - - OSPF with BFD (Bidirectional Forwarding Detection) + - OSPF with BFD (Bidirectional Forwarding Detection) - OSPF default route advertisement and propagation - OSPF debug logging configuration and verification + - RIP basic neighbor discovery and route exchange + - RIP passive interface configuration + - RIP route redistribution (connected, static, and OSPF) + - RIP multi-hop routing with multiple neighbors - Route preference handling for OSPF routes - Route preference handling for DHCP-learned routes - Administrative distance configuration (route preference 255) @@ -42,6 +46,22 @@ include::ospf_debug/Readme.adoc[] <<< +include::rip_basic/Readme.adoc[] + +<<< + +include::rip_passive_interface/Readme.adoc[] + +<<< + +include::rip_redistribute/Readme.adoc[] + +<<< + +include::rip_multihop/Readme.adoc[] + +<<< + include::route_pref_ospf/Readme.adoc[] <<< diff --git a/test/case/routing/all.yaml b/test/case/routing/all.yaml index 437d3457..8086180c 100644 --- a/test/case/routing/all.yaml +++ b/test/case/routing/all.yaml @@ -28,3 +28,15 @@ - name: OSPF Debug Logging case: ospf_debug/test.py + +- name: RIP Basic + case: rip_basic/test.py + +- name: RIP Passive Interface + case: rip_passive_interface/test.py + +- name: RIP Redistribution + case: rip_redistribute/test.py + +- name: RIP Multi-hop + case: rip_multihop/test.py diff --git a/test/case/routing/rip_basic/Readme.adoc b/test/case/routing/rip_basic/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/routing/rip_basic/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/routing/rip_basic/test.adoc b/test/case/routing/rip_basic/test.adoc new file mode 100644 index 00000000..d9c39327 --- /dev/null +++ b/test/case/routing/rip_basic/test.adoc @@ -0,0 +1,28 @@ +=== RIP Basic + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/rip_basic] + +==== Description + +Verifies basic RIP functionality by configuring two routers (R1 and R2) +with RIP on their interconnecting link. The test ensures RIP routes are +exchanged between the routers and end-to-end connectivity is achieved. + +The test PC uses data1 interface to connect to R1's data port, and data2 +interface to connect to R2's data port (which does not have RIP enabled). +This verifies that RIP status information remains accessible when a router +has non-RIP interfaces. + +==== Topology + +image::topology.svg[RIP Basic topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure targets +. Wait for RIP routes to be exchanged +. Test connectivity from PC:data1 to R2 loopback via RIP +. Test connectivity from PC:data2 to R1 loopback via RIP + + diff --git a/test/case/routing/rip_basic/test.py b/test/case/routing/rip_basic/test.py new file mode 100755 index 00000000..e6976c19 --- /dev/null +++ b/test/case/routing/rip_basic/test.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""RIP Basic + +Verifies basic RIP functionality by configuring two routers (R1 and R2) +with RIP on their interconnecting link. The test ensures RIP routes are +exchanged between the routers and end-to-end connectivity is achieved. + +The test PC uses data1 interface to connect to R1's data port, and data2 +interface to connect to R2's data port (which does not have RIP enabled). +This verifies that RIP status information remains accessible when a router +has non-RIP interfaces. + +""" + +import infamy +import infamy.route as route +from infamy.util import until, parallel + + +def config_target1(target, data, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": data, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.10.1", + "prefix-length": 24 + }]} + }, { + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.1", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "192.168.100.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R1" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:static", + "name": "default", + "static-routes": { + "ipv4": { + "route": [{ + "destination-prefix": "192.168.33.1/32", + "next-hop": { + "special-next-hop": "blackhole" + } + }] + } + } + }, { + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "static" + }, { + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": link + }] + } + } + }] + } + } + } + }) + + +def config_target2(target, link, data): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.2", + "prefix-length": 24 + }] + } + }, { + "name": data, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.60.1", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "forwarding": True, + "ipv4": { + "address": [{ + "ip": "192.168.200.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R2" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": link + }] + } + } + }] + } + } + } + }) + + +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("Configure targets"): + _, R1data = env.ltop.xlate("R1", "data") + _, R2link = env.ltop.xlate("R2", "link") + _, R1link = env.ltop.xlate("R1", "link") + _, R2data = env.ltop.xlate("R2", "data") + + parallel(config_target1(R1, R1data, R1link), + config_target2(R2, R2link, R2data)) + + with test.step("Wait for RIP routes to be exchanged"): + print("Waiting for RIP routes to propagate...") + # R1 should learn R2's loopback + until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32", proto="ietf-rip:rip"), attempts=40) + # R2 should learn R1's loopback + until(lambda: route.ipv4_route_exist(R2, "192.168.100.1/32", proto="ietf-rip:rip"), attempts=40) + # R2 should learn R1's static route (redistributed) + until(lambda: route.ipv4_route_exist(R2, "192.168.33.1/32", proto="ietf-rip:rip"), attempts=40) + + with test.step("Test connectivity from PC:data1 to R2 loopback via RIP"): + _, hport0 = env.ltop.xlate("PC", "data1") + with infamy.IsolatedMacVlan(hport0) as ns0: + ns0.addip("192.168.10.2") + ns0.addroute("192.168.200.1/32", "192.168.10.1") + ns0.must_reach("192.168.200.1") + + with test.step("Test connectivity from PC:data2 to R1 loopback via RIP"): + _, hport1 = env.ltop.xlate("PC", "data2") + with infamy.IsolatedMacVlan(hport1) as ns1: + ns1.addip("192.168.60.2") + ns1.addroute("192.168.100.1/32", "192.168.60.1") + ns1.must_reach("192.168.100.1") + + test.succeed() diff --git a/test/case/routing/rip_basic/topology.dot b/test/case/routing/rip_basic/topology.dot new file mode 100644 index 00000000..d8837a83 --- /dev/null +++ b/test/case/routing/rip_basic/topology.dot @@ -0,0 +1,34 @@ +graph "2x2" { + layout="neato"; + overlap="false"; + esep="+40"; + size=10 + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + PC [ + label="PC | { mgmt1 | data1 | \n\n\n\n\n\n\n\n | mgmt2 | data2 }", + pos="20,50!", + requires="controller", + ]; + + R1 [ + label="{ mgmt | data | link} | R1 \n 192.168.100.1/32 \n(lo)", + pos="190,85!", + + requires="infix", + ]; + R2 [ + label="{ link | mgmt | data } | R2 \n 192.168.200.1/32 \n(lo)", + pos="190,15!", + + requires="infix", + ]; + + PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + PC:data1 -- R1:data [color="black", label="192.168.10.0/24", headlabel=".1", taillabel=".2", labeldistance=2, fontcolor="black"] + R1:link -- R2:link [label="192.168.50.0/24", headlabel=".2", taillabel=".1", labeldistance=2, fontcolor="black", color="black"] + PC:data2 -- R2:data [label="192.168.60.0/24", headlabel=".1", taillabel=".2", labeldistance=2, fontcolor="black", color="black"] +} diff --git a/test/case/routing/rip_basic/topology.svg b/test/case/routing/rip_basic/topology.svg new file mode 100644 index 00000000..8f9c7e03 --- /dev/null +++ b/test/case/routing/rip_basic/topology.svg @@ -0,0 +1,89 @@ + + + + + + +2x2 + + + +PC + +PC + +mgmt1 + +data1 + + +mgmt2 + +data2 + + + +R1 + +mgmt + +data + +link + +R1 + 192.168.100.1/32 +(lo) + + + +PC:mgmt1--R1:mgmt + + + + +PC:data1--R1:data + +192.168.10.0/24 +.1 +.2 + + + +R2 + +link + +mgmt + +data + +R2 + 192.168.200.1/32 +(lo) + + + +PC:mgmt2--R2:mgmt + + + + +PC:data2--R2:data + +192.168.60.0/24 +.1 +.2 + + + +R1:link--R2:link + +192.168.50.0/24 +.2 +.1 + + + diff --git a/test/case/routing/rip_multihop/Readme.adoc b/test/case/routing/rip_multihop/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/routing/rip_multihop/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/routing/rip_multihop/test.py b/test/case/routing/rip_multihop/test.py new file mode 100755 index 00000000..44ff528a --- /dev/null +++ b/test/case/routing/rip_multihop/test.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""RIP Multi-hop + +Verifies RIP functionality across multiple hops with three routers in a line +topology (R1 -- R2 -- R3). This test ensures: +- RIP routes propagate through multiple hops +- R2 (middle router) has two RIP neighbors +- End-to-end connectivity works across the RIP network + +Topology: + PC:data1 -- R1 -- R2 -- R3 -- PC:data2 +""" + +import infamy +import infamy.route as route +from infamy.util import until, parallel + + +def config_r1(target, data, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": data, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.10.1", + "prefix-length": 24 + }] + } + }, { + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.1", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "192.168.11.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R1" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": link + }] + } + } + }] + } + } + } + }) + + +def config_r2(target, west, east): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": west, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.2", + "prefix-length": 24 + }] + } + }, { + "name": east, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.60.1", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "192.168.22.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R2" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": west + }, { + "interface": east + }] + } + } + }] + } + } + } + }) + + +def config_r3(target, link, data): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.60.2", + "prefix-length": 24 + }] + } + }, { + "name": data, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.70.1", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "192.168.33.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R3" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": link + }] + } + } + }] + } + } + } + }) + + +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") + R3 = env.attach("R3", "mgmt") + + with test.step("Configure routers"): + _, R1data = env.ltop.xlate("R1", "data") + _, R1link = env.ltop.xlate("R1", "link") + _, R2west = env.ltop.xlate("R2", "west") + _, R2east = env.ltop.xlate("R2", "east") + _, R3link = env.ltop.xlate("R3", "link") + _, R3data = env.ltop.xlate("R3", "data") + + parallel(config_r1(R1, R1data, R1link), + config_r2(R2, R2west, R2east), + config_r3(R3, R3link, R3data)) + + with test.step("Wait for RIP routes to be exchanged"): + print("Waiting for RIP routes to propagate...") + # R1 should learn R2's loopback + until(lambda: route.ipv4_route_exist(R1, "192.168.22.1/32", proto="ietf-rip:rip"), attempts=40) + # R1 should learn R3's loopback (via R2) + until(lambda: route.ipv4_route_exist(R1, "192.168.33.1/32", proto="ietf-rip:rip"), attempts=40) + # R2 should learn R1's loopback + until(lambda: route.ipv4_route_exist(R2, "192.168.11.1/32", proto="ietf-rip:rip"), attempts=40) + # R2 should learn R3's loopback + until(lambda: route.ipv4_route_exist(R2, "192.168.33.1/32", proto="ietf-rip:rip"), attempts=40) + # R3 should learn R2's loopback + until(lambda: route.ipv4_route_exist(R3, "192.168.22.1/32", proto="ietf-rip:rip"), attempts=40) + # R3 should learn R1's loopback (via R2) + until(lambda: route.ipv4_route_exist(R3, "192.168.11.1/32", proto="ietf-rip:rip"), attempts=40) + + with test.step("Verify R2 has two RIP neighbors"): + print("Checking R2 has two RIP neighbors...") + # R2 should have neighbors: 192.168.50.1 (R1) and 192.168.60.2 (R3) + # Query without predicates to avoid RESTCONF encoding issues + routing_data = R2.get_data("/ietf-routing:routing/control-plane-protocols") + + # Navigate to RIP protocol + protocols = routing_data.get("routing", {}).get("control-plane-protocols", {}).get("control-plane-protocol", []) + if not protocols: + raise Exception("No protocols found") + + # Find RIP protocol + rip = None + for protocol in protocols: + if protocol.get("type") == "infix-routing:ripv2" and protocol.get("name") == "default": + rip = protocol.get("rip", {}) + break + + if not rip: + raise Exception("RIP protocol not found in control-plane-protocols") + + ipv4_data = rip.get("ipv4", {}) + neighbors_data = ipv4_data.get("neighbors", {}) + neighbor_list = neighbors_data.get("neighbor", []) + + assert len(neighbor_list) == 2, f"Expected 2 neighbors, found {len(neighbor_list)}" + + neighbor_ips = [n.get("ipv4-address") for n in neighbor_list] + assert "192.168.50.1" in neighbor_ips, "R1 not in neighbor list" + assert "192.168.60.2" in neighbor_ips, "R3 not in neighbor list" + print(f"R2 has correct neighbors: {neighbor_ips}") + + with test.step("Test end-to-end connectivity PC:data1 to R3 loopback"): + _, hport1 = env.ltop.xlate("PC", "data1") + with infamy.IsolatedMacVlan(hport1) as ns1: + ns1.addip("192.168.10.2") + ns1.addroute("192.168.33.1/32", "192.168.10.1") + ns1.must_reach("192.168.33.1") + + with test.step("Test end-to-end connectivity PC:data2 to R1 loopback"): + _, hport2 = env.ltop.xlate("PC", "data2") + with infamy.IsolatedMacVlan(hport2) as ns2: + ns2.addip("192.168.70.2") + ns2.addroute("192.168.11.1/32", "192.168.70.1") + ns2.must_reach("192.168.11.1") + + test.succeed() diff --git a/test/case/routing/rip_multihop/topology.dot b/test/case/routing/rip_multihop/topology.dot new file mode 100644 index 00000000..632dd175 --- /dev/null +++ b/test/case/routing/rip_multihop/topology.dot @@ -0,0 +1,41 @@ +graph "rip-multihop" { + layout="neato"; + overlap="false"; + esep="+40"; + size=10 + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + PC [ + label="PC | { mgmt1 | data1 | \n\n\n\n | mgmt2 | mgmt3 | data2 }", + pos="20,54!", + requires="controller", + ]; + + R1 [ + label="{ mgmt | data | link} | R1 \n 192.168.11.1/32 \n(lo)", + pos="100,70!", + requires="infix", + ]; + + R2 [ + label="{ west | mgmt | east } | R2 \n 192.168.22.1/32 \n(lo)", + pos="100,50!", + requires="infix", + ]; + + R3 [ + label="{ link | mgmt | data } | R3 \n 192.168.33.1/32 \n(lo)", + pos="100,30!", + requires="infix", + ]; + + PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt3 -- R3:mgmt [requires="mgmt", color="lightgray"] + PC:data1 -- R1:data [label="192.168.10.0/24", headlabel=".1", taillabel=".2", labeldistance=2, fontcolor="black", color="black"] + R1:link -- R2:west [label="192.168.50.0/24", headlabel=".2", taillabel=".1", labeldistance=2, fontcolor="black", color="black"] + R2:east -- R3:link [label="192.168.60.0/24", headlabel=".2", taillabel=".1", labeldistance=2, fontcolor="black", color="black"] + R3:data -- PC:data2 [label="192.168.70.0/24", headlabel=".2", taillabel=".1", labeldistance=2, fontcolor="black", color="black"] +} diff --git a/test/case/routing/rip_multihop/topology.svg b/test/case/routing/rip_multihop/topology.svg new file mode 100644 index 00000000..f61c6a2f --- /dev/null +++ b/test/case/routing/rip_multihop/topology.svg @@ -0,0 +1,118 @@ + + + + + + +rip-multihop + + + +PC + +PC + +mgmt1 + +data1 + + +mgmt2 + +mgmt3 + +data2 + + + +R1 + +mgmt + +data + +link + +R1 + 192.168.11.1/32 +(lo) + + + +PC:mgmt1--R1:mgmt + + + + +PC:data1--R1:data + +192.168.10.0/24 +.1 +.2 + + + +R2 + +west + +mgmt + +east + +R2 + 192.168.22.1/32 +(lo) + + + +PC:mgmt2--R2:mgmt + + + + +R3 + +link + +mgmt + +data + +R3 + 192.168.33.1/32 +(lo) + + + +PC:mgmt3--R3:mgmt + + + + +R1:link--R2:west + +192.168.50.0/24 +.2 +.1 + + + +R2:east--R3:link + +192.168.60.0/24 +.2 +.1 + + + +R3:data--PC:data2 + +192.168.70.0/24 +.2 +.1 + + + diff --git a/test/case/routing/rip_passive_interface/Readme.adoc b/test/case/routing/rip_passive_interface/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/routing/rip_passive_interface/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/routing/rip_passive_interface/test.adoc b/test/case/routing/rip_passive_interface/test.adoc new file mode 100644 index 00000000..6e7a1227 --- /dev/null +++ b/test/case/routing/rip_passive_interface/test.adoc @@ -0,0 +1,29 @@ +=== RIP Passive Interface + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/rip_passive_interface] + +==== Description + +Verifies RIP passive interface functionality. A passive interface means that +RIP will include the interface's network in routing updates but will not send +or receive RIP updates on that interface. + +R1 has two RIP-enabled interfaces: +- data: Passive interface (192.168.10.0/24 advertised but no updates sent/received) +- link: Active interface (RIP updates exchanged with R2) + +R2 should learn about 192.168.10.0/24 from R1 via the link interface, even +though the data interface is passive. + +==== Topology + +image::topology.svg[RIP Passive Interface topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure targets +. Wait for RIP to exchange routes +. Verify connectivity to passive interface network + + diff --git a/test/case/routing/rip_passive_interface/test.py b/test/case/routing/rip_passive_interface/test.py new file mode 100755 index 00000000..4761578b --- /dev/null +++ b/test/case/routing/rip_passive_interface/test.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""RIP Passive Interface + +Verifies RIP passive interface functionality. A passive interface means that +RIP will include the interface's network in routing updates but will not send +or receive RIP updates on that interface. + +R1 has two RIP-enabled interfaces: +- data: Passive interface (192.168.10.0/24 advertised but no updates sent/received) +- link: Active interface (RIP updates exchanged with R2) + +R2 should learn about 192.168.10.0/24 from R1 via the link interface, even +though the data interface is passive. + +""" + +import infamy +import infamy.route as route +from infamy.util import until, parallel + + +def config_target1(target, data, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": data, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.10.1", + "prefix-length": 24 + }]} + }, { + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.1", + "prefix-length": 24 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R1" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "interfaces": { + "interface": [{ + "interface": data, + "passive": None + }, { + "interface": link + }] + } + } + }] + } + } + } + }) + + +def config_target2(target, link): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.2", + "prefix-length": 24 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R2" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "interfaces": { + "interface": [{ + "interface": link + }] + } + } + }] + } + } + } + }) + + +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("Configure targets"): + _, R1data = env.ltop.xlate("R1", "data") + _, R2link = env.ltop.xlate("R2", "link") + _, R1link = env.ltop.xlate("R1", "link") + + parallel(config_target1(R1, R1data, R1link), + config_target2(R2, R2link)) + + with test.step("Wait for RIP to exchange routes"): + print("Waiting for RIP routes to propagate...") + # R2 should learn about R1's passive interface network (192.168.10.0/24) + # even though it's passive, R1 should still advertise it + until(lambda: route.ipv4_route_exist(R2, "192.168.10.0/24", proto="ietf-rip:rip"), attempts=40) + + with test.step("Verify connectivity to passive interface network"): + # Test that we can reach the passive interface from PC + _, hport0 = env.ltop.xlate("PC", "data") + with infamy.IsolatedMacVlan(hport0) as ns0: + ns0.addip("192.168.10.2") + # No need for route since we're on the same network + ns0.must_reach("192.168.10.1") + + test.succeed() diff --git a/test/case/routing/rip_passive_interface/topology.dot b/test/case/routing/rip_passive_interface/topology.dot new file mode 100644 index 00000000..8a9bab00 --- /dev/null +++ b/test/case/routing/rip_passive_interface/topology.dot @@ -0,0 +1,33 @@ +graph "rip-passive" { + layout="neato"; + overlap="false"; + esep="+30"; + size=10 + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + PC [ + label="PC | { mgmt1 | data | mgmt2 }", + pos="20,50!", + requires="controller", + ]; + + R1 [ + label="{ mgmt | data | link} | R1", + pos="160,60!", + + requires="infix", + ]; + R2 [ + label="{ link | mgmt } | R2", + pos="160,30!", + + requires="infix", + ]; + + PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + PC:data -- R1:data [color="black", label="192.168.10.0/24", headlabel=".1", taillabel=".2", labeldistance=2, fontcolor="black"] + R1:link -- R2:link [taillabel="192.168.50.0/24", headlabel=".2", taillabel=".1", labeldistance=2, fontcolor="black", color="black"] +} diff --git a/test/case/routing/rip_passive_interface/topology.svg b/test/case/routing/rip_passive_interface/topology.svg new file mode 100644 index 00000000..e678e1a5 --- /dev/null +++ b/test/case/routing/rip_passive_interface/topology.svg @@ -0,0 +1,71 @@ + + + + + + +rip-passive + + + +PC + +PC + +mgmt1 + +data + +mgmt2 + + + +R1 + +mgmt + +data + +link + +R1 + + + +PC:mgmt1--R1:mgmt + + + + +PC:data--R1:data + +192.168.10.0/24 +.1 +.2 + + + +R2 + +link + +mgmt + +R2 + + + +PC:mgmt2--R2:mgmt + + + + +R1:link--R2:link + +.2 +.1 + + + diff --git a/test/case/routing/rip_redistribute/Readme.adoc b/test/case/routing/rip_redistribute/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/routing/rip_redistribute/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/routing/rip_redistribute/test.adoc b/test/case/routing/rip_redistribute/test.adoc new file mode 100644 index 00000000..db970294 --- /dev/null +++ b/test/case/routing/rip_redistribute/test.adoc @@ -0,0 +1,37 @@ +=== RIP Redistribution + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/rip_redistribute] + +==== Description + +Verifies that RIP can redistribute routes from other protocols. + +Topology: +- R1: Gateway router running both RIP and OSPF + - RIP interface to R2 + - OSPF interface to R3 + - Redistributes OSPF routes into RIP + - Redistributes RIP routes into OSPF + +- R2: RIP-only router with loopback 192.168.200.1/32 + +- R3: OSPF-only router with loopback 192.168.100.1/32 + +Expected behavior: +- R2 (RIP) should learn R3's OSPF loopback (192.168.100.1/32) via RIP redistribution +- R3 (OSPF) should learn R2's RIP loopback (192.168.200.1/32) via OSPF redistribution + +==== Topology + +image::topology.svg[RIP Redistribution topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUTs +. Configure routers +. Wait for OSPF to converge on R1-R3 link +. Wait for RIP to converge on R1-R2 link +. Verify R2 (RIP) learns R3's OSPF routes via redistribution +. Verify R3 (OSPF) learns R2's RIP routes via redistribution + + diff --git a/test/case/routing/rip_redistribute/test.py b/test/case/routing/rip_redistribute/test.py new file mode 100755 index 00000000..ad66c594 --- /dev/null +++ b/test/case/routing/rip_redistribute/test.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +"""RIP Redistribution + +Verifies that RIP can redistribute routes from other protocols. + +Topology: +- R1: Gateway router running both RIP and OSPF + - RIP interface to R2 + - OSPF interface to R3 + - Redistributes OSPF routes into RIP + - Redistributes RIP routes into OSPF + +- R2: RIP-only router with loopback 192.168.200.1/32 + +- R3: OSPF-only router with loopback 192.168.100.1/32 + +Expected behavior: +- R2 (RIP) should learn R3's OSPF loopback (192.168.100.1/32) via RIP redistribution +- R3 (OSPF) should learn R2's RIP loopback (192.168.200.1/32) via OSPF redistribution + +""" + +import infamy +import infamy.route as route +from infamy.util import until, parallel + + +def config_r1_gateway(target, rip_link, ospf_link): + """Configure R1 as gateway running both RIP and OSPF""" + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": rip_link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.1", + "prefix-length": 24 + }] + } + }, { + "name": ospf_link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.60.1", + "prefix-length": 24 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R1" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "ospf" + }, { + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": rip_link + }] + } + } + }, { + "type": "infix-routing:ospfv2", + "name": "default", + "ospf": { + "redistribute": { + "redistribute": [{ + "protocol": "rip" + }, { + "protocol": "connected" + }] + }, + "areas": { + "area": [{ + "area-id": "0.0.0.0", + "interfaces": { + "interface": [{ + "enabled": True, + "name": ospf_link, + "hello-interval": 1, + "dead-interval": 3 + }] + } + }] + } + } + }] + } + } + } + }) + + +def config_r2_rip(target, link): + """Configure R2 with RIP only""" + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.50.2", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "192.168.200.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R2" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ripv2", + "name": "default", + "rip": { + "timers": { + "update-interval": 5, + "invalid-interval": 15, + "flush-interval": 20 + }, + "redistribute": { + "redistribute": [{ + "protocol": "connected" + }] + }, + "interfaces": { + "interface": [{ + "interface": link + }] + } + } + }] + } + } + } + }) + + +def config_r3_ospf(target, link): + """Configure R3 with OSPF only""" + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": link, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{ + "ip": "192.168.60.2", + "prefix-length": 24 + }] + } + }, { + "name": "lo", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "192.168.100.1", + "prefix-length": 32 + }] + } + }] + } + }, + "ietf-system": { + "system": { + "hostname": "R3" + } + }, + "ietf-routing": { + "routing": { + "control-plane-protocols": { + "control-plane-protocol": [{ + "type": "infix-routing:ospfv2", + "name": "default", + "ospf": { + "redistribute": { + "redistribute": [{ + "protocol": "connected" + }] + }, + "areas": { + "area": [{ + "area-id": "0.0.0.0", + "interfaces": { + "interface": [{ + "enabled": True, + "name": link, + "hello-interval": 1, + "dead-interval": 3 + }] + } + }] + } + } + }] + } + } + } + }) + + +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") + R3 = env.attach("R3", "mgmt") + + with test.step("Configure routers"): + _, R1rip = env.ltop.xlate("R1", "rip") + _, R1ospf = env.ltop.xlate("R1", "ospf") + _, R2link = env.ltop.xlate("R2", "link") + _, R3link = env.ltop.xlate("R3", "link") + + parallel(config_r1_gateway(R1, R1rip, R1ospf), + config_r2_rip(R2, R2link), + config_r3_ospf(R3, R3link)) + + with test.step("Wait for OSPF to converge on R1-R3 link"): + print("Waiting for OSPF convergence...") + # R1 should learn R3's loopback via OSPF + until(lambda: route.ipv4_route_exist(R1, "192.168.100.1/32", proto="ietf-ospf:ospfv2"), attempts=40) + # R3 should learn R1's OSPF link via OSPF + until(lambda: route.ipv4_route_exist(R3, "192.168.60.0/24", proto="ietf-ospf:ospfv2"), attempts=40) + + with test.step("Wait for RIP to converge on R1-R2 link"): + print("Waiting for RIP convergence...") + # R1 should learn R2's loopback via RIP + until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32", proto="ietf-rip:rip"), attempts=40) + # R2 should learn R1's OSPF link (192.168.60.0/24) via RIP (redistributed from connected) + until(lambda: route.ipv4_route_exist(R2, "192.168.60.0/24", proto="ietf-rip:rip"), attempts=40) + + with test.step("Verify R2 (RIP) learns R3's OSPF routes via redistribution"): + print("Checking OSPF->RIP redistribution...") + # R2 should learn R3's loopback (OSPF route) via RIP redistribution on R1 + until(lambda: route.ipv4_route_exist(R2, "192.168.100.1/32", proto="ietf-rip:rip"), attempts=40) + + with test.step("Verify R3 (OSPF) learns R2's RIP routes via redistribution"): + print("Checking RIP->OSPF redistribution...") + # R3 should learn R2's loopback (RIP route) via OSPF redistribution on R1 + until(lambda: route.ipv4_route_exist(R3, "192.168.200.1/32", proto="ietf-ospf:ospfv2"), attempts=40) + + test.succeed() diff --git a/test/case/routing/rip_redistribute/topology.dot b/test/case/routing/rip_redistribute/topology.dot new file mode 100644 index 00000000..549da253 --- /dev/null +++ b/test/case/routing/rip_redistribute/topology.dot @@ -0,0 +1,39 @@ +graph "rip-redistribute" { + layout="neato"; + overlap="false"; + esep="+40"; + size=10 + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + PC [ + label="PC | { mgmt3 | \n\n | mgmt1 | \n\n | mgmt2 }", + pos="20,50!", + requires="controller", + ]; + + R3 [ + label="{ mgmt | link } | R3 (OSPF) \n 192.168.100.1/32 \n(lo)", + pos="100,100!", + requires="infix", + ]; + + R1 [ + label="{ ospf | mgmt | rip } | R1 (ASBR)", + pos="91,50!", + requires="infix", + ]; + + R2 [ + label="{ link | mgmt } | R2 (RIP) \n 192.168.200.1/32 \n(lo)", + pos="100,10!", + requires="infix", + ]; + + PC:mgmt3 -- R3:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"] + PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"] + R3:link -- R1:ospf [label="192.168.60.0/24", headlabel=".1", taillabel=".2", labeldistance=2, fontcolor="black", color="blue"] + R1:rip -- R2:link [label="192.168.50.0/24", headlabel=".2", taillabel=".1", labeldistance=2, fontcolor="black", color="black"] +} diff --git a/test/case/routing/rip_redistribute/topology.svg b/test/case/routing/rip_redistribute/topology.svg new file mode 100644 index 00000000..f8d19b01 --- /dev/null +++ b/test/case/routing/rip_redistribute/topology.svg @@ -0,0 +1,93 @@ + + + + + + +rip-redistribute + + + +PC + +PC + +mgmt3 + + +mgmt1 + + +mgmt2 + + + +R3 + +mgmt + +link + +R3 (OSPF) + 192.168.100.1/32 +(lo) + + + +PC:mgmt3--R3:mgmt + + + + +R1 + +ospf + +mgmt + +rip + +R1 (ASBR) + + + +PC:mgmt1--R1:mgmt + + + + +R2 + +link + +mgmt + +R2 (RIP) + 192.168.200.1/32 +(lo) + + + +PC:mgmt2--R2:mgmt + + + + +R3:link--R1:ospf + +192.168.60.0/24 +.1 +.2 + + + +R1:rip--R2:link + +192.168.50.0/24 +.2 +.1 + + +