Skip to content
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

caclmgrd: monitor state_db to update dhcp acl #8222

Merged
5 commits merged into from
Aug 7, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 123 additions & 10 deletions src/sonic-host-services/scripts/caclmgrd
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
"""
ACL_TABLE = "ACL_TABLE"
ACL_RULE = "ACL_RULE"
DEVICE_METADATA_TABLE = "DEVICE_METADATA"
MUX_CABLE_TABLE = "MUX_CABLE_TABLE"

ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE"

Expand Down Expand Up @@ -84,6 +86,8 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):

UPDATE_DELAY_SECS = 0.5

DualToR = False

def __init__(self, log_identifier):
super(ControlPlaneAclManager, self).__init__(log_identifier)

Expand All @@ -108,7 +112,12 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
self.namespace_docker_mgmt_ip = {}
self.namespace_docker_mgmt_ipv6 = {}

metadata = self.config_db_map[DEFAULT_NAMESPACE].get_table(self.DEVICE_METADATA_TABLE)
if 'subtype' in metadata['localhost'] and metadata['localhost']['subtype'] == 'DualToR':
self.DualToR = True

namespaces = multi_asic.get_all_namespaces()

for front_asic_namespace in namespaces['front_ns']:
self.update_thread[front_asic_namespace] = None
self.lock[front_asic_namespace] = threading.Lock()
Expand Down Expand Up @@ -319,6 +328,77 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
else:
return False

def setup_dhcp_chain(self, namespace):
all_chains = self.get_chain_list(self.iptables_cmd_ns_prefix[namespace], [""])
dhcp_chain_exist = "DHCP" in all_chains

iptables_cmds = []
if dhcp_chain_exist:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -F DHCP")
self.log_info("DHCP chain exists, flush")
else:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -N DHCP")
self.log_info("DHCP chain does not exist, create")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A DHCP -j RETURN")

self.log_info("Issuing the following iptables commands for DHCP chain:")
for cmd in iptables_cmds:
self.log_info(" " + cmd)

self.run_commands(iptables_cmds)

def get_chain_list(self, iptable_ns_cmd_prefix, exclude_list):
command = iptable_ns_cmd_prefix + "iptables -L -v -n | grep Chain | awk '{print $2}'"
chain_list = self.run_commands([command]).splitlines()

for chain in exclude_list:
if chain in chain_list:
chain_list.remove(chain)

return chain_list

def dhcp_acl_rule(self, iptable_ns_cmd_prefix, op, intf):
'''
sample: iptables --insert/delete/check DHCP -m physdev --physdev-in Ethernet4 -j DROP
'''
return iptable_ns_cmd_prefix + 'iptables --{} DHCP -m physdev --physdev-in {} -j DROP'.format(op, intf)

def update_dhcp_chain(self, op, intf):
for namespace in list(self.config_db_map.keys()):
check_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], "check", intf)
update_cmd = self.dhcp_acl_rule(self.iptables_cmd_ns_prefix[namespace], op, intf)

execute = 0
ret = subprocess.call(check_cmd, shell=True) # ret==0 indicates the rule exists

if op == "insert" and ret == 1:
execute = 1
if op == "delete" and ret == 0:
execute = 1

if execute == 1:
subprocess.call(update_cmd, shell=True)
self.log_info("Update DHCP chain: {}".format(update_cmd))

def update_dhcp_acl(self, key, op, data):
if "state" not in data:
self.log_warning("Unexpected update in MUX_CABLE_TABLE")
return

intf = key
state = data["state"]

if state == "active":
self.update_dhcp_chain("delete", intf)
elif state == "standby":
self.update_dhcp_chain("insert", intf)
elif state == "unknown":
self.update_dhcp_chain("delete", intf)
elif state == "error":
self.log_warning("Cable state shows error")
else:
self.log_warning("Unexpected cable state")

def get_acl_rules_and_translate_to_iptables_commands(self, namespace):
"""
Retrieves current ACL tables and rules from Config DB, translates
Expand All @@ -337,11 +417,12 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -P FORWARD ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -P OUTPUT ACCEPT")

