Add caclmgrd and related files to translate and install control plane ACL rules (#1240)
This commit is contained in:
parent
16763dcc77
commit
0fffa6c63b
@ -232,6 +232,8 @@ sudo cp files/sshd/host-ssh-keygen.sh $FILESYSTEM_ROOT/usr/local/bin/
|
||||
sudo cp -f files/sshd/sshd.service $FILESYSTEM_ROOT/lib/systemd/system/ssh.service
|
||||
## Config sshd
|
||||
sudo augtool --autosave "set /files/etc/ssh/sshd_config/UseDNS no" -r $FILESYSTEM_ROOT
|
||||
sudo sed -i 's/^ListenAddress ::/#ListenAddress ::/' $FILESYSTEM_ROOT/etc/ssh/sshd_config
|
||||
sudo sed -i 's/^#ListenAddress 0.0.0.0/ListenAddress 0.0.0.0/' $FILESYSTEM_ROOT/etc/ssh/sshd_config
|
||||
|
||||
## Config monit
|
||||
sudo sed -i '
|
||||
|
@ -162,6 +162,12 @@ sudo cp $IMAGE_CONFIGS/asn/deployment_id_asn_map.yml $FILESYSTEM_ROOT/etc/sonic/
|
||||
# Copy sudoers configuration file
|
||||
sudo cp $IMAGE_CONFIGS/sudoers/sudoers $FILESYSTEM_ROOT/etc/
|
||||
|
||||
# Copy control plane ACL management daemon files
|
||||
sudo cp $IMAGE_CONFIGS/caclmgrd/caclmgrd.service $FILESYSTEM_ROOT/etc/systemd/system/
|
||||
sudo LANG=C chroot $FILESYSTEM_ROOT systemctl enable caclmgrd.service
|
||||
sudo cp $IMAGE_CONFIGS/caclmgrd/caclmgrd-start.sh $FILESYSTEM_ROOT/usr/bin/
|
||||
sudo cp $IMAGE_CONFIGS/caclmgrd/caclmgrd $FILESYSTEM_ROOT/usr/bin/
|
||||
|
||||
## Install package without starting service
|
||||
## ref: https://wiki.debian.org/chroot
|
||||
sudo tee -a $FILESYSTEM_ROOT/usr/sbin/policy-rc.d > /dev/null <<EOF
|
||||
|
@ -1,3 +1,3 @@
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/docker daemon -H fd:// --storage-driver=aufs --bip=240.127.1.1/24
|
||||
ExecStart=/usr/bin/docker daemon -H fd:// --storage-driver=aufs --bip=240.127.1.1/24 --iptables=false
|
||||
|
251
files/image_config/caclmgrd/caclmgrd
Executable file
251
files/image_config/caclmgrd/caclmgrd
Executable file
@ -0,0 +1,251 @@
|
||||
#!/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 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 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")
|
||||
|
||||
# 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():
|
||||
# Ignore non-control-plane ACL tables
|
||||
if table_data["type"] != self.ACL_TABLE_TYPE_CTRLPLANE:
|
||||
continue
|
||||
|
||||
acl_service = table_data["service"]
|
||||
|
||||
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:
|
||||
acl_rules[rule_props["PRIORITY"]] = rule_props
|
||||
|
||||
# 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
|
||||
|
||||
# If the rule contains an IP protocol, we will use it.
|
||||
# Otherwise, we will apply the rule to the default
|
||||
# protocol(s) for this ACL service
|
||||
if "IP_PROTOCOL" in rule_props:
|
||||
ip_protocols = [rule_props["IP_PROTOCOL"]]
|
||||
|
||||
for ip_protocol in ip_protocols:
|
||||
for dst_port in dst_ports:
|
||||
rule_cmd = "iptables -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, append them
|
||||
if "TCP_FLAGS" in rule_props and rule_props["TCP_FLAGS"]:
|
||||
tcp_flags = int(rule_props["TCP_FLAGS"], 16)
|
||||
|
||||
if tcp_flags > 0:
|
||||
rule_cmd += " --tcp-flags "
|
||||
|
||||
if tcp_flags & 0x01:
|
||||
rule_cmd += "FIN,"
|
||||
if tcp_flags & 0x02:
|
||||
rule_cmd += "SYN,"
|
||||
if tcp_flags & 0x04:
|
||||
rule_cmd += "RST,"
|
||||
if tcp_flags & 0x08:
|
||||
rule_cmd += "PSH,"
|
||||
if tcp_flags & 0x10:
|
||||
rule_cmd += "ACK,"
|
||||
if tcp_flags & 0x20:
|
||||
rule_cmd += "URG,"
|
||||
if tcp_flags & 0x40:
|
||||
rule_cmd += "ECE,"
|
||||
if tcp_flags & 0x80:
|
||||
rule_cmd += "CWR,"
|
||||
|
||||
# Delete the trailing comma
|
||||
rule_cmd = rule_cmd[:-1]
|
||||
|
||||
# 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()
|
10
files/image_config/caclmgrd/caclmgrd-start.sh
Executable file
10
files/image_config/caclmgrd/caclmgrd-start.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Only start control plance ACL manager daemon if not an Arista platform.
|
||||
# Arista devices will use their own service ACL manager daemon(s) instead.
|
||||
if [ "$(sonic-cfggen -v "platform" | grep -c "arista")" -gt 0 ]; then
|
||||
echo "Not starting caclmgrd - unsupported platform"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec /usr/bin/caclmgrd
|
11
files/image_config/caclmgrd/caclmgrd.service
Normal file
11
files/image_config/caclmgrd/caclmgrd.service
Normal file
@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Control Plane ACL configuration daemon
|
||||
Requires=database.service
|
||||
After=database.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/caclmgrd-start.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -200,7 +200,17 @@ def parse_dpg(dpg, hname):
|
||||
acl_intfs = port_alias_map.values()
|
||||
break;
|
||||
if acl_intfs:
|
||||
acls[aclname] = { 'policy_desc': aclname, 'ports': acl_intfs, 'type': 'MIRROR' if is_mirror else 'L3'}
|
||||
acls[aclname] = {'policy_desc': aclname,
|
||||
'ports': acl_intfs,
|
||||
'type': 'MIRROR' if is_mirror else 'L3',
|
||||
'service': 'N/A'}
|
||||
else:
|
||||
# This ACL has no interfaces to attach to -- consider this a control plane ACL
|
||||
aclservice = aclintf.find(str(QName(ns, "Type"))).text
|
||||
acls[aclname] = {'policy_desc': aclname,
|
||||
'ports': acl_intfs,
|
||||
'type': 'CTRLPLANE',
|
||||
'service': aclservice if aclservice is not None else ''}
|
||||
return intfs, lo_intfs, mgmt_intf, vlans, vlan_members, pcs, acls
|
||||
return None, None, None, None, None, None, None
|
||||
|
||||
|
@ -73,7 +73,7 @@ class TestCfgGen(TestCase):
|
||||
def test_minigraph_acl(self):
|
||||
argument = '-m "' + self.sample_graph_t0 + '" -p "' + self.port_config + '" -v ACL_TABLE'
|
||||
output = self.run_script(argument)
|
||||
self.assertEqual(output.strip(), "{'DATAACL': {'type': 'L3', 'policy_desc': 'DATAACL', 'ports': ['Ethernet112', 'Ethernet116', 'Ethernet120', 'Ethernet124']}}")
|
||||
self.assertEqual(output.strip(), "{'DATAACL': {'type': 'L3', 'policy_desc': 'DATAACL', 'service': 'N/A', 'ports': ['Ethernet112', 'Ethernet116', 'Ethernet120', 'Ethernet124']}}")
|
||||
|
||||
def test_minigraph_everflow(self):
|
||||
argument = '-m "' + self.sample_graph_t0 + '" -p "' + self.port_config + '" -v MIRROR_SESSION'
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 5ad84866491e7d4cf4cebcb96e469423c9c91961
|
||||
Subproject commit 6823ce2f3e46d9bbf5fcfa6b371705a7368929cb
|
Loading…
Reference in New Issue
Block a user