Skip to content

Commit fbc6c41

Browse files
Added CACL entry for Redfish (#380)
What: Adds a REDFISH entry (tcp/443) to caclmgrd's ACL_SERVICES and gates it on FEATURE.redfish — init-time seed, walker skip when disabled, and a FEATURE subscriber that flips the flag and triggers a re-walk. Includes unit tests for the walker (v4+v6, enabled+disabled) and the handler branches. Why: Without a REDFISH registration, operator ACL_TABLE entries referencing REDFISH were silently dropped as "unrecognized service", leaving the BMC's tcp/443 (bmcweb in docker-sonic-redfish) ungoverned by CACL. How: Mirrors existing SSH/SNMP/NTP entries in ACL_SERVICES; introduces RedfishAllowed flag analogous to bfdAllowed/VxlanAllowed; subscribes to FEATURE table to converge iptables on redfish enable/disable. On non-BMC platforms a stray REDFISH template is a no-op (warn + ignore). Testing: Azure CI green (build + sonic-net.sonic-host-services), Analyze/DCO/EasyCLA/Semgrep pass. New unit tests cover v4+v6 enabled (rules emitted) and disabled (rules skipped) states plus handler toggles. Companion buildimage PR sonic-net/sonic-buildimage#27349. Signed-off-by: shreyansh-nexthop <shreyansh@nexthop.ai>
1 parent 5a370b6 commit fbc6c41

3 files changed

Lines changed: 354 additions & 1 deletion

File tree

scripts/caclmgrd

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ class ControlPlaneAclManager(logger.Logger):
108108
"dst_ports": ["22"],
109109
"multi_asic_ns_to_host_fwd":True
110110
},
111+
"REDFISH": {
112+
"ip_protocols": ["tcp"],
113+
"dst_ports": ["443"],
114+
"multi_asic_ns_to_host_fwd":False
115+
},
111116
"EXTERNAL_CLIENT": {
112117
"ip_protocols": ["tcp"],
113118
"multi_asic_ns_to_host_fwd":False
@@ -126,6 +131,7 @@ class ControlPlaneAclManager(logger.Logger):
126131
bfdAllowed = False
127132
VxlanAllowed = False
128133
VxlanSrcIP = ""
134+
RedfishAllowed = False
129135
# a map from dpu name to port
130136
dashHaPortMap = {}
131137

@@ -162,6 +168,9 @@ class ControlPlaneAclManager(logger.Logger):
162168
self.feature_present = {}
163169
self.update_feature_present()
164170

171+
feature_tb = self.config_db_map[DEFAULT_NAMESPACE].get_table(self.FEATURE_TABLE)
172+
self.RedfishAllowed = feature_tb.get("redfish", {}).get("state") == "enabled"
173+
165174
metadata = self.config_db_map[DEFAULT_NAMESPACE].get_table(self.DEVICE_METADATA_TABLE)
166175
if 'subtype' in metadata['localhost'] and metadata['localhost']['subtype'] == 'DualToR':
167176
self.DualToR = True
@@ -751,6 +760,11 @@ class ControlPlaneAclManager(logger.Logger):
751760
.format(table_name, acl_service))
752761
continue
753762

763+
if acl_service == "REDFISH" and not self.RedfishAllowed:
764+
self.log_warning("Ignoring control plane ACL '{}': REDFISH service is not enabled on this platform"
765+
.format(table_name))
766+
continue
767+
754768
self.log_info("Translating ACL rules for control plane ACL '{}' (service: '{}')"
755769
.format(table_name, acl_service))
756770

@@ -1057,6 +1071,36 @@ class ControlPlaneAclManager(logger.Logger):
10571071
self.VxlanSrcIP = ""
10581072
return True
10591073

