diff --git a/scripts/caclmgrd b/scripts/caclmgrd index ef31da1f..258991e3 100755 --- a/scripts/caclmgrd +++ b/scripts/caclmgrd @@ -733,6 +733,15 @@ class ControlPlaneAclManager(logger.Logger): iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-t', 'raw', '-A', 'PREROUTING', '-p', 'ipv6-icmp', '-j', 'NOTRACK']) iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-t', 'raw', '-A', 'OUTPUT', '-p', 'ipv6-icmp', '-j', 'NOTRACK']) + for k, v in self.ACL_SERVICES.items(): + if namespace != DEFAULT_NAMESPACE and v["multi_asic_ns_to_host_fwd"]: + for ip_protocol in v["ip_protocols"]: + for dst_port in v["dst_ports"]: + iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + + ['ip6tables', '-A', 'FORWARD', '-p', str(ip_protocol), '-s', self.namespace_mgmt_ipv6, '--sport', dst_port, '-j', 'ACCEPT']) + iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + + ['iptables', '-A', 'FORWARD', '-p', str(ip_protocol), '-s', self.namespace_mgmt_ip, '--sport', dst_port, '-j', 'ACCEPT']) + # Get current ACL tables and rules from Config DB self._tables_db_info = config_db_connector.get_table(self.ACL_TABLE) @@ -848,7 +857,10 @@ class ControlPlaneAclManager(logger.Logger): for dst_port in dst_ports: rule_cmd = ["ip6tables"] if table_ip_version == 6 else ["iptables"] - rule_cmd += ["-A", "INPUT"] + if namespace != DEFAULT_NAMESPACE and self.ACL_SERVICES[acl_service]["multi_asic_ns_to_host_fwd"]: + rule_cmd += ["-A", "FORWARD"] + else: + rule_cmd += ["-A", "INPUT"] if ip_protocol != "any": rule_cmd += ["-p", str(ip_protocol)] @@ -912,6 +924,9 @@ class ControlPlaneAclManager(logger.Logger): if num_ctrl_plane_acl_rules > 0: iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'INPUT', '-j', 'DROP']) iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-A', 'INPUT', '-j', 'DROP']) + if namespace != DEFAULT_NAMESPACE: + iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'FORWARD', '-j', 'DROP']) + iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-A', 'FORWARD', '-j', 'DROP']) return iptables_cmds, service_to_source_ip_map diff --git a/tests/caclmgrd/caclmgrd_gil_ip_multiasic_test.py b/tests/caclmgrd/caclmgrd_gil_ip_multiasic_test.py new file mode 100644 index 00000000..f826b961 --- /dev/null +++ b/tests/caclmgrd/caclmgrd_gil_ip_multiasic_test.py @@ -0,0 +1,84 @@ +import os +import sys + +from swsscommon import swsscommon +from parameterized import parameterized +from sonic_py_common.general import load_module_from_source +from unittest import TestCase, mock +from pyfakefs.fake_filesystem_unittest import patchfs + +from .test_gil_ip_multiasic_vectors import ( + CACLMGRD_GIL_IP_MULTIASIC_TEST_VECTOR, + NAMESPACE_MGMT_IP, + NAMESPACE_MGMT_IPV6, + ASIC0_NS_PREFIX, +) +from tests.common.mock_configdb import MockConfigDb + + +DBCONFIG_PATH = '/var/run/redis/sonic-db/database_config.json' + + +class TestCaclmgrdGilIpMultiAsic(TestCase): + """ + Test caclmgrd GIL IP multi-asic FORWARD chain rules. + + Verifies that for non-default namespaces, connections from the GIL + (Global In-band Link) management IP are allowed/blocked via the + FORWARD iptables chain for services with multi_asic_ns_to_host_fwd=True. + """ + + def setUp(self): + swsscommon.ConfigDBConnector = MockConfigDb + test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + modules_path = os.path.dirname(test_path) + scripts_path = os.path.join(modules_path, "scripts") + sys.path.insert(0, modules_path) + caclmgrd_path = os.path.join(scripts_path, 'caclmgrd') + self.caclmgrd = load_module_from_source('caclmgrd', caclmgrd_path) + self.maxDiff = None + + def _make_daemon(self): + """Create a daemon instance with mocked network helpers.""" + self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock(return_value=NAMESPACE_MGMT_IP) + self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock(return_value=NAMESPACE_MGMT_IPV6) + self.caclmgrd.ControlPlaneAclManager.generate_block_ip2me_traffic_iptables_commands = mock.MagicMock(return_value=[]) + self.caclmgrd.ControlPlaneAclManager.get_chain_list = mock.MagicMock(return_value=["INPUT", "FORWARD", "OUTPUT"]) + self.caclmgrd.ControlPlaneAclManager.get_chassis_midplane_interface_ip = mock.MagicMock(return_value='') + daemon = self.caclmgrd.ControlPlaneAclManager("caclmgrd") + # Wire up asic0 namespace + daemon.iptables_cmd_ns_prefix['asic0'] = ASIC0_NS_PREFIX + daemon.namespace_mgmt_ip = NAMESPACE_MGMT_IP + daemon.namespace_mgmt_ipv6 = NAMESPACE_MGMT_IPV6 + daemon.namespace_docker_mgmt_ip['asic0'] = '1.1.1.1' + daemon.namespace_docker_mgmt_ipv6['asic0'] = 'fd::01' + return daemon + + @parameterized.expand(CACLMGRD_GIL_IP_MULTIASIC_TEST_VECTOR) + @patchfs + def test_caclmgrd_gil_ip_multiasic(self, test_name, test_data, fs): + if not os.path.exists(DBCONFIG_PATH): + fs.create_file(DBCONFIG_PATH) + + MockConfigDb.set_config_db(test_data["config_db"]) + daemon = self._make_daemon() + namespace = test_data["namespace"] + + iptables_rules, _ = daemon.get_acl_rules_and_translate_to_iptables_commands( + namespace, MockConfigDb() + ) + + # Convert to tuples for set operations + rules_set = set(tuple(r) for r in iptables_rules) + + for rule in test_data.get("expected_present", []): + self.assertIn( + tuple(rule), rules_set, + msg=f"[{test_name}] Expected rule missing: {rule}" + ) + + for rule in test_data.get("expected_absent", []): + self.assertNotIn( + tuple(rule), rules_set, + msg=f"[{test_name}] Unexpected rule found: {rule}" + ) diff --git a/tests/caclmgrd/test_gil_ip_multiasic_vectors.py b/tests/caclmgrd/test_gil_ip_multiasic_vectors.py new file mode 100644 index 00000000..1183e48c --- /dev/null +++ b/tests/caclmgrd/test_gil_ip_multiasic_vectors.py @@ -0,0 +1,206 @@ +from unittest.mock import call + +""" + caclmgrd test vectors for GIL IP multi-asic FORWARD chain rules. + + These tests verify that for non-default namespaces (e.g. asic0): + 1. FORWARD ACCEPT rules are inserted for SSH/SNMP (multi_asic_ns_to_host_fwd=True) + sourced from the namespace management IPs (GIL IPs). + 2. ACL rules for SSH/SNMP use the FORWARD chain instead of INPUT. + 3. FORWARD DROP rules are appended after INPUT DROP when ACL rules exist. + 4. Default namespace (empty string) still uses INPUT chain (no regression). +""" + +NAMESPACE_MGMT_IP = "10.1.0.1" +NAMESPACE_MGMT_IPV6 = "fc00::1" +ASIC0_NS_PREFIX = ['ip', 'netns', 'exec', 'asic0'] + +# --------------------------------------------------------------------------- +# Helper: wrap a rule list with the asic0 namespace prefix +# --------------------------------------------------------------------------- +def _ns(rule): + return ASIC0_NS_PREFIX + rule + + +CACLMGRD_GIL_IP_MULTIASIC_TEST_VECTOR = [ + # ------------------------------------------------------------------ + # 1. Non-default namespace: GIL FORWARD ACCEPT rules generated for + # SSH and SNMP even when no ACL tables are configured. + # ------------------------------------------------------------------ + [ + "Non-default namespace: GIL FORWARD ACCEPT rules added for SSH and SNMP", + { + "namespace": "asic0", + "config_db": { + "ACL_TABLE": {}, + "ACL_RULE": {}, + "DEVICE_METADATA": {"localhost": {}}, + "FEATURE": {}, + }, + # Rules that MUST be present in the output + "expected_present": [ + _ns(['ip6tables', '-A', 'FORWARD', '-p', 'tcp', '-s', NAMESPACE_MGMT_IPV6, '--sport', '22', '-j', 'ACCEPT']), + _ns(['iptables', '-A', 'FORWARD', '-p', 'tcp', '-s', NAMESPACE_MGMT_IP, '--sport', '22', '-j', 'ACCEPT']), + _ns(['ip6tables', '-A', 'FORWARD', '-p', 'tcp', '-s', NAMESPACE_MGMT_IPV6, '--sport', '161', '-j', 'ACCEPT']), + _ns(['iptables', '-A', 'FORWARD', '-p', 'tcp', '-s', NAMESPACE_MGMT_IP, '--sport', '161', '-j', 'ACCEPT']), + _ns(['ip6tables', '-A', 'FORWARD', '-p', 'udp', '-s', NAMESPACE_MGMT_IPV6, '--sport', '161', '-j', 'ACCEPT']), + _ns(['iptables', '-A', 'FORWARD', '-p', 'udp', '-s', NAMESPACE_MGMT_IP, '--sport', '161', '-j', 'ACCEPT']), + ], + # Rules that must NOT be present (NTP has multi_asic_ns_to_host_fwd=False) + "expected_absent": [ + _ns(['iptables', '-A', 'FORWARD', '-p', 'udp', '-s', NAMESPACE_MGMT_IP, '--sport', '123', '-j', 'ACCEPT']), + _ns(['ip6tables', '-A', 'FORWARD', '-p', 'udp', '-s', NAMESPACE_MGMT_IPV6, '--sport', '123', '-j', 'ACCEPT']), + ], + } + ], + + # ------------------------------------------------------------------ + # 2. Non-default namespace: ACL rules for SSH use FORWARD chain. + # ------------------------------------------------------------------ + [ + "Non-default namespace: SSH ACL rule uses FORWARD chain", + { + "namespace": "asic0", + "config_db": { + "ACL_TABLE": { + "SSH_ONLY": { + "stage": "INGRESS", + "type": "CTRLPLANE", + "services": ["SSH"], + } + }, + "ACL_RULE": { + "SSH_ONLY|RULE_1": { + "PACKET_ACTION": "ACCEPT", + "PRIORITY": "9999", + "SRC_IP": "192.168.1.0/24", + }, + }, + "DEVICE_METADATA": {"localhost": {}}, + "FEATURE": {}, + }, + "expected_present": [ + _ns(['iptables', '-A', 'FORWARD', '-p', 'tcp', '-s', '192.168.1.0/24', '--dport', '22', '-j', 'ACCEPT']), + # FORWARD DROP added because num_ctrl_plane_acl_rules > 0 + _ns(['iptables', '-A', 'FORWARD', '-j', 'DROP']), + _ns(['ip6tables', '-A', 'FORWARD', '-j', 'DROP']), + ], + "expected_absent": [ + # Must NOT use INPUT for SSH in non-default namespace + _ns(['iptables', '-A', 'INPUT', '-p', 'tcp', '-s', '192.168.1.0/24', '--dport', '22', '-j', 'ACCEPT']), + ], + } + ], + + # ------------------------------------------------------------------ + # 3. Non-default namespace: SNMP ACL rule uses FORWARD chain. + # ------------------------------------------------------------------ + [ + "Non-default namespace: SNMP ACL rule uses FORWARD chain", + { + "namespace": "asic0", + "config_db": { + "ACL_TABLE": { + "SNMP_ACL": { + "stage": "INGRESS", + "type": "CTRLPLANE", + "services": ["SNMP"], + } + }, + "ACL_RULE": { + "SNMP_ACL|RULE_1": { + "PACKET_ACTION": "ACCEPT", + "PRIORITY": "9999", + "SRC_IP": "10.0.0.0/8", + }, + }, + "DEVICE_METADATA": {"localhost": {}}, + "FEATURE": {}, + }, + "expected_present": [ + _ns(['iptables', '-A', 'FORWARD', '-p', 'tcp', '-s', '10.0.0.0/8', '--dport', '161', '-j', 'ACCEPT']), + _ns(['iptables', '-A', 'FORWARD', '-p', 'udp', '-s', '10.0.0.0/8', '--dport', '161', '-j', 'ACCEPT']), + _ns(['iptables', '-A', 'FORWARD', '-j', 'DROP']), + _ns(['ip6tables', '-A', 'FORWARD', '-j', 'DROP']), + ], + "expected_absent": [ + _ns(['iptables', '-A', 'INPUT', '-p', 'tcp', '-s', '10.0.0.0/8', '--dport', '161', '-j', 'ACCEPT']), + ], + } + ], + + # ------------------------------------------------------------------ + # 4. Non-default namespace: NTP ACL rule still uses INPUT chain + # (multi_asic_ns_to_host_fwd=False for NTP). + # ------------------------------------------------------------------ + [ + "Non-default namespace: NTP ACL rule still uses INPUT chain", + { + "namespace": "asic0", + "config_db": { + "ACL_TABLE": { + "NTP_ACL": { + "stage": "INGRESS", + "type": "CTRLPLANE", + "services": ["NTP"], + } + }, + "ACL_RULE": { + "NTP_ACL|RULE_1": { + "PACKET_ACTION": "ACCEPT", + "PRIORITY": "9999", + "SRC_IP": "10.0.0.1/32", + }, + }, + "DEVICE_METADATA": {"localhost": {}}, + "FEATURE": {}, + }, + "expected_present": [ + _ns(['iptables', '-A', 'INPUT', '-p', 'udp', '-s', '10.0.0.1/32', '--dport', '123', '-j', 'ACCEPT']), + ], + "expected_absent": [ + _ns(['iptables', '-A', 'FORWARD', '-p', 'udp', '-s', '10.0.0.1/32', '--dport', '123', '-j', 'ACCEPT']), + ], + } + ], + + # ------------------------------------------------------------------ + # 5. Default namespace: SSH ACL still uses INPUT chain (no regression). + # ------------------------------------------------------------------ + [ + "Default namespace: SSH ACL rule uses INPUT chain (no regression)", + { + "namespace": "", + "config_db": { + "ACL_TABLE": { + "SSH_ONLY": { + "stage": "INGRESS", + "type": "CTRLPLANE", + "services": ["SSH"], + } + }, + "ACL_RULE": { + "SSH_ONLY|RULE_1": { + "PACKET_ACTION": "ACCEPT", + "PRIORITY": "9999", + "SRC_IP": "192.168.1.0/24", + }, + }, + "DEVICE_METADATA": {"localhost": {}}, + "FEATURE": {}, + }, + "expected_present": [ + ['iptables', '-A', 'INPUT', '-p', 'tcp', '-s', '192.168.1.0/24', '--dport', '22', '-j', 'ACCEPT'], + ['iptables', '-A', 'INPUT', '-j', 'DROP'], + ['ip6tables', '-A', 'INPUT', '-j', 'DROP'], + ], + "expected_absent": [ + # No FORWARD DROP for default namespace + ['iptables', '-A', 'FORWARD', '-j', 'DROP'], + ['ip6tables', '-A', 'FORWARD', '-j', 'DROP'], + # GIL FORWARD ACCEPT rules must not appear for default namespace + ['iptables', '-A', 'FORWARD', '-p', 'tcp', '-s', NAMESPACE_MGMT_IP, '--sport', '22', '-j', 'ACCEPT'], + ], + } + ], +]