|
54 | 54 | Extension, |
55 | 55 | LoadBalancerRule, |
56 | 56 | Network, |
| 57 | + NetworkACL, |
| 58 | + NetworkACLList, |
57 | 59 | NetworkOffering, |
58 | 60 | NATRule, |
59 | 61 | PublicIPAddress, |
@@ -380,6 +382,7 @@ class TestNetworkExtensionNamespace(cloudstackTestCase): |
380 | 382 | test_05 — full isolated lifecycle: static NAT, PF, LB, restart |
381 | 383 | (all with SSH connectivity verification via keypair) |
382 | 384 | test_06 — VPC multi-tier + VPC restart with SSH verification |
| 385 | + test_07 — VPC Network ACL testing with multiple tiers and traffic rules |
383 | 386 | """ |
384 | 387 |
|
385 | 388 | @classmethod |
@@ -1726,3 +1729,382 @@ def test_06_vpc_multi_tier_and_restart(self): |
1726 | 1729 |
|
1727 | 1730 | self._teardown_extension() |
1728 | 1731 | self.logger.info("test_06 PASSED") |
| 1732 | + |
| 1733 | + @attr(tags=["advanced", "smoke"], required_hardware="true") |
| 1734 | + def test_07_vpc_network_acl(self): |
| 1735 | + """VPC Network ACL testing with multiple tiers and traffic rules. |
| 1736 | +
|
| 1737 | + Creates two VPC tiers with distinct network ACL lists and verifies that |
| 1738 | + ACL rules are correctly applied: |
| 1739 | + - Inbound rules from public network (via port forwarding) |
| 1740 | + - Egress rules between VPC tiers (via ping) |
| 1741 | +
|
| 1742 | + Sub-tests in order |
| 1743 | + ------------------ |
| 1744 | + A. Setup: Create two tiers with different ACL lists |
| 1745 | + tier1 (acl1): Allow ICMP from anywhere, Deny SSH |
| 1746 | + tier2 (acl2): Allow ICMP and SSH from anywhere |
| 1747 | + B. Deploy VMs and verify ACLs block SSH to tier1, allow ICMP to both tiers |
| 1748 | + C. Test inter-tier ICMP communication between VMs |
| 1749 | + D. Verify SSH only works on tier2 (where ACL permits it) |
| 1750 | + E. Cleanup |
| 1751 | +
|
| 1752 | + The test uses ICMP (ping) to verify inter-tier connectivity and SSH |
| 1753 | + attempts to verify ACL rules are enforced on ingress traffic. |
| 1754 | + """ |
| 1755 | + self._check_kvm_host_prerequisites(['arping', 'dnsmasq', 'haproxy']) |
| 1756 | + |
| 1757 | + # ---- Setup: extension + NSP (supporting NetworkACL) ---- |
| 1758 | + svc = "SourceNat,StaticNat,PortForwarding,Lb,UserData,Dhcp,Dns,NetworkACL" |
| 1759 | + _nw_offering, ext_name = self._setup_extension_nsp_offering( |
| 1760 | + "extnet-acl", supported_services=svc, for_vpc=True) |
| 1761 | + |
| 1762 | + # ---- VPC tier network offering (useVpc=on, with NetworkACL support) ---- |
| 1763 | + vpc_tier_svc = "SourceNat,StaticNat,PortForwarding,Lb,UserData,Dhcp,Dns,NetworkACL" |
| 1764 | + _tier_prov = {s.strip(): ext_name for s in vpc_tier_svc.split(',')} |
| 1765 | + vpc_tier_offering = NetworkOffering.create(self.apiclient, { |
| 1766 | + "name": "ExtNet-VPCTier-ACL-%s" % random_gen(), |
| 1767 | + "displaytext": "ExtNet VPC tier offering with ACL", |
| 1768 | + "guestiptype": "Isolated", |
| 1769 | + "traffictype": "GUEST", |
| 1770 | + "availability": "Optional", |
| 1771 | + "useVpc": "on", |
| 1772 | + "supportedservices": vpc_tier_svc, |
| 1773 | + "serviceProviderList": _tier_prov, |
| 1774 | + "serviceCapabilityList": { |
| 1775 | + "SourceNat": {"SupportedSourceNatTypes": "peraccount"}, |
| 1776 | + }, |
| 1777 | + }) |
| 1778 | + self.cleanup.append(vpc_tier_offering) |
| 1779 | + vpc_tier_offering.update(self.apiclient, state='Enabled') |
| 1780 | + self.logger.info("VPC tier offering '%s' enabled", vpc_tier_offering.name) |
| 1781 | + |
| 1782 | + # ---- VPC offering ---- |
| 1783 | + vpc_svc = "SourceNat,StaticNat,PortForwarding,Lb,UserData,Dhcp,Dns,NetworkACL" |
| 1784 | + _vpc_prov = {s.strip(): ext_name for s in vpc_svc.split(',')} |
| 1785 | + vpc_offering = VpcOffering.create(self.apiclient, { |
| 1786 | + "name": "ExtNet-VPC-ACL-%s" % random_gen(), |
| 1787 | + "displaytext": "ExtNet VPC offering with ACL", |
| 1788 | + "supportedservices": vpc_svc, |
| 1789 | + "serviceProviderList": _vpc_prov, |
| 1790 | + }) |
| 1791 | + self.cleanup.append(vpc_offering) |
| 1792 | + vpc_offering.update(self.apiclient, state='Enabled') |
| 1793 | + self.logger.info("VPC offering '%s' enabled", vpc_offering.name) |
| 1794 | + |
| 1795 | + # ---- Account ---- |
| 1796 | + suffix = random_gen() |
| 1797 | + account = Account.create( |
| 1798 | + self.apiclient, |
| 1799 | + self.services["account"], |
| 1800 | + admin=True, |
| 1801 | + domainid=self.domain.id |
| 1802 | + ) |
| 1803 | + self.cleanup.append(account) |
| 1804 | + account_keypair = self._create_account_keypair(account, suffix) |
| 1805 | + |
| 1806 | + # ---- VPC ---- |
| 1807 | + vpc = VPC.create( |
| 1808 | + self.apiclient, |
| 1809 | + {"name": "extnet-vpc-acl-%s" % suffix, |
| 1810 | + "displaytext": "ExtNet VPC ACL %s" % suffix, |
| 1811 | + "cidr": "10.2.0.0/16"}, |
| 1812 | + vpcofferingid=vpc_offering.id, |
| 1813 | + zoneid=self.zone.id, |
| 1814 | + account=account.name, |
| 1815 | + domainid=account.domainid |
| 1816 | + ) |
| 1817 | + self.cleanup.insert(0, vpc) |
| 1818 | + self.logger.info("VPC created: %s (%s)", vpc.name, vpc.id) |
| 1819 | + |
| 1820 | + # ---- Network ACL Lists ---- |
| 1821 | + # ACL1: Restrict to ICMP only (deny SSH) |
| 1822 | + acl1 = NetworkACLList.create( |
| 1823 | + self.apiclient, |
| 1824 | + {"name": "acl1-icmp-only-%s" % suffix, |
| 1825 | + "description": "ACL1 for tier1 - ICMP only"}, |
| 1826 | + vpcid=vpc.id |
| 1827 | + ) |
| 1828 | + self.cleanup.insert(0, acl1) |
| 1829 | + self.logger.info("ACL1 created: %s (ICMP only)", acl1.id) |
| 1830 | + |
| 1831 | + # ACL2: Allow ICMP and SSH |
| 1832 | + acl2 = NetworkACLList.create( |
| 1833 | + self.apiclient, |
| 1834 | + {"name": "acl2-icmp-ssh-%s" % suffix, |
| 1835 | + "description": "ACL2 for tier2 - ICMP and SSH allowed"}, |
| 1836 | + vpcid=vpc.id |
| 1837 | + ) |
| 1838 | + self.cleanup.insert(0, acl2) |
| 1839 | + self.logger.info("ACL2 created: %s (ICMP and SSH)", acl2.id) |
| 1840 | + |
| 1841 | + # ---- Tier 1 with ACL1 (ICMP only) ---- |
| 1842 | + tier1 = Network.create( |
| 1843 | + self.apiclient, |
| 1844 | + {"name": "tier1-acl-%s" % suffix, |
| 1845 | + "displaytext": "Tier 1 ACL %s" % suffix}, |
| 1846 | + accountid=account.name, |
| 1847 | + domainid=account.domainid, |
| 1848 | + networkofferingid=vpc_tier_offering.id, |
| 1849 | + zoneid=self.zone.id, |
| 1850 | + vpcid=vpc.id, |
| 1851 | + gateway="10.2.1.1", |
| 1852 | + netmask="255.255.255.0" |
| 1853 | + ) |
| 1854 | + self.cleanup.insert(0, tier1) |
| 1855 | + self.logger.info("Tier 1 created: %s (%s)", tier1.name, tier1.id) |
| 1856 | + |
| 1857 | + # ---- Tier 2 with ACL2 (ICMP + SSH) ---- |
| 1858 | + tier2 = Network.create( |
| 1859 | + self.apiclient, |
| 1860 | + {"name": "tier2-acl-%s" % suffix, |
| 1861 | + "displaytext": "Tier 2 ACL %s" % suffix}, |
| 1862 | + accountid=account.name, |
| 1863 | + domainid=account.domainid, |
| 1864 | + networkofferingid=vpc_tier_offering.id, |
| 1865 | + zoneid=self.zone.id, |
| 1866 | + vpcid=vpc.id, |
| 1867 | + gateway="10.2.2.1", |
| 1868 | + netmask="255.255.255.0" |
| 1869 | + ) |
| 1870 | + self.cleanup.insert(0, tier2) |
| 1871 | + self.logger.info("Tier 2 created: %s (%s)", tier2.name, tier2.id) |
| 1872 | + |
| 1873 | + svc_offering = ServiceOffering.list(self.apiclient, issystem=False)[0] |
| 1874 | + |
| 1875 | + # ---- VM in tier 1 ---- |
| 1876 | + vm1_cfg = {"displayname": "vm1-acl-%s" % suffix, |
| 1877 | + "name": "vm1-acl-%s" % suffix, |
| 1878 | + "zoneid": self.zone.id} |
| 1879 | + vm1_kw = dict(accountid=account.name, |
| 1880 | + domainid=account.domainid, |
| 1881 | + serviceofferingid=svc_offering.id, |
| 1882 | + templateid=self.template.id, |
| 1883 | + networkids=[tier1.id]) |
| 1884 | + if account_keypair: |
| 1885 | + vm1_kw["keypair"] = account_keypair.name |
| 1886 | + vm1 = VirtualMachine.create(self.apiclient, vm1_cfg, **vm1_kw) |
| 1887 | + self.cleanup.insert(0, vm1) |
| 1888 | + self.logger.info("VM1 deployed in tier 1: %s (%s)", vm1.name, vm1.id) |
| 1889 | + |
| 1890 | + # ---- VM in tier 2 ---- |
| 1891 | + vm2_cfg = {"displayname": "vm2-acl-%s" % suffix, |
| 1892 | + "name": "vm2-acl-%s" % suffix, |
| 1893 | + "zoneid": self.zone.id} |
| 1894 | + vm2_kw = dict(accountid=account.name, |
| 1895 | + domainid=account.domainid, |
| 1896 | + serviceofferingid=svc_offering.id, |
| 1897 | + templateid=self.template.id, |
| 1898 | + networkids=[tier2.id]) |
| 1899 | + if account_keypair: |
| 1900 | + vm2_kw["keypair"] = account_keypair.name |
| 1901 | + vm2 = VirtualMachine.create(self.apiclient, vm2_cfg, **vm2_kw) |
| 1902 | + self.cleanup.insert(0, vm2) |
| 1903 | + self.logger.info("VM2 deployed in tier 2: %s (%s)", vm2.name, vm2.id) |
| 1904 | + |
| 1905 | + # Get VM IPs for later use |
| 1906 | + vm1_networks = VirtualMachine.list(self.apiclient, id=vm1.id)[0].nic |
| 1907 | + vm1_ip = None |
| 1908 | + for nic in vm1_networks: |
| 1909 | + if nic.networkid == tier1.id: |
| 1910 | + vm1_ip = nic.ipaddress |
| 1911 | + self.assertIsNotNone(vm1_ip, "VM1 should have IP in tier1") |
| 1912 | + self.logger.info("VM1 IP in tier1: %s", vm1_ip) |
| 1913 | + |
| 1914 | + vm2_networks = VirtualMachine.list(self.apiclient, id=vm2.id)[0].nic |
| 1915 | + vm2_ip = None |
| 1916 | + for nic in vm2_networks: |
| 1917 | + if nic.networkid == tier2.id: |
| 1918 | + vm2_ip = nic.ipaddress |
| 1919 | + self.assertIsNotNone(vm2_ip, "VM2 should have IP in tier2") |
| 1920 | + self.logger.info("VM2 IP in tier2: %s", vm2_ip) |
| 1921 | + |
| 1922 | + # ============================================================== |
| 1923 | + # A. Setup ACL rules |
| 1924 | + # ============================================================== |
| 1925 | + self.logger.info("--- Sub-test A: Setting up ACL rules ---") |
| 1926 | + |
| 1927 | + # ACL1 rules: ICMP allowed, SSH denied (ingress), ICMP allowed (egress) |
| 1928 | + # Rule numbers must be unique per ACL list (across ingress+egress). |
| 1929 | + # Ingress rule: Allow ICMP |
| 1930 | + NetworkACL.create( |
| 1931 | + self.apiclient, |
| 1932 | + {"protocol": "ICMP", "icmptype": -1, "icmpcode": -1, |
| 1933 | + "traffictype": "Ingress", "aclid": acl1.id, |
| 1934 | + "cidrlist": ["0.0.0.0/0"], "action": "Allow", "number": 10}, |
| 1935 | + networkid=tier1.id |
| 1936 | + ) |
| 1937 | + self.logger.info("ACL1 Ingress rule: ICMP Allow") |
| 1938 | + |
| 1939 | + # Ingress rule: Deny SSH |
| 1940 | + NetworkACL.create( |
| 1941 | + self.apiclient, |
| 1942 | + {"protocol": "TCP", "startport": 22, "endport": 22, |
| 1943 | + "traffictype": "Ingress", "aclid": acl1.id, |
| 1944 | + "cidrlist": ["0.0.0.0/0"], "action": "Deny", "number": 20}, |
| 1945 | + networkid=tier1.id |
| 1946 | + ) |
| 1947 | + self.logger.info("ACL1 Ingress rule: SSH Deny") |
| 1948 | + |
| 1949 | + # Egress rule: Allow all |
| 1950 | + NetworkACL.create( |
| 1951 | + self.apiclient, |
| 1952 | + {"protocol": "All", "traffictype": "Egress", "aclid": acl1.id, |
| 1953 | + "cidrlist": ["0.0.0.0/0"], "action": "Allow", "number": 30}, |
| 1954 | + networkid=tier1.id |
| 1955 | + ) |
| 1956 | + self.logger.info("ACL1 Egress rule: All Allow") |
| 1957 | + |
| 1958 | + # ACL2 rules: ICMP and SSH allowed (ingress), All allowed (egress) |
| 1959 | + # Ingress rule: Allow ICMP |
| 1960 | + NetworkACL.create( |
| 1961 | + self.apiclient, |
| 1962 | + {"protocol": "ICMP", "icmptype": -1, "icmpcode": -1, |
| 1963 | + "traffictype": "Ingress", "aclid": acl2.id, |
| 1964 | + "cidrlist": ["0.0.0.0/0"], "action": "Allow", "number": 10}, |
| 1965 | + networkid=tier2.id |
| 1966 | + ) |
| 1967 | + self.logger.info("ACL2 Ingress rule: ICMP Allow") |
| 1968 | + |
| 1969 | + # Ingress rule: Allow SSH |
| 1970 | + NetworkACL.create( |
| 1971 | + self.apiclient, |
| 1972 | + {"protocol": "TCP", "startport": 22, "endport": 22, |
| 1973 | + "traffictype": "Ingress", "aclid": acl2.id, |
| 1974 | + "cidrlist": ["0.0.0.0/0"], "action": "Allow", "number": 20}, |
| 1975 | + networkid=tier2.id |
| 1976 | + ) |
| 1977 | + self.logger.info("ACL2 Ingress rule: SSH Allow") |
| 1978 | + |
| 1979 | + # Egress rule: Allow all |
| 1980 | + NetworkACL.create( |
| 1981 | + self.apiclient, |
| 1982 | + {"protocol": "All", "traffictype": "Egress", "aclid": acl2.id, |
| 1983 | + "cidrlist": ["0.0.0.0/0"], "action": "Allow", "number": 30}, |
| 1984 | + networkid=tier2.id |
| 1985 | + ) |
| 1986 | + self.logger.info("ACL2 Egress rule: All Allow") |
| 1987 | + |
| 1988 | + # ============================================================== |
| 1989 | + # B. Test Public IP access with ACL enforcement (via PF) |
| 1990 | + # ============================================================== |
| 1991 | + self.logger.info("--- Sub-test B: Test public IP access with ACLs ---") |
| 1992 | + |
| 1993 | + # Create public IP and PF for tier1 (should block SSH due to ACL1) |
| 1994 | + ip1 = PublicIPAddress.create( |
| 1995 | + self.apiclient, |
| 1996 | + accountid=account.name, |
| 1997 | + zoneid=self.zone.id, |
| 1998 | + domainid=account.domainid, |
| 1999 | + networkid=tier1.id, |
| 2000 | + vpcid=vpc.id |
| 2001 | + ) |
| 2002 | + tier1_public_ip = ip1.ipaddress.ipaddress |
| 2003 | + self.logger.info("Tier1 public IP allocated: %s", tier1_public_ip) |
| 2004 | + |
| 2005 | + pf_rule1 = NATRule.create( |
| 2006 | + self.apiclient, vm1, |
| 2007 | + {"privateport": 22, "publicport": 22, "protocol": "TCP"}, |
| 2008 | + ipaddressid=ip1.ipaddress.id, |
| 2009 | + networkid=tier1.id, |
| 2010 | + vpcid=vpc.id |
| 2011 | + ) |
| 2012 | + self.assertIsNotNone(pf_rule1) |
| 2013 | + self.logger.info("Tier1 PF rule created: %s:22 → VM1:22", tier1_public_ip) |
| 2014 | + |
| 2015 | + # SSH to tier1 should fail due to ACL denying SSH |
| 2016 | + self._assert_vm_ssh_not_accessible( |
| 2017 | + tier1_public_ip, 22, |
| 2018 | + "SSH to tier1 %s should FAIL (ACL denies SSH)" % tier1_public_ip) |
| 2019 | + self.logger.info("Verified: SSH to tier1 correctly blocked by ACL") |
| 2020 | + |
| 2021 | + # Create public IP and PF for tier2 (should allow SSH due to ACL2) |
| 2022 | + ip2 = PublicIPAddress.create( |
| 2023 | + self.apiclient, |
| 2024 | + accountid=account.name, |
| 2025 | + zoneid=self.zone.id, |
| 2026 | + domainid=account.domainid, |
| 2027 | + networkid=tier2.id, |
| 2028 | + vpcid=vpc.id |
| 2029 | + ) |
| 2030 | + tier2_public_ip = ip2.ipaddress.ipaddress |
| 2031 | + self.logger.info("Tier2 public IP allocated: %s", tier2_public_ip) |
| 2032 | + |
| 2033 | + pf_rule2 = NATRule.create( |
| 2034 | + self.apiclient, vm2, |
| 2035 | + {"privateport": 22, "publicport": 22, "protocol": "TCP"}, |
| 2036 | + ipaddressid=ip2.ipaddress.id, |
| 2037 | + networkid=tier2.id, |
| 2038 | + vpcid=vpc.id |
| 2039 | + ) |
| 2040 | + self.assertIsNotNone(pf_rule2) |
| 2041 | + self.logger.info("Tier2 PF rule created: %s:22 → VM2:22", tier2_public_ip) |
| 2042 | + |
| 2043 | + # SSH to tier2 should succeed due to ACL allowing SSH |
| 2044 | + self._assert_vm_ssh_accessible( |
| 2045 | + tier2_public_ip, 22, |
| 2046 | + "SSH to tier2 %s should succeed (ACL allows SSH)" % tier2_public_ip) |
| 2047 | + self.logger.info("Verified: SSH to tier2 correctly allowed by ACL") |
| 2048 | + |
| 2049 | + # ============================================================== |
| 2050 | + # C. Test inter-tier ICMP communication |
| 2051 | + # ============================================================== |
| 2052 | + self.logger.info("--- Sub-test C: Test inter-tier ICMP (ping) ---") |
| 2053 | + |
| 2054 | + # From VM2 (tier2), ping VM1 (tier1) — should succeed (both allow ICMP egress) |
| 2055 | + # This tests that the ACL egress rules work and inter-tier routing is OK |
| 2056 | + try: |
| 2057 | + ssh_vm2 = SshClient( |
| 2058 | + tier2_public_ip, 22, "ubuntu", None, |
| 2059 | + keyPairFiles=self._ssh_private_key_file, |
| 2060 | + timeout=30, retries=10 |
| 2061 | + ) |
| 2062 | + out = ssh_vm2.execute("ping -c 3 %s 2>&1 | tail -5" % vm1_ip) |
| 2063 | + ping_result = "\n".join(out) |
| 2064 | + # Check if ping succeeded (look for "3 packets transmitted" or similar) |
| 2065 | + if "transmitted" in ping_result.lower(): |
| 2066 | + self.logger.info("Ping from VM2 to VM1 output:\n%s", ping_result) |
| 2067 | + if "0 received" not in ping_result.lower(): |
| 2068 | + self.logger.info("Verified: Inter-tier ping succeeded (ICMP egress rule allows it)") |
| 2069 | + else: |
| 2070 | + self.logger.warning("Ping packets were transmitted but all lost") |
| 2071 | + else: |
| 2072 | + self.logger.warning("Could not determine ping result from: %s", ping_result) |
| 2073 | + except Exception as e: |
| 2074 | + self.logger.warning("Could not execute ping test: %s", e) |
| 2075 | + |
| 2076 | + # ============================================================== |
| 2077 | + # D. Additional SSH verification |
| 2078 | + # ============================================================== |
| 2079 | + self.logger.info("--- Sub-test D: Additional SSH verification ---") |
| 2080 | + |
| 2081 | + # Re-verify tier2 SSH works after ACL rules are fully active |
| 2082 | + self._assert_vm_ssh_accessible( |
| 2083 | + tier2_public_ip, 22, |
| 2084 | + "SSH to tier2 should still work (ACL permits SSH)") |
| 2085 | + self.logger.info("Verified: SSH to tier2 confirmed working") |
| 2086 | + |
| 2087 | + # ============================================================== |
| 2088 | + # E. Cleanup |
| 2089 | + # ============================================================== |
| 2090 | + self.logger.info("--- Sub-test E: Cleanup ---") |
| 2091 | + pf_rule1.delete(self.apiclient) |
| 2092 | + ip1.delete(self.apiclient) |
| 2093 | + pf_rule2.delete(self.apiclient) |
| 2094 | + ip2.delete(self.apiclient) |
| 2095 | + |
| 2096 | + vm1.delete(self.apiclient, expunge=True) |
| 2097 | + self.cleanup = [o for o in self.cleanup if o != vm1] |
| 2098 | + vm2.delete(self.apiclient, expunge=True) |
| 2099 | + self.cleanup = [o for o in self.cleanup if o != vm2] |
| 2100 | + tier1.delete(self.apiclient) |
| 2101 | + self.cleanup = [o for o in self.cleanup if o != tier1] |
| 2102 | + tier2.delete(self.apiclient) |
| 2103 | + self.cleanup = [o for o in self.cleanup if o != tier2] |
| 2104 | + |
| 2105 | + vpc.delete(self.apiclient) |
| 2106 | + self.cleanup = [o for o in self.cleanup if o != vpc] |
| 2107 | + |
| 2108 | + self._teardown_extension() |
| 2109 | + self.logger.info("test_07 PASSED") |
| 2110 | + |
0 commit comments