1074+
def allow_redfish(self):
1075+
self.RedfishAllowed = True
1076+
self.log_info("Enabled REDFISH control plane ACL handling")
1077+
1078+
def block_redfish(self):
1079+
self.RedfishAllowed = False
1080+
self.log_info("Disabled REDFISH control plane ACL handling")
1081+
1082+
def handle_redfish_feature_events(self, subscribe_feature_table, namespace, ctrl_plane_acl_notification):
1083+
"""Drain FEATURE table events and toggle REDFISH control plane ACL handling.
1084+
1085+
REDFISH iptables rules are only emitted when FEATURE.redfish is enabled. On a
1086+
state change, flip RedfishAllowed and request a re-walk so any existing REDFISH
1087+
ACL rules get (de)programmed immediately.
1088+
"""
1089+
while True:
1090+
key, op, fvs = subscribe_feature_table.pop()
1091+
if not key:
1092+
break
1093+
if key != "redfish":
1094+
continue
1095+
new_state = (op == 'SET' and dict(fvs).get("state") == "enabled")
1096+
if new_state == self.RedfishAllowed:
1097+
continue
1098+
if new_state:
1099+
self.allow_redfish()
1100+
else:
1101+
self.block_redfish()
1102+
ctrl_plane_acl_notification.add(namespace)
1103+
10601104
def remove_dash_ha_rules(self, namespace, port):
10611105
iptables_cmds = []
10621106
# Remove iptables/ip6tables commands that allow DASH-HA packets
@@ -1162,6 +1206,9 @@ class ControlPlaneAclManager(logger.Logger):
11621206

11631207
subscribe_dpu_table = swsscommon.SubscriberStateTable(config_db_connector, self.DPU_TABLE)
11641208
sel.addSelectable(subscribe_dpu_table)
1209+
1210+
subscribe_feature_table = swsscommon.SubscriberStateTable(config_db_connector, self.FEATURE_TABLE)
1211+
sel.addSelectable(subscribe_feature_table)
11651212
# Map of Namespace <--> susbcriber table's object
11661213
config_db_subscriber_table_map = {}
11671214

@@ -1248,6 +1295,8 @@ class ControlPlaneAclManager(logger.Logger):
12481295
self.update_dhcp_acl(key, op, dict(fvs), mark)
12491296
continue
12501297

1298+
ctrl_plane_acl_notification = set()
1299+
12511300
if db_id == config_db_id:
12521301
while True:
12531302
key, op, fvs = subscribe_vxlan_table.pop()
@@ -1265,7 +1314,7 @@ class ControlPlaneAclManager(logger.Logger):
12651314
if "dash-ha" in self.feature_present:
12661315
self.update_dash_ha_rules(namespace, key, op, fvs)
12671316

1268-
ctrl_plane_acl_notification = set()
1317+
self.handle_redfish_feature_events(subscribe_feature_table, namespace, ctrl_plane_acl_notification)
12691318

