Files
infix/test/case/firewall/wan-dmz-lan/test.py
T
Joachim Wiberg 1bbd80d8c7 test: drop leading ietf/infix prefix from directories
Let's drop the leading IETF or Infix prefixes from tests.  Initially the
idea was to mimnic the YANG models, but it's difficult to navigate and
does not provide any real benefit to developers or end-users.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-11-20 20:23:23 +01:00

285 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""WAN-DMZ-LAN Firewall with Port Forwarding
Multi-zone firewall setup with port forwarding (DNAT) to a DMZ server,
and masquerading (SNAT) of WAN-bound traffic.
image::wan-dmz-lan.svg[align=center, scaledwidth=50%]
- DUT/Gateway with WAN/DMZ/LAN zones and NAT
- Test host's WAN interface acts as external Internet client
- Test host's DMZ interface acts as internal server (HTTP on port 80)
- Test host's LAN interface acts as internal LAN client
"""
import time
import infamy
from infamy.util import until
with infamy.Test() as test:
with test.step("Set up topology and attach to gateway"):
env = infamy.Env()
gateway = env.attach("gateway", "mgmt")
_, wan_if = env.ltop.xlate("gateway", "wan")
_, dmz_if = env.ltop.xlate("gateway", "dmz")
_, lan_if = env.ltop.xlate("gateway", "lan")
_, mgmt_if = env.ltop.xlate("gateway", "mgmt")
_, host_wan = env.ltop.xlate("host", "wan")
_, host_dmz = env.ltop.xlate("host", "dmz")
_, host_lan = env.ltop.xlate("host", "lan")
WAN_NET = "203.0.113.0/24" # RFC 5737 test network
WAN_ROUTER_IP = "203.0.113.1" # Gateway WAN interface
WAN_CLIENT_IP = "203.0.113.100" # Host WAN interface
DMZ_NET = "10.0.1.0/24"
DMZ_ROUTER_IP = "10.0.1.1" # Gateway DMZ interface
DMZ_SERVER_IP = "10.0.1.100" # Host DMZ interface
LAN_NET = "192.168.1.0/24"
LAN_ROUTER_IP = "192.168.1.1" # Gateway LAN interface
LAN_CLIENT_IP = "192.168.1.100" # Host LAN interface
with test.step("Configure gateway with multi-zone firewall and NAT"):
gateway.put_config_dict("ietf-interfaces", {
"interfaces": {
"interface": [
{
"name": wan_if,
"enabled": True,
"ipv4": {
"forwarding": True,
"address": [{
"ip": WAN_ROUTER_IP,
"prefix-length": 24
}]
}
},
{
"name": dmz_if,
"enabled": True,
"ipv4": {
"forwarding": True,
"address": [{
"ip": DMZ_ROUTER_IP,
"prefix-length": 24
}]
}
},
{
"name": lan_if,
"enabled": True,
"ipv4": {
"forwarding": True,
"address": [{
"ip": LAN_ROUTER_IP,
"prefix-length": 24
}]
}
}
]
}
})
gateway.put_config_dict("infix-firewall", {
"firewall": {
"default": "wan",
"logging": "all",
"zone": [
{
"name": "wan",
"description": "External WAN interface - untrusted",
"action": "drop",
"interface": [wan_if],
"port-forward": [{
"lower": 8080,
"proto": "tcp",
"to": {
"addr": DMZ_SERVER_IP,
"port": 80
}
}]
},
{
"name": "dmz",
"description": "DMZ network - limited trust",
"action": "reject",
"network": [DMZ_NET],
"service": ["http"]
},
{
"name": "lan",
"description": "Internal LAN network - trusted",
"action": "accept",
"interface": [lan_if, mgmt_if]
}
],
"policy": [
{
"name": "loc-to-wan",
"description": "Allow local networks to WAN with SNAT",
"ingress": ["lan", "dmz"],
"egress": ["wan"],
"action": "accept",
"masquerade": True
}, {
"name": "lan-to-dmz",
"description": "Allow LAN access to DMZ services",
"ingress": ["lan"],
"egress": ["dmz"],
"action": "accept",
"service": ["ssh", "http"]
}
]
}
})
# Wait for configuration to be activated
infamy.Firewall.wait_for_operational(gateway, {
"wan": {"action": "drop"},
"dmz": {"action": "reject"},
"lan": {"action": "accept"}
})
# Verify firewall operational state
data = gateway.get_data("/infix-firewall:firewall")
fw = data["firewall"]
zones = {z["name"]: z for z in fw["zone"]}
# Verify WAN zone with port forwarding
wan_zone = zones["wan"]
assert wan_zone["action"] == "drop"
assert wan_if in wan_zone["interface"]
assert len(wan_zone["port-forward"]) == 1
pf = next(iter(wan_zone["port-forward"]))
assert pf["lower"] == 8080
assert pf["to"]["addr"] == DMZ_SERVER_IP
assert pf["to"]["port"] == 80
# Verify DMZ zone
dmz_zone = zones["dmz"]
assert dmz_zone["action"] == "reject"
assert DMZ_NET in dmz_zone["network"]
assert "http" in dmz_zone["service"]
# Verify LAN zone
lan_zone = zones["lan"]
assert lan_zone["action"] == "accept"
assert lan_if in lan_zone["interface"]
# Check policies
policies = {p["name"]: p for p in fw["policy"]}
# Verify loc-to-wan policy
loc_wan_policy = policies["loc-to-wan"]
assert set(loc_wan_policy["ingress"]) == {"lan", "dmz"}
assert loc_wan_policy["egress"] == ["wan"]
assert loc_wan_policy["masquerade"] is True
# Verify lan-to-dmz policy
lan_dmz_policy = policies["lan-to-dmz"]
assert lan_dmz_policy["ingress"] == ["lan"]
assert lan_dmz_policy["egress"] == ["dmz"]
assert "ssh" in lan_dmz_policy["service"]
assert "http" in lan_dmz_policy["service"]
with infamy.IsolatedMacVlan(host_wan) as wan_client:
wan_client.addip(WAN_CLIENT_IP)
with infamy.IsolatedMacVlan(host_dmz) as dmz_server:
dmz_server.addip(DMZ_SERVER_IP)
dmz_server.addroute("0.0.0.0", DMZ_ROUTER_IP, prefix_length="0")
with infamy.IsolatedMacVlan(host_lan) as lan_client:
lan_client.addip(LAN_CLIENT_IP)
lan_client.addroute("0.0.0.0", LAN_ROUTER_IP, prefix_length="0")
with test.step("Verify basic connectivity within zones"):
lan_client.must_reach(LAN_ROUTER_IP, timeout=3)
dmz_server.must_not_reach(DMZ_ROUTER_IP, timeout=3)
with test.step("Verify WAN to DMZ port forwarding (DNAT)"):
firewall = infamy.Firewall(wan_client, dmz_server)
# Test port forwarding: WAN:8080 → DMZ:80
ok, info = firewall.verify_dnat(
WAN_ROUTER_IP, forward_port=8080, target_port=80)
if not ok:
print(f" ⚠ {info}")
test.fail()
with test.step("Verify LAN to DMZ connectivity"):
lan_client.must_reach(DMZ_SERVER_IP, timeout=3)
firewall = infamy.Firewall(lan_client, None)
svc = [
(22, "tcp", "ssh"),
(80, "tcp", "http"),
]
ok, ports = firewall.verify_allowed(DMZ_SERVER_IP, svc)
if not ok:
print(f" ⚠ Some DMZ services filtered from LAN: {', '.join(ports)}")
test.fail()
with test.step("Verify DMZ to LAN blocking"):
dmz_server.must_not_reach(LAN_CLIENT_IP, timeout=3)
with test.step("Verify WAN isolation"):
firewall = infamy.Firewall(wan_client, None)
ok, ports, _ = firewall.verify_blocked(LAN_ROUTER_IP)
if not ok:
print(f" ⚠ WAN can access LAN ports: {', '.join(ports)}")
test.fail()
ok, ports, _ = firewall.verify_blocked(DMZ_ROUTER_IP)
if not ok:
print(f" ⚠ WAN can access DMZ ports: {', '.join(ports)}")
with test.step("Verify LAN to WAN connectivity with SNAT"):
firewall = infamy.Firewall(lan_client, wan_client)
lan_client.must_reach(WAN_CLIENT_IP, timeout=3)
ok, info = firewall.verify_snat(WAN_CLIENT_IP, WAN_ROUTER_IP)
if not ok:
print(f" ⚠ LAN to WAN SNAT: {info}")
test.fail()
with test.step("Verify DMZ to WAN connectivity with SNAT"):
firewall = infamy.Firewall(dmz_server, wan_client)
dmz_server.must_reach(WAN_CLIENT_IP, timeout=3)
ok, info = firewall.verify_snat(WAN_CLIENT_IP, WAN_ROUTER_IP)
if not ok:
print(f" ⚠ DMZ to WAN SNAT: {info}")
test.fail()
with test.step("Verify zone default actions/services"):
firewall_lan = infamy.Firewall(lan_client, None)
firewall_dmz = infamy.Firewall(dmz_server, None)
firewall_wan = infamy.Firewall(wan_client, None)
svc = [
(22, "tcp", "ssh"),
(53, "udp", "dns"),
(67, "udp", "dhcp")
]
ok, ports = firewall_lan.verify_allowed(LAN_ROUTER_IP, svc)
if not ok:
print(f" ⚠ LAN services not properly accessible: {', '.join(ports)}")
svc = [(80, "tcp", "http")]
ok, ports = firewall_dmz.verify_allowed(DMZ_ROUTER_IP, svc)
if not ok:
print(f" ⚠ DMZ HTTP service not accessible: {', '.join(ports)}")
ok, ports, _ = firewall_wan.verify_blocked(WAN_ROUTER_IP)
if not ok:
print(f" ⚠ WAN has unexpected open ports: {', '.join(ports)}")
test.succeed()