From 23ed6e2f03aa03ae777be5d676ec4cbda591bb06 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 25 Sep 2025 10:15:31 +0200 Subject: [PATCH] test: new test, zone migration, custom service, and IPv6 Signed-off-by: Joachim Wiberg --- test/case/infix_firewall/Readme.adoc | 4 + test/case/infix_firewall/all.yaml | 3 + .../ipv6-zone-migration/Readme.adoc | 1 + .../ipv6-zone-migration/test.adoc | 31 +++ .../ipv6-zone-migration/test.py | 240 ++++++++++++++++++ .../ipv6-zone-migration/topology.dot | 24 ++ .../ipv6-zone-migration/topology.svg | 53 ++++ 7 files changed, 356 insertions(+) create mode 120000 test/case/infix_firewall/ipv6-zone-migration/Readme.adoc create mode 100644 test/case/infix_firewall/ipv6-zone-migration/test.adoc create mode 100755 test/case/infix_firewall/ipv6-zone-migration/test.py create mode 100644 test/case/infix_firewall/ipv6-zone-migration/topology.dot create mode 100644 test/case/infix_firewall/ipv6-zone-migration/topology.svg diff --git a/test/case/infix_firewall/Readme.adoc b/test/case/infix_firewall/Readme.adoc index a9c1b589..fd3e888c 100644 --- a/test/case/infix_firewall/Readme.adoc +++ b/test/case/infix_firewall/Readme.adoc @@ -16,3 +16,7 @@ include::wan-dmz-lan/Readme.adoc[] <<< include::ipv6-lan-wan/Readme.adoc[] + +<<< + +include::ipv6-zone-migration/Readme.adoc[] diff --git a/test/case/infix_firewall/all.yaml b/test/case/infix_firewall/all.yaml index 1dc2560a..2778a31b 100644 --- a/test/case/infix_firewall/all.yaml +++ b/test/case/infix_firewall/all.yaml @@ -10,3 +10,6 @@ - name: IPv6 LAN-WAN Firewall case: ipv6-lan-wan/test.py + +- name: IPv6 Zone Migration with Custom Services + case: ipv6-zone-migration/test.py diff --git a/test/case/infix_firewall/ipv6-zone-migration/Readme.adoc b/test/case/infix_firewall/ipv6-zone-migration/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/infix_firewall/ipv6-zone-migration/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/infix_firewall/ipv6-zone-migration/test.adoc b/test/case/infix_firewall/ipv6-zone-migration/test.adoc new file mode 100644 index 00000000..a39387d2 --- /dev/null +++ b/test/case/infix_firewall/ipv6-zone-migration/test.adoc @@ -0,0 +1,31 @@ +=== IPv6 Zone Migration with Custom Services + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/ipv6-zone-migration] + +==== Description + +This test verifies that firewall rules work consistently across IPv4/IPv6 +protocols and that interfaces can be moved between zones without breaking +active connections. + +- Requires DUT with at least 2 data interfaces supporting IPv6 +- Test host must support dual-stack IPv4/IPv6 configuration +- Custom service ports (8080/tcp) should be available for testing + +==== Topology + +image::topology.svg[IPv6 Zone Migration with Custom Services topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target +. Configure dual-stack interfaces and initial firewall +. Verify initial zone configuration and custom service +. Verify IPv4/IPv6 connectivity and custom service restrictions +. Verify IPv6 custom service functionality +. Perform dynamic zone migration +. Verify connectivity after zone migration +. Verify custom service from migrated interface +. Verify operational state reflects zone changes + + diff --git a/test/case/infix_firewall/ipv6-zone-migration/test.py b/test/case/infix_firewall/ipv6-zone-migration/test.py new file mode 100755 index 00000000..1fd2b2b4 --- /dev/null +++ b/test/case/infix_firewall/ipv6-zone-migration/test.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""IPv6 Zone Migration with Custom Service + +This test verifies that firewall rules work consistently across IPv4/IPv6 +protocols and that interfaces can be moved between zones without breaking +active connections. + +- Requires DUT with at least 2 data interfaces supporting IPv6 +- Test host must support dual-stack IPv4/IPv6 configuration +- Custom service ports (8080/tcp) should be available for testing +""" + +import time +import infamy +from infamy.util import until + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, data1_if = env.ltop.xlate("target", "data1") + _, data2_if = env.ltop.xlate("target", "data2") + _, mgmt_if = env.ltop.xlate("target", "mgmt") + _, host_data1 = env.ltop.xlate("host", "data1") + _, host_data2 = env.ltop.xlate("host", "data2") + + # IPv4 addressing + DATA1_NET_V4 = "10.1.1.0/24" + DATA1_TARGET_V4 = "10.1.1.1" + DATA1_HOST_V4 = "10.1.1.100" + + DATA2_NET_V4 = "10.2.2.0/24" + DATA2_TARGET_V4 = "10.2.2.1" + DATA2_HOST_V4 = "10.2.2.100" + + # IPv6 addressing + DATA1_NET_V6 = "fd01:1:1::/64" + DATA1_TARGET_V6 = "fd01:1:1::1" + DATA1_HOST_V6 = "fd01:1:1::100" + + DATA2_NET_V6 = "fd02:2:2::/64" + DATA2_TARGET_V6 = "fd02:2:2::1" + DATA2_HOST_V6 = "fd02:2:2::100" + + # Custom service port + CUSTOM_PORT = 8080 + + with test.step("Configure dual-stack interfaces and initial firewall"): + target.put_config_dicts({ + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": data1_if, + "enabled": True, + "ipv4": { + "address": [{ + "ip": DATA1_TARGET_V4, + "prefix-length": 24 + }] + }, + "ipv6": { + "enabled": True, + "address": [{ + "ip": DATA1_TARGET_V6, + "prefix-length": 64 + }] + } + }, { + "name": data2_if, + "enabled": True, + "ipv4": { + "address": [{ + "ip": DATA2_TARGET_V4, + "prefix-length": 24 + }] + }, + "ipv6": { + "enabled": True, + "address": [{ + "ip": DATA2_TARGET_V6, + "prefix-length": 64 + }] + } + }] + } + }, + "infix-firewall": { + "firewall": { + "default": "untrusted", + "logging": "all", + "service": [{ + "name": "myapp", + "port": [{ + "lower": CUSTOM_PORT, + "proto": "tcp" + }] + }], + "zone": [{ + "name": "mgmt", + "description": "Management network", + "action": "accept", + "interface": [mgmt_if], + "service": ["ssh", "netconf", "restconf"] + }, { + "name": "untrusted", + "description": "Untrusted zone", + "action": "accept", + "interface": [data1_if] + }, { + "name": "trusted", + "description": "Trusted zone", + "action": "accept", + "interface": [data2_if], + "service": ["ssh", "myapp"] + }] + } + } + }) + + # Wait for configuration to be activated + infamy.Firewall.wait_for_operational(target, { + "untrusted": {"action": "accept"}, + "trusted": {"action": "accept"}, + "mgmt": {"action": "accept"} + }) + + with test.step("Verify initial zone configuration and custom service"): + # Verify operational state matches expected configuration + data = target.get_data("/infix-firewall:firewall") + fw = data["firewall"] + + assert fw["default"] == "untrusted" + + zones = {zone["name"]: zone for zone in fw["zone"]} + services = {svc["name"]: svc for svc in fw.get("service", [])} + + # Verify custom service exists + assert "myapp" in services, "Custom service myapp not found" + custom_service = services["myapp"] + assert len(custom_service["port"]) == 1 + port_entry = next(iter(custom_service["port"])) + assert port_entry["proto"] == "tcp" + assert int(port_entry["lower"]) == CUSTOM_PORT + + # Verify zone assignments + untrusted_zone = zones["untrusted"] + trusted_zone = zones["trusted"] + + assert data1_if in untrusted_zone["interface"] + assert data2_if in trusted_zone["interface"] + + # Check services safely - they may not exist in operational data if empty + trusted_services = trusted_zone.get("service", []) + untrusted_services = untrusted_zone.get("service", []) + + assert "myapp" in trusted_services, f"Custom service should be in trusted zone, got: {trusted_services}" + assert "myapp" not in untrusted_services, f"Custom service should not be in untrusted zone, got: {untrusted_services}" + + with infamy.IsolatedMacVlan(host_data1) as ns1: + ns1.addip(DATA1_HOST_V4, prefix_length=24, proto="ipv4") + ns1.addip(DATA1_HOST_V6, prefix_length=64, proto="ipv6") + + with infamy.IsolatedMacVlan(host_data2) as ns2: + ns2.addip(DATA2_HOST_V4, prefix_length=24, proto="ipv4") + ns2.addip(DATA2_HOST_V6, prefix_length=64, proto="ipv6") + + with test.step("Verify IPv4/IPv6 connectivity and custom service restrictions"): + # print(f"Testing IPv4 connectivity: {DATA1_HOST_V4} -> {DATA1_TARGET_V4}") + # print(f"Testing IPv4 connectivity: {DATA2_HOST_V4} -> {DATA2_TARGET_V4}") + ns1.must_reach(DATA1_TARGET_V4, timeout=5) + ns2.must_reach(DATA2_TARGET_V4, timeout=5) + + # print(f"Testing IPv6 connectivity: {DATA1_HOST_V6} -> {DATA1_TARGET_V6}") + # print(f"Testing IPv6 connectivity: {DATA2_HOST_V6} -> {DATA2_TARGET_V6}") + ns1.must_reach(DATA1_TARGET_V6, timeout=5) + ns2.must_reach(DATA2_TARGET_V6, timeout=5) + + firewall_ns1 = infamy.Firewall(ns1, None) + firewall_ns2 = infamy.Firewall(ns2, None) + + ok, ports = firewall_ns1.verify_allowed(DATA1_TARGET_V4, + [(CUSTOM_PORT, "tcp", "myapp")]) + ok, ports = firewall_ns2.verify_allowed(DATA2_TARGET_V4, + [(CUSTOM_PORT, "tcp", "myapp")]) + + with test.step("Verify IPv6 custom service functionality"): + ok, ports = firewall_ns1.verify_allowed(DATA1_TARGET_V6, + [(CUSTOM_PORT, "tcp", "myapp")]) + ok, ports = firewall_ns2.verify_allowed(DATA2_TARGET_V6, + [(CUSTOM_PORT, "tcp", "myapp")]) + + with test.step("Perform dynamic zone migration"): + target.delete_xpath(f"/infix-firewall:firewall/zone[name='untrusted']/interface[.='{data1_if}']") + target.put_config_dict("infix-firewall", { + "firewall": { + "zone": [{ + "name": "trusted", + "interface": [data1_if] + }] + } + }) + + infamy.Firewall.wait_for_operational(target, { + "untrusted": {"action": "accept"}, + "trusted": {"action": "accept"} + }) + + with test.step("Verify connectivity after zone migration"): + ns1.must_reach(DATA1_TARGET_V4, timeout=3) + ns1.must_reach(DATA1_TARGET_V6, timeout=3) + ns2.must_reach(DATA2_TARGET_V4, timeout=3) + ns2.must_reach(DATA2_TARGET_V6, timeout=3) + + with test.step("Verify custom service from migrated interface"): + firewall_migrated = infamy.Firewall(ns1, None) + ok, ports = firewall_migrated.verify_allowed(DATA1_TARGET_V4, + [(CUSTOM_PORT, "tcp", "myapp")]) + assert ok, f"Custom service should work on IPv4 after zone migration" + + ok, ports = firewall_migrated.verify_allowed(DATA1_TARGET_V6, + [(CUSTOM_PORT, "tcp", "myapp")]) + assert ok, f"Custom service should work on IPv6 after zone migration" + + with test.step("Verify operational state reflects zone changes"): + data = target.get_data("/infix-firewall:firewall") + fw = data["firewall"] + zones = {zone["name"]: zone for zone in fw["zone"]} + + trusted_zone = zones["trusted"] + untrusted_zone = zones["untrusted"] + + assert data1_if in trusted_zone["interface"], "data1_if should be in trusted zone" + assert data2_if in trusted_zone["interface"], "data2_if should be in trusted zone" + assert data1_if not in untrusted_zone.get("interface", []), "data1_if should no longer be in untrusted zone" + + trusted_services = trusted_zone.get("service", []) + assert "myapp" in trusted_services, f"Custom service should be available in trusted zone, got: {trusted_services}" + + test.succeed() diff --git a/test/case/infix_firewall/ipv6-zone-migration/topology.dot b/test/case/infix_firewall/ipv6-zone-migration/topology.dot new file mode 100644 index 00000000..82b25089 --- /dev/null +++ b/test/case/infix_firewall/ipv6-zone-migration/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout = "neato"; + overlap = false; + esep = "+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="1,1!", + requires="controller" + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="3,1!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color=red, fontcolor=red, taillabel="10.1.1.0/24, fd01:1:1::/64"] + host:data2 -- target:data2 [color=green, fontcolor=green, taillabel="10.2.2.0/24, fd02:2:2::/64"] +} \ No newline at end of file diff --git a/test/case/infix_firewall/ipv6-zone-migration/topology.svg b/test/case/infix_firewall/ipv6-zone-migration/topology.svg new file mode 100644 index 00000000..162f896c --- /dev/null +++ b/test/case/infix_firewall/ipv6-zone-migration/topology.svg @@ -0,0 +1,53 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + +10.1.1.0/24, fd01:1:1::/64 + + + +host:data2--target:data2 + +10.2.2.0/24, fd02:2:2::/64 + + +