12701319
# Pop data of both Subscriber Table object of namespace that got config db acl table event
12711320
for table in config_db_subscriber_table_map[namespace]:
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import os
2+
import sys
3+
4+
from swsscommon import swsscommon
5+
from parameterized import parameterized
6+
from sonic_py_common.general import load_module_from_source
7+
from unittest import TestCase, mock
8+
from pyfakefs.fake_filesystem_unittest import patchfs
9+
10+
from .test_redfish_acl_vectors import REDFISH_ACL_TEST_VECTOR, REDFISH_ACL_DISABLED_FEATURE_VECTORS
11+
from tests.common.mock_configdb import MockConfigDb
12+
13+
14+
DBCONFIG_PATH = '/var/run/redis/sonic-db/database_config.json'
15+
16+
17+
class TestCaclmgrdRedfishAcl(TestCase):
18+
"""
19+
Verifies that an ACL_TABLE referencing the REDFISH service produces
20+
iptables rules on tcp/443. Guards against the ACL_SERVICES["REDFISH"]
21+
entry being removed or renamed.
22+
"""
23+
def setUp(self):
24+
swsscommon.ConfigDBConnector = MockConfigDb
25+
test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26+
modules_path = os.path.dirname(test_path)
27+
scripts_path = os.path.join(modules_path, "scripts")
28+
sys.path.insert(0, modules_path)
29+
caclmgrd_path = os.path.join(scripts_path, 'caclmgrd')
30+
self.caclmgrd = load_module_from_source('caclmgrd', caclmgrd_path)
31+
32+
@parameterized.expand(REDFISH_ACL_TEST_VECTOR)
33+
@patchfs
34+
def test_caclmgrd_redfish_acl(self, test_name, test_data, fs):
35+
if not os.path.exists(DBCONFIG_PATH):
36+
fs.create_file(DBCONFIG_PATH)
37+
38+
MockConfigDb.set_config_db(test_data["config_db"])
39+
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock()
40+
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock()
41+
self.caclmgrd.ControlPlaneAclManager.generate_block_ip2me_traffic_iptables_commands = mock.MagicMock(return_value=[])
42+
self.caclmgrd.ControlPlaneAclManager.get_chain_list = mock.MagicMock(return_value=["INPUT", "FORWARD", "OUTPUT"])
43+
self.caclmgrd.ControlPlaneAclManager.get_chassis_midplane_interface_ip = mock.MagicMock(return_value='')
44+
caclmgrd_daemon = self.caclmgrd.ControlPlaneAclManager("caclmgrd")
45+
46+
self.assertTrue(caclmgrd_daemon.RedfishAllowed,
47+
"Seed at __init__ should set RedfishAllowed=True for these vectors")
48+
49+
self.assertIn("REDFISH", caclmgrd_daemon.ACL_SERVICES,
50+
"ACL_SERVICES is missing the 'REDFISH' entry — see scripts/caclmgrd")
51+
self.assertEqual(caclmgrd_daemon.ACL_SERVICES["REDFISH"]["dst_ports"], ["443"])
52+
self.assertEqual(caclmgrd_daemon.ACL_SERVICES["REDFISH"]["ip_protocols"], ["tcp"])
53+
54+
iptables_rules_ret, _ = caclmgrd_daemon.get_acl_rules_and_translate_to_iptables_commands('', MockConfigDb())
55+
test_data['return'] = [tuple(i) for i in test_data['return']]
56+
iptables_rules_ret = [tuple(i) for i in iptables_rules_ret]
57+
self.assertEqual(set(test_data["return"]).issubset(set(iptables_rules_ret)), True,
58+
"Expected iptables rules not produced. Got: {}".format(iptables_rules_ret))
59+
60+
@parameterized.expand(REDFISH_ACL_DISABLED_FEATURE_VECTORS)
61+
@patchfs
62+
def test_caclmgrd_redfish_acl_skipped_when_feature_disabled(self, test_name, test_data, fs):
63+
"""
64+
REDFISH only ships on BMC-equipped platforms. When the redfish FEATURE
65+
is "disabled", RedfishAllowed stays False and an operator template
66+
referencing the REDFISH service must not program any tcp/443 iptables
67+
rules -- otherwise it would silently govern whatever else (if anything)
68+
listens on 443. Parametrized over IPv4 and IPv6 to confirm the gate
69+
skips REDFISH regardless of IP family.
70+
"""
71+
if not os.path.exists(DBCONFIG_PATH):
72+
fs.create_file(DBCONFIG_PATH)
73+
74+
MockConfigDb.set_config_db(test_data["config_db"])
75+
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock()
76+
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock()
77+
self.caclmgrd.ControlPlaneAclManager.generate_block_ip2me_traffic_iptables_commands = mock.MagicMock(return_value=[])
78+
self.caclmgrd.ControlPlaneAclManager.get_chain_list = mock.MagicMock(return_value=["INPUT", "FORWARD", "OUTPUT"])
79+
self.caclmgrd.ControlPlaneAclManager.get_chassis_midplane_interface_ip = mock.MagicMock(return_value='')
80+
caclmgrd_daemon = self.caclmgrd.ControlPlaneAclManager("caclmgrd")
81+
82+
self.assertFalse(caclmgrd_daemon.RedfishAllowed,
83+
"Seed at __init__ should set RedfishAllowed=False when FEATURE.redfish.state=disabled")
84+
85+
iptables_rules_ret, _ = caclmgrd_daemon.get_acl_rules_and_translate_to_iptables_commands('', MockConfigDb())
86+
87+
port_443_rules = [r for r in iptables_rules_ret if "--dport" in r and "443" in r]
88+
self.assertEqual(port_443_rules, test_data["return"],
89+
"REDFISH ACL should be skipped when RedfishAllowed=False, "
90+
"but found tcp/443 rules: {}".format(port_443_rules))
91+
92+
@patchfs
93+
def test_handle_redfish_feature_events(self, fs):
94+
"""
95+
Drive handle_redfish_feature_events (the FEATURE-table subscription
96+
handler) directly, covering every branch: enable transition, disable
97+
transition, non-redfish event ignored, and same-state no-op.
98+
"""
99+
if not os.path.exists(DBCONFIG_PATH):
100+
fs.create_file(DBCONFIG_PATH)
101+
102+
MockConfigDb.set_config_db({
103+
"DEVICE_METADATA": {"localhost": {}},
104+
"FEATURE": {"redfish": {"state": "disabled"}},
105+
})
106+
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock()
107+
self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock()
108+
self.caclmgrd.ControlPlaneAclManager.generate_block_ip2me_traffic_iptables_commands = mock.MagicMock(return_value=[])
109+
self.caclmgrd.ControlPlaneAclManager.get_chain_list = mock.MagicMock(return_value=["INPUT", "FORWARD", "OUTPUT"])
110+
self.caclmgrd.ControlPlaneAclManager.get_chassis_midplane_interface_ip = mock.MagicMock(return_value='')
111+
caclmgrd_daemon = self.caclmgrd.ControlPlaneAclManager("caclmgrd")
112+
self.assertFalse(caclmgrd_daemon.RedfishAllowed)
113+
114+
def sub_with(events):
115+
sub = mock.MagicMock()
116+
sub.pop.side_effect = events + [("", None, None)]
117+
return sub
118+
119+
# enable: flag flips True and namespace queued for re-walk
120+
notif = set()
121+
caclmgrd_daemon.handle_redfish_feature_events(
122+
sub_with([("redfish", "SET", (("state", "enabled"),))]), "", notif)
123+
self.assertTrue(caclmgrd_daemon.RedfishAllowed)
124+
self.assertIn("", notif)
125+
126+
# disable: flag flips False and namespace queued
127+
notif = set()
128+
caclmgrd_daemon.handle_redfish_feature_events(
129+
sub_with([("redfish", "SET", (("state", "disabled"),))]), "", notif)
130+
self.assertFalse(caclmgrd_daemon.RedfishAllowed)
131+
self.assertIn("", notif)
132+
133+
# non-redfish FEATURE event: ignored, nothing queued
134+
notif = set()
135+
caclmgrd_daemon.handle_redfish_feature_events(
136+
sub_with([("snmp", "SET", (("state", "enabled"),))]), "", notif)
137+
self.assertFalse(caclmgrd_daemon.RedfishAllowed)
138+
self.assertEqual(notif, set())
139+
140+
# same-state event (already disabled): no-op, nothing queued
141+
notif = set()
142+
caclmgrd_daemon.handle_redfish_feature_events(
143+
sub_with([("redfish", "SET", (("state", "disabled"),))]), "", notif)
144+
self.assertFalse(caclmgrd_daemon.RedfishAllowed)
145+
self.assertEqual(notif, set())

0 commit comments

Comments
 (0)