Skip to content

Commit a0b641f

Browse files
committed
test: new test, zone migration, custom service, and IPv6
Signed-off-by: Joachim Wiberg <[email protected]>
1 parent 8584989 commit a0b641f

File tree

7 files changed

+356
-0
lines changed

7 files changed

+356
-0
lines changed

test/case/infix_firewall/Readme.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ include::wan-dmz-lan/Readme.adoc[]
1616
<<<
1717

1818
include::ipv6-lan-wan/Readme.adoc[]
19+
20+
<<<
21+
22+
include::ipv6-zone-migration/Readme.adoc[]

test/case/infix_firewall/all.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010

1111
- name: IPv6 LAN-WAN Firewall
1212
case: ipv6-lan-wan/test.py
13+
14+
- name: IPv6 Zone Migration with Custom Services
15+
case: ipv6-zone-migration/test.py
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test.adoc
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
=== IPv6 Zone Migration with Custom Services
2+
3+
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/ipv6-zone-migration]
4+
5+
==== Description
6+
7+
This test verifies that firewall rules work consistently across IPv4/IPv6
8+
protocols and that interfaces can be moved between zones without breaking
9+
active connections.
10+
11+
- Requires DUT with at least 2 data interfaces supporting IPv6
12+
- Test host must support dual-stack IPv4/IPv6 configuration
13+
- Custom service ports (8080/tcp) should be available for testing
14+
15+
==== Topology
16+
17+
image::topology.svg[IPv6 Zone Migration with Custom Services topology, align=center, scaledwidth=75%]
18+
19+
==== Sequence
20+
21+
. Set up topology and attach to target
22+
. Configure dual-stack interfaces and initial firewall
23+
. Verify initial zone configuration and custom service
24+
. Verify IPv4/IPv6 connectivity and custom service restrictions
25+
. Verify IPv6 custom service functionality
26+
. Perform dynamic zone migration
27+
. Verify connectivity after zone migration
28+
. Verify custom service from migrated interface
29+
. Verify operational state reflects zone changes
30+
31+
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/usr/bin/env python3
2+
"""IPv6 Zone Migration with Custom Service
3+
4+
This test verifies that firewall rules work consistently across IPv4/IPv6
5+
protocols and that interfaces can be moved between zones without breaking
6+
active connections.
7+
8+
- Requires DUT with at least 2 data interfaces supporting IPv6
9+
- Test host must support dual-stack IPv4/IPv6 configuration
10+
- Custom service ports (8080/tcp) should be available for testing
11+
"""
12+
13+
import time
14+
import infamy
15+
from infamy.util import until
16+
17+
18+
with infamy.Test() as test:
19+
with test.step("Set up topology and attach to target"):
20+
env = infamy.Env()
21+
target = env.attach("target", "mgmt")
22+
_, data1_if = env.ltop.xlate("target", "data1")
23+
_, data2_if = env.ltop.xlate("target", "data2")
24+
_, mgmt_if = env.ltop.xlate("target", "mgmt")
25+
_, host_data1 = env.ltop.xlate("host", "data1")
26+
_, host_data2 = env.ltop.xlate("host", "data2")
27+
28+
# IPv4 addressing
29+
DATA1_NET_V4 = "10.1.1.0/24"
30+
DATA1_TARGET_V4 = "10.1.1.1"
31+
DATA1_HOST_V4 = "10.1.1.100"
32+
33+
DATA2_NET_V4 = "10.2.2.0/24"
34+
DATA2_TARGET_V4 = "10.2.2.1"
35+
DATA2_HOST_V4 = "10.2.2.100"
36+
37+
# IPv6 addressing
38+
DATA1_NET_V6 = "fd01:1:1::/64"
39+
DATA1_TARGET_V6 = "fd01:1:1::1"
40+
DATA1_HOST_V6 = "fd01:1:1::100"
41+
42+
DATA2_NET_V6 = "fd02:2:2::/64"
43+
DATA2_TARGET_V6 = "fd02:2:2::1"
44+
DATA2_HOST_V6 = "fd02:2:2::100"
45+
46+
# Custom service port
47+
CUSTOM_PORT = 8080
48+
49+
with test.step("Configure dual-stack interfaces and initial firewall"):
50+
target.put_config_dicts({
51+
"ietf-interfaces": {
52+
"interfaces": {
53+
"interface": [{
54+
"name": data1_if,
55+
"enabled": True,
56+
"ipv4": {
57+
"address": [{
58+
"ip": DATA1_TARGET_V4,
59+
"prefix-length": 24
60+
}]
61+
},
62+
"ipv6": {
63+
"enabled": True,
64+
"address": [{
65+
"ip": DATA1_TARGET_V6,
66+
"prefix-length": 64
67+
}]
68+
}
69+
}, {
70+
"name": data2_if,
71+
"enabled": True,
72+
"ipv4": {
73+
"address": [{
74+
"ip": DATA2_TARGET_V4,
75+
"prefix-length": 24
76+
}]
77+
},
78+
"ipv6": {
79+
"enabled": True,
80+
"address": [{
81+
"ip": DATA2_TARGET_V6,
82+
"prefix-length": 64
83+
}]
84+
}
85+
}]
86+
}
87+
},
88+
"infix-firewall": {
89+
"firewall": {
90+
"default": "untrusted",
91+
"logging": "all",
92+
"service": [{
93+
"name": "myapp",
94+
"port": [{
95+
"lower": CUSTOM_PORT,
96+
"proto": "tcp"
97+
}]
98+
}],
99+
"zone": [{
100+
"name": "mgmt",
101+
"description": "Management network",
102+
"action": "accept",
103+
"interface": [mgmt_if],
104+
"service": ["ssh", "netconf", "restconf"]
105+
}, {
106+
"name": "untrusted",
107+
"description": "Untrusted zone",
108+
"action": "accept",
109+
"interface": [data1_if]
110+
}, {
111+
"name": "trusted",
112+
"description": "Trusted zone",
113+
"action": "accept",
114+
"interface": [data2_if],
115+
"service": ["ssh", "myapp"]
116+
}]
117+
}
118+
}
119+
})
120+
121+
# Wait for configuration to be activated
122+
infamy.Firewall.wait_for_operational(target, {
123+
"untrusted": {"action": "accept"},
124+
"trusted": {"action": "accept"},
125+
"mgmt": {"action": "accept"}
126+
})
127+
128+
with test.step("Verify initial zone configuration and custom service"):
129+
# Verify operational state matches expected configuration
130+
data = target.get_data("/infix-firewall:firewall")
131+
fw = data["firewall"]
132+
133+
assert fw["default"] == "untrusted"
134+
135+
zones = {zone["name"]: zone for zone in fw["zone"]}
136+
services = {svc["name"]: svc for svc in fw.get("service", [])}
137+
138+
# Verify custom service exists
139+
assert "myapp" in services, "Custom service myapp not found"
140+
custom_service = services["myapp"]
141+
assert len(custom_service["port"]) == 1
142+
port_entry = next(iter(custom_service["port"]))
143+
assert port_entry["proto"] == "tcp"
144+
assert int(port_entry["lower"]) == CUSTOM_PORT
145+
146+
# Verify zone assignments
147+
untrusted_zone = zones["untrusted"]
148+
trusted_zone = zones["trusted"]
149+
150+
assert data1_if in untrusted_zone["interface"]
151+
assert data2_if in trusted_zone["interface"]
152+
153+
# Check services safely - they may not exist in operational data if empty
154+
trusted_services = trusted_zone.get("service", [])
155+
untrusted_services = untrusted_zone.get("service", [])
156+
157+
assert "myapp" in trusted_services, f"Custom service should be in trusted zone, got: {trusted_services}"
158+
assert "myapp" not in untrusted_services, f"Custom service should not be in untrusted zone, got: {untrusted_services}"
159+
160+
with infamy.IsolatedMacVlan(host_data1) as ns1:
161+
ns1.addip(DATA1_HOST_V4, prefix_length=24, proto="ipv4")
162+
ns1.addip(DATA1_HOST_V6, prefix_length=64, proto="ipv6")
163+
164+
with infamy.IsolatedMacVlan(host_data2) as ns2:
165+
ns2.addip(DATA2_HOST_V4, prefix_length=24, proto="ipv4")
166+
ns2.addip(DATA2_HOST_V6, prefix_length=64, proto="ipv6")
167+
168+
with test.step("Verify IPv4/IPv6 connectivity and custom service restrictions"):
169+
# print(f"Testing IPv4 connectivity: {DATA1_HOST_V4} -> {DATA1_TARGET_V4}")
170+
# print(f"Testing IPv4 connectivity: {DATA2_HOST_V4} -> {DATA2_TARGET_V4}")
171+
ns1.must_reach(DATA1_TARGET_V4, timeout=5)
172+
ns2.must_reach(DATA2_TARGET_V4, timeout=5)
173+
174+
# print(f"Testing IPv6 connectivity: {DATA1_HOST_V6} -> {DATA1_TARGET_V6}")
175+
# print(f"Testing IPv6 connectivity: {DATA2_HOST_V6} -> {DATA2_TARGET_V6}")
176+
ns1.must_reach(DATA1_TARGET_V6, timeout=5)
177+
ns2.must_reach(DATA2_TARGET_V6, timeout=5)
178+
179+
firewall_ns1 = infamy.Firewall(ns1, None)
180+
firewall_ns2 = infamy.Firewall(ns2, None)
181+
182+
ok, ports = firewall_ns1.verify_allowed(DATA1_TARGET_V4,
183+
[(CUSTOM_PORT, "tcp", "myapp")])
184+
ok, ports = firewall_ns2.verify_allowed(DATA2_TARGET_V4,
185+
[(CUSTOM_PORT, "tcp", "myapp")])
186+
187+
with test.step("Verify IPv6 custom service functionality"):
188+
ok, ports = firewall_ns1.verify_allowed(DATA1_TARGET_V6,
189+
[(CUSTOM_PORT, "tcp", "myapp")])
190+
ok, ports = firewall_ns2.verify_allowed(DATA2_TARGET_V6,
191+
[(CUSTOM_PORT, "tcp", "myapp")])
192+
193+
with test.step("Perform dynamic zone migration"):
194+
target.delete_xpath(f"/infix-firewall:firewall/zone[name='untrusted']/interface[.='{data1_if}']")
195+
target.put_config_dict("infix-firewall", {
196+
"firewall": {
197+
"zone": [{
198+
"name": "trusted",
199+
"interface": [data1_if]
200+
}]
201+
}
202+
})
203+
204+
infamy.Firewall.wait_for_operational(target, {
205+
"untrusted": {"action": "accept"},
206+
"trusted": {"action": "accept"}
207+
})
208+
209+
with test.step("Verify connectivity after zone migration"):
210+
ns1.must_reach(DATA1_TARGET_V4, timeout=3)
211+
ns1.must_reach(DATA1_TARGET_V6, timeout=3)
212+
ns2.must_reach(DATA2_TARGET_V4, timeout=3)
213+
ns2.must_reach(DATA2_TARGET_V6, timeout=3)
214+
215+
with test.step("Verify custom service from migrated interface"):
216+
firewall_migrated = infamy.Firewall(ns1, None)
217+
ok, ports = firewall_migrated.verify_allowed(DATA1_TARGET_V4,
218+
[(CUSTOM_PORT, "tcp", "myapp")])
219+
assert ok, f"Custom service should work on IPv4 after zone migration"
220+
221+
ok, ports = firewall_migrated.verify_allowed(DATA1_TARGET_V6,
222+
[(CUSTOM_PORT, "tcp", "myapp")])
223+
assert ok, f"Custom service should work on IPv6 after zone migration"
224+
225+
with test.step("Verify operational state reflects zone changes"):
226+
data = target.get_data("/infix-firewall:firewall")
227+
fw = data["firewall"]
228+
zones = {zone["name"]: zone for zone in fw["zone"]}
229+
230+
trusted_zone = zones["trusted"]
231+
untrusted_zone = zones["untrusted"]
232+
233+
assert data1_if in trusted_zone["interface"], "data1_if should be in trusted zone"
234+
assert data2_if in trusted_zone["interface"], "data2_if should be in trusted zone"
235+
assert data1_if not in untrusted_zone.get("interface", []), "data1_if should no longer be in untrusted zone"
236+
237+
trusted_services = trusted_zone.get("service", [])
238+
assert "myapp" in trusted_services, f"Custom service should be available in trusted zone, got: {trusted_services}"
239+
240+
test.succeed()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
graph "1x3" {
2+
layout = "neato";
3+
overlap = false;
4+
esep = "+80";
5+
6+
node [shape=record, fontname="DejaVu Sans Mono, Book"];
7+
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
8+
9+
host [
10+
label="host | { <mgmt> mgmt | <data1> data1 | <data2> data2 }",
11+
pos="1,1!",
12+
requires="controller"
13+
];
14+
15+
target [
16+
label="{ <mgmt> mgmt | <data1> data1 | <data2> data2 } | target",
17+
pos="3,1!",
18+
requires="infix",
19+
];
20+
21+
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
22+
host:data1 -- target:data1 [color=red, fontcolor=red, taillabel="10.1.1.0/24, fd01:1:1::/64"]
23+
host:data2 -- target:data2 [color=green, fontcolor=green, taillabel="10.2.2.0/24, fd02:2:2::/64"]
24+
}
Lines changed: 53 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)