-
Notifications
You must be signed in to change notification settings - Fork 8
[202608] [caclmgrd] Own the redfish docker0 syslog INPUT exception #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 202608
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -670,9 +670,11 @@ class ControlPlaneAclManager(logger.Logger): | |
| iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'INPUT', '-s', '127.0.0.1', '-i', 'lo', '-j', 'ACCEPT']) | ||
| iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-A', 'INPUT', '-s', '::1', '-i', 'lo', '-j', 'ACCEPT']) | ||
|
|
||
| # dhcp_server docker0 syslog (RELP tcp/2514) exception; host namespace / IPv4 only | ||
| if namespace == DEFAULT_NAMESPACE and self.DhcpServerSyslogAllowed: | ||
| iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'INPUT', '-i', 'docker0', '-p', 'tcp', '--dport', '2514', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'dhcp_server_syslog']) | ||
| # bridged-container docker0 syslog (RELP tcp/2514) exceptions; host namespace / IPv4 only | ||
| if namespace == DEFAULT_NAMESPACE: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The set of bridged-syslog features is now spelled out in four places: this tuple, the Adding a third bridged container means four coordinated edits, and missing any one silently yields a feature whose flag flips but whose rule never appears. A single module-level |
||
| for feature, allowed in (("redfish", self.RedfishAllowed), ("dhcp_server", self.DhcpServerSyslogAllowed)): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggest widening the comparison in all three places: state = feature_tb.get("redfish", {}).get("state")
self.RedfishAllowed = state in ("enabled", "always_enabled")Worth a test case too — every test in the new file uses |
||
| if allowed: | ||
| iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'INPUT', '-i', 'docker0', '-p', 'tcp', '--dport', '2514', '-j', 'ACCEPT', '-m', 'comment', '--comment', feature + '_syslog']) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both iterations emit a byte-identical match —
Either emit a single rule when either feature is on, or make the two rules actually distinguishable. Separately: the match has no source restriction, so this opens the host rsyslog imrelp listener on tcp/2514 to every container on docker0, not just the redfish container. Scoping with |
||
|
|
||
| if self.bfdAllowed: | ||
| iptables_cmds += self.get_bfd_iptable_commands(namespace) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import os | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is a near-verbatim copy of That file already imports |
||
| import sys | ||
|
|
||
| from swsscommon import swsscommon | ||
| from sonic_py_common.general import load_module_from_source | ||
| from unittest import TestCase, mock | ||
| from pyfakefs.fake_filesystem_unittest import patchfs | ||
|
|
||
| from tests.common.mock_configdb import MockConfigDb | ||
|
|
||
|
|
||
| DBCONFIG_PATH = '/var/run/redis/sonic-db/database_config.json' | ||
|
|
||
| # Must stay byte-identical to the container-side -C check in docker_image_ctl.j2. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no |
||
| REDFISH_SYSLOG_RULE = ( | ||
| 'iptables', '-A', 'INPUT', '-i', 'docker0', '-p', 'tcp', '--dport', '2514', | ||
| '-j', 'ACCEPT', '-m', 'comment', '--comment', 'redfish_syslog', | ||
| ) | ||
|
|
||
|
|
||
| class TestCaclmgrdRedfishSyslog(TestCase): | ||
| """ | ||
| Verifies caclmgrd owns the redfish docker0 syslog (RELP tcp/2514) INPUT | ||
| exception, gated on FEATURE.redfish, and re-emits it before the | ||
| catch-all DROP on every rebuild. | ||
|
|
||
| redfish is bridge-networked, so its rsyslog forwards over RELP to the | ||
| docker0 gateway instead of 127.0.0.1. That traffic arrives on docker0 | ||
| (not lo), so it is not covered by the loopback ACCEPT and would be | ||
| swept into the control-plane catch-all DROP without this exception. | ||
| """ | ||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead here — |
||
|
|
||
| def setup_daemon(self, config_db): | ||
| MockConfigDb.set_config_db(config_db) | ||
| self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ip = mock.MagicMock() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These six assignments replace methods on the Combined with |
||
| self.caclmgrd.ControlPlaneAclManager.get_namespace_mgmt_ipv6 = mock.MagicMock() | ||
| self.caclmgrd.ControlPlaneAclManager.generate_block_ip2me_traffic_iptables_commands = mock.MagicMock(return_value=[]) | ||
| self.caclmgrd.ControlPlaneAclManager.generate_allow_internal_docker_ip_traffic_commands = mock.MagicMock(return_value=[]) | ||
| self.caclmgrd.ControlPlaneAclManager.generate_allow_internal_chasis_midplane_traffic = 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='') | ||
| return self.caclmgrd.ControlPlaneAclManager("caclmgrd") | ||
|
|
||
| @patchfs | ||
| def test_init_seeds_flag_from_feature_state(self, fs): | ||
| """RedfishAllowed is seeded from the persisted FEATURE state at __init__.""" | ||
| if not os.path.exists(DBCONFIG_PATH): | ||
| fs.create_file(DBCONFIG_PATH) | ||
|
|
||
| enabled = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {"redfish": {"state": "enabled"}}}) | ||
| disabled = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {"redfish": {"state": "disabled"}}}) | ||
| absent = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {}}) | ||
| self.assertTrue(enabled.RedfishAllowed) | ||
| self.assertFalse(disabled.RedfishAllowed) | ||
| self.assertFalse(absent.RedfishAllowed) | ||
|
|
||
| @patchfs | ||
| def test_rule_emitted_when_feature_enabled(self, fs): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing here covers the case this diff actually exists for — both Unasserted as a result: that both rules are emitted, the order between them, that the |
||
| """When enabled, the docker0/2514 ACCEPT rule is present in the host rebuild.""" | ||
| if not os.path.exists(DBCONFIG_PATH): | ||
| fs.create_file(DBCONFIG_PATH) | ||
|
|
||
| daemon = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {"redfish": {"state": "enabled"}}}) | ||
| self.assertTrue(daemon.RedfishAllowed) | ||
|
|
||
| cmds, _ = daemon.get_acl_rules_and_translate_to_iptables_commands('', MockConfigDb()) | ||
| self.assertIn(REDFISH_SYSLOG_RULE, [tuple(c) for c in cmds]) | ||
|
|
||
| @patchfs | ||
| def test_rule_absent_when_feature_disabled(self, fs): | ||
| """When disabled, no exception is programmed.""" | ||
| if not os.path.exists(DBCONFIG_PATH): | ||
| fs.create_file(DBCONFIG_PATH) | ||
|
|
||
| daemon = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {"redfish": {"state": "disabled"}}}) | ||
| self.assertFalse(daemon.RedfishAllowed) | ||
|
|
||
| cmds, _ = daemon.get_acl_rules_and_translate_to_iptables_commands('', MockConfigDb()) | ||
| self.assertNotIn(REDFISH_SYSLOG_RULE, [tuple(c) for c in cmds]) | ||
|
|
||
| @patchfs | ||
| def test_rule_absent_when_feature_entry_missing(self, fs): | ||
| """Images built without redfish have no FEATURE entry at all -- no exception.""" | ||
| if not os.path.exists(DBCONFIG_PATH): | ||
| fs.create_file(DBCONFIG_PATH) | ||
|
|
||
| daemon = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {}}) | ||
| self.assertFalse(daemon.RedfishAllowed) | ||
|
|
||
| cmds, _ = daemon.get_acl_rules_and_translate_to_iptables_commands('', MockConfigDb()) | ||
| self.assertNotIn(REDFISH_SYSLOG_RULE, [tuple(c) for c in cmds]) | ||
|
|
||
| @patchfs | ||
| def test_rule_reinserted_before_catch_all_drop(self, fs): | ||
| """With a CACL rule present (so the catch-all DROP exists), the exception appears | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring claims more than the assertion can show. The real window is also the opposite of the one described: after |
||
| AND strictly before the DROP -- there is never a rebuild window where a DROP | ||
| exists without it.""" | ||
| if not os.path.exists(DBCONFIG_PATH): | ||
| fs.create_file(DBCONFIG_PATH) | ||
|
|
||
| daemon = self.setup_daemon({ | ||
| "ACL_TABLE": { | ||
| "SSH_ONLY": {"stage": "INGRESS", "type": "CTRLPLANE", "services": ["SSH"]}, | ||
| }, | ||
| "ACL_RULE": { | ||
| "SSH_ONLY|RULE_1": {"PACKET_ACTION": "ACCEPT", "PRIORITY": "9999", "SRC_IP": "10.0.0.0/8"}, | ||
| }, | ||
| "DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {"redfish": {"state": "enabled"}}, | ||
| }) | ||
|
|
||
| cmds, _ = daemon.get_acl_rules_and_translate_to_iptables_commands('', MockConfigDb()) | ||
| cmds = [tuple(c) for c in cmds] | ||
| catch_all_drop = ('iptables', '-A', 'INPUT', '-j', 'DROP') | ||
| self.assertIn(REDFISH_SYSLOG_RULE, cmds, "exception must be present in rebuild") | ||
| self.assertIn(catch_all_drop, cmds, "test setup should produce a catch-all DROP") | ||
| self.assertLess(cmds.index(REDFISH_SYSLOG_RULE), cmds.index(catch_all_drop), | ||
| "exception must come before the catch-all DROP") | ||
|
|
||
| @patchfs | ||
| def test_rule_host_namespace_only(self, fs): | ||
| """docker0 lives in the host namespace, so asic namespaces get no exception.""" | ||
| if not os.path.exists(DBCONFIG_PATH): | ||
| fs.create_file(DBCONFIG_PATH) | ||
|
|
||
| daemon = self.setup_daemon({"DEVICE_METADATA": {"localhost": {}}, | ||
| "FEATURE": {"redfish": {"state": "enabled"}}}) | ||
| self.assertTrue(daemon.RedfishAllowed) | ||
|
|
||
| daemon.iptables_cmd_ns_prefix['asic0'] = [] | ||
| cmds, _ = daemon.get_acl_rules_and_translate_to_iptables_commands('asic0', MockConfigDb()) | ||
| self.assertNotIn(REDFISH_SYSLOG_RULE, [tuple(c) for c in cmds]) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"IPv4 only" is stated but not enforced. The rebuild appends an
ip6tables -A INPUT -j DROPcatch-all whenever any CTRLPLANE rule exists, and with"ipv6": truein/etc/docker/daemon.jsondocker0 gets an IPv6 gateway. If the container'sSYSLOG_TARGET_IPresolves to that address, the RELP session arrives over IPv6 and hits that DROP.rsyslog.conf.j2binds imrelp per-address with no v4 constraint, so this reads as a real (if currently unhit) gap rather than a design decision — worth either emitting theip6tablescounterpart or saying in the comment why v6 cannot happen.