# Add iptables command to flush the current rules
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -F")

# Add iptables command to delete all non-default chains
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -X")
# Add iptables command to flush the current rules and delete all non-default chains
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this change for?, if caclmgrd restarts, can we flush as before?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to shield DHCP related rule in separate chain list. When flushing, exclude DHCP chain and flush/delete all other chains other than built-in chains.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, if caclmgrd restarts, we flush all.

chain_list = self.get_chain_list(self.iptables_cmd_ns_prefix[namespace], ["DHCP"] if self.DualToR else [""])
for chain in chain_list:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -F " + chain)
if chain not in ["INPUT", "FORWARD", "OUTPUT"]:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -X " + chain)

# Add same set of commands for ip6tables
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P INPUT ACCEPT")
Expand Down Expand Up @@ -383,6 +464,10 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT")

# Add iptables commands to link the DCHP chain to block dhcp packets based on ingress interfaces
if self.DualToR:
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p udp --dport 67 -j DHCP")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the below rules required for dualtor? Also why not port 68?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to direct matching into DHCP chain, dhcp discovers/requests use dport 67


# Add iptables/ip6tables commands to allow all incoming IPv4 DHCP packets
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p udp --dport 67:68 -j ACCEPT")
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p udp --dport 67:68 -j ACCEPT")
Expand Down Expand Up @@ -617,9 +702,26 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):

# Create the Select object
sel = swsscommon.Select()

# Set up STATE_DB connector to monitor the change in MUX_CABLE_TABLE
state_db_connector = None
subscribe_mux_cable = None
if self.DualToR:
self.log_info("Dual ToR mode")

# set up state_db connector
state_db_id = swsscommon.SonicDBConfig.getDbId("STATE_DB")
state_db_connector = swsscommon.DBConnector("STATE_DB", 0)
subscribe_mux_cable = swsscommon.SubscriberStateTable(state_db_connector, self.MUX_CABLE_TABLE)
sel.addSelectable(subscribe_mux_cable)

# create DHCP chain
for namespace in list(self.config_db_map.keys()):
self.setup_dhcp_chain(namespace)

# Map of Namespace <--> susbcriber table's object
config_db_subscriber_table_map = {}

# Loop through all asic namespaces (if present) and host namespace (DEFAULT_NAMESPACE)
for namespace in list(self.config_db_map.keys()):
# Unconditionally update control plane ACLs once at start on given namespace
Expand All @@ -641,10 +743,8 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
# Get the ACL rule table seprator
acl_rule_table_seprator = subscribe_acl_rule_table.getTableNameSeparator()

# Loop on select to see if any event happen on config db of any namespace
# Loop on select to see if any event happen on state db or config db of any namespace
while True:
ctrl_plane_acl_notification = set()

(state, selectableObj) = sel.select(SELECT_TIMEOUT_MS)
# Continue if select is timeout or selectable object is not return
if state != swsscommon.Select.OBJECT:
Expand All @@ -653,8 +753,21 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
# Get the redisselect object from selectable object
redisSelectObj = swsscommon.CastSelectableToRedisSelectObj(selectableObj)

# Get the corresponding namespace from redisselect db connector object
# Get the corresponding namespace and db_id from redisselect
namespace = redisSelectObj.getDbConnector().getNamespace()
db_id = redisSelectObj.getDbConnector().getDbId()

if db_id == state_db_id:
if self.DualToR:
while True:
key, op, fvs = subscribe_mux_cable.pop()
if not key:
break
self.log_info("mux cable update : '%s'" % str((key, op, fvs)))
self.update_dhcp_acl(key, op, dict(fvs))
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm this flow is tested on a non-dualtor testbed?

Copy link
Author

@ghost ghost Aug 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, i tested it with both subtype=dualtor and none on a vs testbed


ctrl_plane_acl_notification = set()
tahmed-dev marked this conversation as resolved.
Show resolved Hide resolved

# Pop data of both Subscriber Table object of namespace that got config db acl table event
for table in config_db_subscriber_table_map[namespace]:
Expand Down