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

Add caclmgrd and related files to translate and install control plane ACL rules #1240

Merged
merged 8 commits into from
Jan 10, 2018
2 changes: 2 additions & 0 deletions build_debian.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

@lguohan lguohan Dec 29, 2017

Choose a reason for hiding this comment

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

why do we need to uncomment these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We decided to only accept SSH connections over IPv4 interfaces, not IPv6. These two lines accomplish this.


## Config monit
sudo sed -i '
Expand Down
6 changes: 6 additions & 0 deletions files/build_templates/sonic_debian_extension.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion files/docker/docker.service.conf
Original file line number Diff line number Diff line change
@@ -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 changes: 251 additions & 0 deletions files/image_config/caclmgrd/caclmgrd
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This ImportError try/except block is something that was present in many SONiC Python scripts when I first joined the team. It is something I have carried over into new scripts for consistency. It is not necessary, it just presents a clear error message to the user if it fails to import a module and attempts to continue on.

It might be better to simply throw the exception and exit; again, I've simply carried on using it for consistency within the project.


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))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to continue with the following commands when previous command failed? Not quite sure whether to stop or to continue is a better approach, might need to look into detail scenario.


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 changes: 10 additions & 0 deletions files/image_config/caclmgrd/caclmgrd-start.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions files/image_config/caclmgrd/caclmgrd.service
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion src/sonic-config-engine/minigraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Copy link
Contributor

Choose a reason for hiding this comment

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

So is this "service" attribute only feasible for CTRLPLANE type? Will orchagent read this field for L3 and MIRROR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is only applicable to CTRLPLANE type. Orchagent currently has no concept of this field, so it is effectively ignored for L3 and MIRROR ACLs.

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

Expand Down
2 changes: 1 addition & 1 deletion src/sonic-config-engine/tests/test_cfggen.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_render_template(self):
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'
Expand Down
2 changes: 1 addition & 1 deletion src/sonic-utilities