sonic-buildimage/files/image_config/caclmgrd/caclmgrd
Joe LeVeque 116ddb996a [caclmgrd] Don't crash if we find empty/null rule_props (#2475)
* [caclmgrd] Don't crash if we find empty/null rule_props
2019-01-23 18:47:05 -08:00

298 lines
11 KiB
Python
Executable File

#!/usr/bin/env python
#
# caclmgrd
#
# Control plane ACL manager daemon for SONiC
#
# Upon starting, this daemon reads control plane ACL tables and rules from
# Config DB, converts the rules into iptables rules and installs the iptables
# rules. The daemon then indefintely listens for notifications from Config DB
# and updates iptables rules if control plane ACL configuration has changed.
#
try:
import ipaddr as ipaddress
import os
import subprocess
import sys
import syslog
from swsssdk import ConfigDBConnector
except ImportError as err:
raise ImportError("%s - required module not found" % str(err))
VERSION = "1.0"
SYSLOG_IDENTIFIER = "caclmgrd"
# ========================== Syslog wrappers ==========================
def log_info(msg):
syslog.openlog(SYSLOG_IDENTIFIER)
syslog.syslog(syslog.LOG_INFO, msg)
syslog.closelog()
def log_warning(msg):
syslog.openlog(SYSLOG_IDENTIFIER)
syslog.syslog(syslog.LOG_WARNING, msg)
syslog.closelog()
def log_error(msg):
syslog.openlog(SYSLOG_IDENTIFIER)
syslog.syslog(syslog.LOG_ERR, msg)
syslog.closelog()
# ============================== Classes ==============================
class ControlPlaneAclManager(object):
"""
Class which reads control plane ACL tables and rules from Config DB,
translates them into equivalent iptables commands and runs those
commands in order to apply the control plane ACLs.
Attributes:
config_db: Handle to Config Redis database via SwSS SDK
"""
ACL_TABLE = "ACL_TABLE"
ACL_RULE = "ACL_RULE"
ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE"
# To specify a port range, use iptables format: separate start and end
# ports with a colon, e.g., "1000:2000"
ACL_SERVICES = {
"NTP": {"ip_protocols": ["udp"], "dst_ports": ["123"]},
"SNMP": {"ip_protocols": ["tcp", "udp"], "dst_ports": ["161"]},
"SSH": {"ip_protocols": ["tcp"], "dst_ports": ["22"]}
}
def __init__(self):
# Open a handle to the Config database
self.config_db = ConfigDBConnector()
self.config_db.connect()
def run_commands(self, commands):
"""
Given a list of shell commands, run them in order
Args:
commands: List of strings, each string is a shell command
"""
for cmd in commands:
proc = subprocess.Popen(cmd, shell=True)
(stdout, stderr) = proc.communicate()
if proc.returncode != 0:
log_error("Error running command '{}'".format(cmd))
def parse_int_to_tcp_flags(self, hex_value):
tcp_flags_str = ""
if hex_value & 0x01:
tcp_flags_str += "FIN,"
if hex_value & 0x02:
tcp_flags_str += "SYN,"
if hex_value & 0x04:
tcp_flags_str += "RST,"
if hex_value & 0x08:
tcp_flags_str += "PSH,"
if hex_value & 0x10:
tcp_flags_str += "ACK,"
if hex_value & 0x20:
tcp_flags_str += "URG,"
# iptables doesn't handle the flags below now. It has some special keys for it:
# --ecn-tcp-cwr This matches if the TCP ECN CWR (Congestion Window Received) bit is set.
# --ecn-tcp-ece This matches if the TCP ECN ECE (ECN Echo) bit is set.
# if hex_value & 0x40:
# tcp_flags_str += "ECE,"
# if hex_value & 0x80:
# tcp_flags_str += "CWR,"
# Delete the trailing comma
tcp_flags_str = tcp_flags_str[:-1]
return tcp_flags_str
def get_acl_rules_and_translate_to_iptables_commands(self):
"""
Retrieves current ACL tables and rules from Config DB, translates
control plane ACLs into a list of iptables commands that can be run
in order to install ACL rules.
Returns:
A list of strings, each string is an iptables shell command
"""
iptables_cmds = []
# First, add iptables commands to set default policies to accept all
# traffic. In case we are connected remotely, the connection will not
# drop when we flush the current rules
iptables_cmds.append("iptables -P INPUT ACCEPT")
iptables_cmds.append("iptables -P FORWARD ACCEPT")
iptables_cmds.append("iptables -P OUTPUT ACCEPT")
# Add iptables command to flush the current rules
iptables_cmds.append("iptables -F")
# Add iptables command to delete all non-default chains
iptables_cmds.append("iptables -X")
# Add same set of commands for ip6tables
iptables_cmds.append("ip6tables -P INPUT ACCEPT")
iptables_cmds.append("ip6tables -P FORWARD ACCEPT")
iptables_cmds.append("ip6tables -P OUTPUT ACCEPT")
iptables_cmds.append("ip6tables -F")
iptables_cmds.append("ip6tables -X")
# Add iptables commands to allow all IPv4 and IPv6 traffic from localhost
iptables_cmds.append("iptables -A INPUT -s 127.0.0.1 -i lo -j ACCEPT")
iptables_cmds.append("ip6tables -A INPUT -s ::1 -i lo -j ACCEPT")
# Get current ACL tables and rules from Config DB
self._tables_db_info = self.config_db.get_table(self.ACL_TABLE)
self._rules_db_info = self.config_db.get_table(self.ACL_RULE)
# Walk the ACL tables
for (table_name, table_data) in self._tables_db_info.iteritems():
table_ip_version = None
# Ignore non-control-plane ACL tables
if table_data["type"] != self.ACL_TABLE_TYPE_CTRLPLANE:
continue
acl_services = table_data["services"]
for acl_service in acl_services:
if acl_service not in self.ACL_SERVICES:
log_warning("Ignoring control plane ACL '{}' with unrecognized service '{}'"
.format(table_name, acl_service))
continue
log_info("Translating ACL rules for control plane ACL '{}' (service: '{}')"
.format(table_name, acl_service))
# Obtain default IP protocol(s) and destination port(s) for this service
ip_protocols = self.ACL_SERVICES[acl_service]["ip_protocols"]
dst_ports = self.ACL_SERVICES[acl_service]["dst_ports"]
acl_rules = {}
for ((rule_table_name, rule_id), rule_props) in self._rules_db_info.iteritems():
if rule_table_name == table_name:
if not rule_props:
log_warning("rule_props for rule_id {} empty or null!".format(rule_id))
continue
try:
acl_rules[rule_props["PRIORITY"]] = rule_props
except KeyError:
log_error("rule_props for rule_id {} does not have key 'PRIORITY'!".format(rule_id))
continue
# If we haven't determined the IP version for this ACL table yet,
# try to do it now. We determine heuristically based on whether the
# src IP is an IPv4 or IPv6 address.
if not table_ip_version and "SRC_IP" in rule_props and rule_props["SRC_IP"]:
ip_addr = ipaddress.IPAddress(rule_props["SRC_IP"].split("/")[0])
if isinstance(ip_addr, ipaddress.IPv6Address):
table_ip_version = 6
elif isinstance(ip_addr, ipaddress.IPv4Address):
table_ip_version = 4
# If we were unable to determine whether this ACL table contains
# IPv4 or IPv6 rules, log a message and skip processing this table.
if not table_ip_version:
log_warning("Unable to determine if ACL table '{}' contains IPv4 or IPv6 rules. Skipping table..."
.format(table_name))
continue
# For each ACL rule in this table (in descending order of priority)
for priority in sorted(acl_rules.iterkeys(), reverse=True):
rule_props = acl_rules[priority]
if "PACKET_ACTION" not in rule_props:
log_error("ACL rule does not contain PACKET_ACTION property")
continue
# Apply the rule to the default protocol(s) for this ACL service
for ip_protocol in ip_protocols:
for dst_port in dst_ports:
rule_cmd = "ip6tables" if table_ip_version == 6 else "iptables"
rule_cmd += " -A INPUT -p {}".format(ip_protocol)
if "SRC_IP" in rule_props and rule_props["SRC_IP"]:
rule_cmd += " -s {}".format(rule_props["SRC_IP"])
rule_cmd += " --dport {}".format(dst_port)
# If there are TCP flags present and ip protocol is TCP, append them
if ip_protocol == "tcp" and "TCP_FLAGS" in rule_props and rule_props["TCP_FLAGS"]:
tcp_flags, tcp_flags_mask = rule_props["TCP_FLAGS"].split("/")
tcp_flags = int(tcp_flags, 16)
tcp_flags_mask = int(tcp_flags_mask, 16)
if tcp_flags_mask > 0:
rule_cmd += " --tcp-flags {mask} {flags}".format(mask = self.parse_int_to_tcp_flags(tcp_flags_mask), flags = self.parse_int_to_tcp_flags(tcp_flags))
# Append the packet action as the jump target
rule_cmd += " -j {}".format(rule_props["PACKET_ACTION"])
iptables_cmds.append(rule_cmd)
return iptables_cmds
def update_control_plane_acls(self):
"""
Convenience wrapper which retrieves current ACL tables and rules from
Config DB, translates control plane ACLs into a list of iptables
commands and runs them.
"""
iptables_cmds = self.get_acl_rules_and_translate_to_iptables_commands()
log_info("Issuing the following iptables commands:")
for cmd in iptables_cmds:
log_info(" " + cmd)
self.run_commands(iptables_cmds)
def notification_handler(self, key, data):
log_info("ACL configuration changed. Updating iptables rules for control plane ACLs...")
self.update_control_plane_acls()
def run(self):
# Unconditionally update control plane ACLs once at start
self.update_control_plane_acls()
# Subscribe to notifications when ACL tables or rules change
self.config_db.subscribe(self.ACL_TABLE,
lambda table, key, data: self.notification_handler(key, data))
self.config_db.subscribe(self.ACL_RULE,
lambda table, key, data: self.notification_handler(key, data))
# Indefinitely listen for Config DB notifications
self.config_db.listen()
# ============================= Functions =============================
def main():
log_info("Starting up...")
if not os.geteuid() == 0:
log_error("Must be root to run this daemon")
print "Error: Must be root to run this daemon"
sys.exit(1)
# Instantiate a ControlPlaneAclManager object
caclmgr = ControlPlaneAclManager()
caclmgr.run()
if __name__ == "__main__":
main()