1064cd5cd0
What I did:- For multi-asic platforms added iptable v4 rule to communicate on docker bridge ip For multi-asic platforms extend iptable v4 rule for iptable v6 also For multi-asic program made all internal rules applicable for all protocols (not filter based on tcp/udp). This is done to be consistent same as local host rule For multi-asic platforms made nat rule (to forward traffic from namespace to host) generic for all protocols and also use Source IP if present for matching
705 lines
38 KiB
Python
Executable File
705 lines
38 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 ipaddress
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
from sonic_py_common import daemon_base, device_info
|
|
from swsscommon import swsscommon
|
|
from swsssdk import SonicDBConfig, ConfigDBConnector
|
|
except ImportError as err:
|
|
raise ImportError("%s - required module not found" % str(err))
|
|
|
|
VERSION = "1.0"
|
|
|
|
SYSLOG_IDENTIFIER = "caclmgrd"
|
|
|
|
DEFAULT_NAMESPACE = ''
|
|
|
|
|
|
# ========================== Helper Functions =========================
|
|
|
|
|
|
def _ip_prefix_in_key(key):
|
|
"""
|
|
Function to check if IP prefix is present in a Redis database key.
|
|
If it is present, then the key will be a tuple. Otherwise, the
|
|
key will be a string.
|
|
"""
|
|
return (isinstance(key, tuple))
|
|
|
|
# ============================== Classes ==============================
|
|
|
|
|
|
class ControlPlaneAclManager(daemon_base.DaemonBase):
|
|
"""
|
|
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 instead of a single port, use iptables format:
|
|
# separate start and end ports with a colon, e.g., "1000:2000"
|
|
ACL_SERVICES = {
|
|
"NTP": {
|
|
"ip_protocols": ["udp"],
|
|
"dst_ports": ["123"],
|
|
"multi_asic_ns_to_host_fwd":False
|
|
},
|
|
"SNMP": {
|
|
"ip_protocols": ["tcp", "udp"],
|
|
"dst_ports": ["161"],
|
|
"multi_asic_ns_to_host_fwd":True
|
|
},
|
|
"SSH": {
|
|
"ip_protocols": ["tcp"],
|
|
"dst_ports": ["22"],
|
|
"multi_asic_ns_to_host_fwd":True
|
|
}
|
|
}
|
|
|
|
UPDATE_DELAY_SECS = 0.5
|
|
|
|
def __init__(self, log_identifier):
|
|
super(ControlPlaneAclManager, self).__init__(log_identifier)
|
|
|
|
# Update-thread-specific data per namespace
|
|
self.update_thread = {}
|
|
self.lock = {}
|
|
self.num_changes = {}
|
|
|
|
# Initialize update-thread-specific data for default namespace
|
|
self.update_thread[DEFAULT_NAMESPACE] = None
|
|
self.lock[DEFAULT_NAMESPACE] = threading.Lock()
|
|
self.num_changes[DEFAULT_NAMESPACE] = 0
|
|
|
|
SonicDBConfig.load_sonic_global_db_config()
|
|
self.config_db_map = {}
|
|
self.iptables_cmd_ns_prefix = {}
|
|
self.config_db_map[DEFAULT_NAMESPACE] = ConfigDBConnector(use_unix_socket_path=True, namespace=DEFAULT_NAMESPACE)
|
|
self.config_db_map[DEFAULT_NAMESPACE].connect()
|
|
self.iptables_cmd_ns_prefix[DEFAULT_NAMESPACE] = ""
|
|
self.namespace_mgmt_ip = self.get_namespace_mgmt_ip(self.iptables_cmd_ns_prefix[DEFAULT_NAMESPACE], DEFAULT_NAMESPACE)
|
|
self.namespace_mgmt_ipv6 = self.get_namespace_mgmt_ipv6(self.iptables_cmd_ns_prefix[DEFAULT_NAMESPACE], DEFAULT_NAMESPACE)
|
|
self.namespace_docker_mgmt_ip = {}
|
|
self.namespace_docker_mgmt_ipv6 = {}
|
|
|
|
namespaces = device_info.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()
|
|
self.num_changes[front_asic_namespace] = 0
|
|
|
|
self.config_db_map[front_asic_namespace] = ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespace)
|
|
self.config_db_map[front_asic_namespace].connect()
|
|
self.iptables_cmd_ns_prefix[front_asic_namespace] = "ip netns exec " + front_asic_namespace + " "
|
|
self.namespace_docker_mgmt_ip[front_asic_namespace] = self.get_namespace_mgmt_ip(self.iptables_cmd_ns_prefix[front_asic_namespace],
|
|
front_asic_namespace)
|
|
self.namespace_docker_mgmt_ipv6[front_asic_namespace] = self.get_namespace_mgmt_ipv6(self.iptables_cmd_ns_prefix[front_asic_namespace],
|
|
front_asic_namespace)
|
|
|
|
for back_asic_namespace in namespaces['back_ns']:
|
|
self.update_thread[back_asic_namespace] = None
|
|
self.lock[back_asic_namespace] = threading.Lock()
|
|
self.num_changes[back_asic_namespace] = 0
|
|
|
|
self.iptables_cmd_ns_prefix[back_asic_namespace] = "ip netns exec " + back_asic_namespace + " "
|
|
self.namespace_docker_mgmt_ip[back_asic_namespace] = self.get_namespace_mgmt_ip(self.iptables_cmd_ns_prefix[back_asic_namespace],
|
|
back_asic_namespace)
|
|
self.namespace_docker_mgmt_ipv6[back_asic_namespace] = self.get_namespace_mgmt_ipv6(self.iptables_cmd_ns_prefix[back_asic_namespace],
|
|
back_asic_namespace)
|
|
|
|
def get_namespace_mgmt_ip(self, iptable_ns_cmd_prefix, namespace):
|
|
ip_address_get_command = iptable_ns_cmd_prefix + "ip -4 -o addr show " + ("eth0" if namespace else "docker0") +\
|
|
" | awk '{print $4}' | cut -d'/' -f1 | head -1"
|
|
|
|
return self.run_commands([ip_address_get_command])
|
|
|
|
def get_namespace_mgmt_ipv6(self, iptable_ns_cmd_prefix, namespace):
|
|
ipv6_address_get_command = iptable_ns_cmd_prefix + "ip -6 -o addr show scope global " + ("eth0" if namespace else "docker0") +\
|
|
" | awk '{print $4}' | cut -d'/' -f1 | head -1"
|
|
return self.run_commands([ipv6_address_get_command])
|
|
|
|
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=subprocess.PIPE)
|
|
|
|
(stdout, stderr) = proc.communicate()
|
|
|
|
if proc.returncode != 0:
|
|
self.log_error("Error running command '{}'".format(cmd))
|
|
elif stdout:
|
|
return stdout.rstrip('\n')
|
|
|
|
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 generate_block_ip2me_traffic_iptables_commands(self, namespace):
|
|
INTERFACE_TABLE_NAME_LIST = [
|
|
"LOOPBACK_INTERFACE",
|
|
"MGMT_INTERFACE",
|
|
"VLAN_INTERFACE",
|
|
"PORTCHANNEL_INTERFACE",
|
|
"INTERFACE"
|
|
]
|
|
|
|
block_ip2me_cmds = []
|
|
|
|
# Add iptables rules to drop all packets destined for peer-to-peer interface IP addresses
|
|
for iface_table_name in INTERFACE_TABLE_NAME_LIST:
|
|
iface_table = self.config_db_map[namespace].get_table(iface_table_name)
|
|
if iface_table:
|
|
for key, _ in iface_table.iteritems():
|
|
if not _ip_prefix_in_key(key):
|
|
continue
|
|
|
|
iface_name, iface_cidr = key
|
|
ip_ntwrk = ipaddress.ip_network(iface_cidr, strict=False)
|
|
|
|
# For VLAN interfaces, the IP address we want to block is the default gateway (i.e.,
|
|
# the first available host IP address of the VLAN subnet)
|
|
ip_addr = next(ip_ntwrk.hosts()) if iface_table_name == "VLAN_INTERFACE" else ip_ntwrk.network_address
|
|
|
|
if isinstance(ip_ntwrk, ipaddress.IPv4Network):
|
|
block_ip2me_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -d {}/{} -j DROP".format(ip_addr, ip_ntwrk.max_prefixlen))
|
|
elif isinstance(ip_ntwrk, ipaddress.IPv6Network):
|
|
block_ip2me_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -d {}/{} -j DROP".format(ip_addr, ip_ntwrk.max_prefixlen))
|
|
else:
|
|
self.log_warning("Unrecognized IP address type on interface '{}': {}".format(iface_name, ip_ntwrk))
|
|
|
|
return block_ip2me_cmds
|
|
|
|
def generate_allow_internal_docker_ip_traffic_commands(self, namespace):
|
|
allow_internal_docker_ip_cmds = []
|
|
|
|
if namespace:
|
|
# For namespace docker allow local communication on docker management ip for all proto
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(self.namespace_docker_mgmt_ip[namespace], self.namespace_docker_mgmt_ip[namespace]))
|
|
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(self.namespace_docker_mgmt_ipv6[namespace], self.namespace_docker_mgmt_ipv6[namespace]))
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(self.namespace_mgmt_ip, self.namespace_docker_mgmt_ip[namespace]))
|
|
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(self.namespace_mgmt_ipv6, self.namespace_docker_mgmt_ipv6[namespace]))
|
|
|
|
else:
|
|
|
|
# Also host namespace communication on docker bridge on multi-asic.
|
|
if self.namespace_docker_mgmt_ip:
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(self.namespace_mgmt_ip, self.namespace_mgmt_ip))
|
|
|
|
if self.namespace_docker_mgmt_ipv6:
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(self.namespace_mgmt_ipv6, self.namespace_mgmt_ipv6))
|
|
# In host allow all tcp/udp traffic from namespace docker eth0 management ip to host docker bridge
|
|
for docker_mgmt_ip in self.namespace_docker_mgmt_ip.values():
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(docker_mgmt_ip, self.namespace_mgmt_ip))
|
|
|
|
for docker_mgmt_ipv6 in list(self.namespace_docker_mgmt_ipv6.values()):
|
|
allow_internal_docker_ip_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -s {} -d {} -j ACCEPT".format
|
|
(docker_mgmt_ipv6, self.namespace_mgmt_ipv6))
|
|
|
|
return allow_internal_docker_ip_cmds
|
|
|
|
def generate_fwd_traffic_from_namespace_to_host_commands(self, namespace, acl_source_ip_map):
|
|
"""
|
|
The below SNAT and DNAT rules are added in asic namespace in multi-ASIC platforms. It helps to forward request coming
|
|
in through the front panel interfaces created/present in the asic namespace for the servie running in linux host network namespace.
|
|
The external IP addresses are NATed to the internal docker IP addresses for the Host service to respond.
|
|
"""
|
|
|
|
if not namespace:
|
|
return []
|
|
|
|
fwd_traffic_from_namespace_to_host_cmds = []
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -t nat -X")
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -t nat -F")
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -t nat -X")
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -t nat -F")
|
|
|
|
for acl_service in self.ACL_SERVICES:
|
|
if self.ACL_SERVICES[acl_service]["multi_asic_ns_to_host_fwd"]:
|
|
# Get the Source IP Set if exists else use default source ip prefix
|
|
nat_source_ipv4_set = acl_source_ip_map[acl_service]["ipv4"] if acl_source_ip_map and acl_source_ip_map[acl_service]["ipv4"] else { "0.0.0.0/0" }
|
|
nat_source_ipv6_set = acl_source_ip_map[acl_service]["ipv6"] if acl_source_ip_map and acl_source_ip_map[acl_service]["ipv6"] else { "::/0" }
|
|
|
|
for ip_protocol in self.ACL_SERVICES[acl_service]["ip_protocols"]:
|
|
for dst_port in self.ACL_SERVICES[acl_service]["dst_ports"]:
|
|
for ipv4_src_ip in nat_source_ipv4_set:
|
|
# IPv4 rules
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
|
|
"iptables -t nat -A PREROUTING -p {} -s {} --dport {} -j DNAT --to-destination {}".format
|
|
(ip_protocol, ipv4_src_ip, dst_port,
|
|
self.namespace_mgmt_ip))
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
|
|
"iptables -t nat -A POSTROUTING -p {} -s {} --dport {} -j SNAT --to-source {}".format
|
|
(ip_protocol, ipv4_src_ip, dst_port,
|
|
self.namespace_docker_mgmt_ip[namespace]))
|
|
for ipv6_src_ip in nat_source_ipv6_set:
|
|
# IPv6 rules
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
|
|
"ip6tables -t nat -A PREROUTING -p {} -s {} --dport {} -j DNAT --to-destination {}".format
|
|
(ip_protocol, ipv6_src_ip, dst_port,
|
|
self.namespace_mgmt_ipv6))
|
|
fwd_traffic_from_namespace_to_host_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
|
|
"ip6tables -t nat -A POSTROUTING -p {} -s {} --dport {} -j SNAT --to-source {}".format
|
|
(ip_protocol,ipv6_src_ip, dst_port,
|
|
self.namespace_docker_mgmt_ipv6[namespace]))
|
|
|
|
return fwd_traffic_from_namespace_to_host_cmds
|
|
|
|
def is_rule_ipv4(self, rule_props):
|
|
if (("SRC_IP" in rule_props and rule_props["SRC_IP"]) or
|
|
("DST_IP" in rule_props and rule_props["DST_IP"])):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def is_rule_ipv6(self, rule_props):
|
|
if (("SRC_IPV6" in rule_props and rule_props["SRC_IPV6"]) or
|
|
("DST_IPV6" in rule_props and rule_props["DST_IPV6"])):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def get_acl_rules_and_translate_to_iptables_commands(self, namespace):
|
|
"""
|
|
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 = []
|
|
service_to_source_ip_map = {}
|
|
|
|
# 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(self.iptables_cmd_ns_prefix[namespace] + "iptables -P INPUT ACCEPT")
|
|
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 same set of commands for ip6tables
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P INPUT ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P FORWARD ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -P OUTPUT ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -F")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -X")
|
|
|
|
# Add iptables/ip6tables commands to allow all traffic from localhost
|
|
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")
|
|
|
|
# Add iptables commands to allow internal docker traffic
|
|
iptables_cmds += self.generate_allow_internal_docker_ip_traffic_commands(namespace)
|
|
|
|
# Add iptables/ip6tables commands to allow all incoming packets from established
|
|
# connections or new connections which are related to established connections
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT")
|
|
|
|
# Add iptables/ip6tables commands to allow bidirectional ICMPv4 ping and traceroute
|
|
# TODO: Support processing ICMPv4 service ACL rules, and remove this blanket acceptance
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT")
|
|
|
|
# Add iptables/ip6tables commands to allow bidirectional ICMPv6 ping and traceroute
|
|
# TODO: Support processing ICMPv6 service ACL rules, and remove this blanket acceptance
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-request -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-reply -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT")
|
|
|
|
# Add iptables/ip6tables commands to allow all incoming Neighbor Discovery Protocol (NDP) NS/NA/RS/RA messages
|
|
# TODO: Support processing NDP service ACL rules, and remove this blanket acceptance
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT")
|
|
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/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")
|
|
|
|
# Add iptables/ip6tables commands to allow all incoming IPv6 DHCP packets
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p udp --dport 546:547 -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p udp --dport 546:547 -j ACCEPT")
|
|
|
|
# Add iptables/ip6tables commands to allow all incoming BGP traffic
|
|
# TODO: Determine BGP ACLs based on configured device sessions, and remove this blanket acceptance
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p tcp --dport 179 -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -p tcp --sport 179 -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p tcp --dport 179 -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p tcp --sport 179 -j ACCEPT")
|
|
|
|
# Get current ACL tables and rules from Config DB
|
|
self._tables_db_info = self.config_db_map[namespace].get_table(self.ACL_TABLE)
|
|
self._rules_db_info = self.config_db_map[namespace].get_table(self.ACL_RULE)
|
|
|
|
num_ctrl_plane_acl_rules = 0
|
|
|
|
# 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:
|
|
self.log_warning("Ignoring control plane ACL '{}' with unrecognized service '{}'"
|
|
.format(table_name, acl_service))
|
|
continue
|
|
|
|
self.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():
|
|
rule_props = {k.upper(): v for k,v in rule_props.iteritems()}
|
|
if rule_table_name == table_name:
|
|
if not rule_props:
|
|
self.log_warning("rule_props for rule_id {} empty or null!".format(rule_id))
|
|
continue
|
|
|
|
try:
|
|
acl_rules[rule_props["PRIORITY"]] = rule_props
|
|
except KeyError:
|
|
self.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 attempt to determine heuristically based on
|
|
# whether the src or dst IP of this rule is an IPv4 or IPv6 address.
|
|
if not table_ip_version:
|
|
if self.is_rule_ipv6(rule_props):
|
|
table_ip_version = 6
|
|
elif self.is_rule_ipv4(rule_props):
|
|
table_ip_version = 4
|
|
|
|
if (self.is_rule_ipv6(rule_props) and (table_ip_version == 4)):
|
|
self.log_error("CtrlPlane ACL table {} is a IPv4 based table and rule {} is a IPV6 rule! Ignoring rule."
|
|
.format(table_name, rule_id))
|
|
acl_rules.pop(rule_props["PRIORITY"])
|
|
elif (self.is_rule_ipv4(rule_props) and (table_ip_version == 6)):
|
|
self.log_error("CtrlPlane ACL table {} is a IPv6 based table and rule {} is a IPV4 rule! Ignroing rule."
|
|
.format(table_name, rule_id))
|
|
acl_rules.pop(rule_props["PRIORITY"])
|
|
|
|
# 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:
|
|
self.log_warning("Unable to determine if ACL table '{}' contains IPv4 or IPv6 rules. Skipping table..."
|
|
.format(table_name))
|
|
continue
|
|
ipv4_src_ip_set = set()
|
|
ipv6_src_ip_set = set()
|
|
# 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:
|
|
self.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_IPV6" in rule_props and rule_props["SRC_IPV6"]:
|
|
rule_cmd += " -s {}".format(rule_props["SRC_IPV6"])
|
|
if rule_props["PACKET_ACTION"] == "ACCEPT":
|
|
ipv6_src_ip_set.add(rule_props["SRC_IPV6"])
|
|
elif "SRC_IP" in rule_props and rule_props["SRC_IP"]:
|
|
rule_cmd += " -s {}".format(rule_props["SRC_IP"])
|
|
if rule_props["PACKET_ACTION"] == "ACCEPT":
|
|
ipv4_src_ip_set.add(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(self.iptables_cmd_ns_prefix[namespace] + rule_cmd)
|
|
num_ctrl_plane_acl_rules += 1
|
|
|
|
|
|
service_to_source_ip_map.update({ acl_service:{ "ipv4":ipv4_src_ip_set, "ipv6":ipv6_src_ip_set } })
|
|
|
|
# Add iptables commands to block ip2me traffic
|
|
iptables_cmds += self.generate_block_ip2me_traffic_iptables_commands(namespace)
|
|
|
|
# Add iptables/ip6tables commands to allow all incoming packets with TTL of 0 or 1
|
|
# This allows the device to respond to tools like tcptraceroute
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -m ttl --ttl-lt 2 -j ACCEPT")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -p tcp -m hl --hl-lt 2 -j ACCEPT")
|
|
|
|
# Finally, if the device has control plane ACLs configured,
|
|
# add iptables/ip6tables commands to drop all other incoming packets
|
|
if num_ctrl_plane_acl_rules > 0:
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "iptables -A INPUT -j DROP")
|
|
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + "ip6tables -A INPUT -j DROP")
|
|
|
|
return iptables_cmds, service_to_source_ip_map
|
|
|
|
def update_control_plane_acls(self, namespace):
|
|
"""
|
|
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, service_to_source_ip_map = self.get_acl_rules_and_translate_to_iptables_commands(namespace)
|
|
self.log_info("Issuing the following iptables commands:")
|
|
for cmd in iptables_cmds:
|
|
self.log_info(" " + cmd)
|
|
|
|
self.run_commands(iptables_cmds)
|
|
|
|
self.update_control_plane_nat_acls(namespace, service_to_source_ip_map)
|
|
|
|
def update_control_plane_nat_acls(self, namespace, service_to_source_ip_map):
|
|
"""
|
|
Convenience wrapper for multi-asic platforms
|
|
which programs the NAT rules for redirecting the
|
|
traffic coming on the front panel interface map to namespace
|
|
to the host.
|
|
"""
|
|
# Add iptables commands to allow front panel traffic
|
|
iptables_cmds = self.generate_fwd_traffic_from_namespace_to_host_commands(namespace, service_to_source_ip_map)
|
|
|
|
self.log_info("Issuing the following iptables commands:")
|
|
for cmd in iptables_cmds:
|
|
self.log_info(" " + cmd)
|
|
|
|
self.run_commands(iptables_cmds)
|
|
|
|
def check_and_update_control_plane_acls(self, namespace, num_changes):
|
|
"""
|
|
This function is intended to be spawned in a separate thread.
|
|
Its purpose is to prevent unnecessary iptables updates if we receive
|
|
multiple rapid ACL table update notifications. It sleeps for UPDATE_DELAY_SECS
|
|
then checks if any more ACL table updates were received in that window. If new
|
|
updates were received, it will sleep again and repeat the process until no
|
|
updates were received during the delay window, at which point it will update
|
|
iptables using the current ACL rules.
|
|
"""
|
|
while True:
|
|
# Sleep for our delay interval
|
|
time.sleep(self.UPDATE_DELAY_SECS)
|
|
|
|
with self.lock[namespace]:
|
|
if self.num_changes[namespace] > num_changes:
|
|
# More ACL table changes occurred since this thread was spawned
|
|
# spawn a new thread with the current number of changes
|
|
new_changes = self.num_changes[namespace] - num_changes
|
|
self.log_info("ACL config not stable for namespace '{}': {} changes detected in the past {} seconds. Skipping update ..."
|
|
.format(namespace, new_changes, self.UPDATE_DELAY_SECS))
|
|
num_changes = self.num_changes[namespace]
|
|
else:
|
|
if num_changes == self.num_changes[namespace] and num_changes > 0:
|
|
self.log_info("ACL config for namespace '{}' has not changed for {} seconds. Applying updates ..."
|
|
.format(namespace, self.UPDATE_DELAY_SECS))
|
|
self.update_control_plane_acls(namespace)
|
|
else:
|
|
self.log_error("Error updating ACLs for namespace '{}'".format(namespace))
|
|
|
|
# Re-initialize
|
|
self.num_changes[namespace] = 0
|
|
self.update_thread[namespace] = None
|
|
return
|
|
|
|
def run(self):
|
|
# Set select timeout to 1 second
|
|
SELECT_TIMEOUT_MS = 1000
|
|
|
|
self.log_info("Starting up ...")
|
|
|
|
if not os.geteuid() == 0:
|
|
self.log_error("Must be root to run this daemon")
|
|
print("Error: Must be root to run this daemon")
|
|
sys.exit(1)
|
|
|
|
# Initlaize Global config that loads all database*.json
|
|
if device_info.is_multi_npu():
|
|
swsscommon.SonicDBConfig.initializeGlobalConfig()
|
|
|
|
# Create the Select object
|
|
sel = swsscommon.Select()
|
|
# 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 self.config_db_map.keys():
|
|
# Unconditionally update control plane ACLs once at start on given namespace
|
|
self.update_control_plane_acls(namespace)
|
|
# Connect to Config DB of given namespace
|
|
acl_db_connector = swsscommon.DBConnector("CONFIG_DB", 0, False, namespace)
|
|
# Subscribe to notifications when ACL tables changes
|
|
subscribe_acl_table = swsscommon.SubscriberStateTable(acl_db_connector, swsscommon.CFG_ACL_TABLE_TABLE_NAME)
|
|
# Subscribe to notifications when ACL rule tables changes
|
|
subscribe_acl_rule_table = swsscommon.SubscriberStateTable(acl_db_connector, swsscommon.CFG_ACL_RULE_TABLE_NAME)
|
|
# Add both tables to the selectable object
|
|
sel.addSelectable(subscribe_acl_table)
|
|
sel.addSelectable(subscribe_acl_rule_table)
|
|
# Update the map
|
|
config_db_subscriber_table_map[namespace] = []
|
|
config_db_subscriber_table_map[namespace].append(subscribe_acl_table)
|
|
config_db_subscriber_table_map[namespace].append(subscribe_acl_rule_table)
|
|
|
|
# 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
|
|
while True:
|
|
ctrl_plane_acl_notification = set()
|
|
|
|
# A brief sleep appears necessary in this loop or any spawned
|
|
# update threads will get stuck. Appears to be due to the sel.select() call.
|
|
# TODO: Eliminate the need for this sleep.
|
|
time.sleep(0.1)
|
|
|
|
(state, selectableObj) = sel.select(SELECT_TIMEOUT_MS)
|
|
# Continue if select is timeout or selectable object is not return
|
|
if state != swsscommon.Select.OBJECT:
|
|
continue
|
|
|
|
# Get the redisselect object from selectable object
|
|
redisSelectObj = swsscommon.CastSelectableToRedisSelectObj(selectableObj)
|
|
|
|
# Get the corresponding namespace from redisselect db connector object
|
|
namespace = redisSelectObj.getDbConnector().getNamespace()
|
|
|
|
# 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]:
|
|
while True:
|
|
(key, op, fvp) = table.pop()
|
|
# Pop of table that does not have data so break
|
|
if key == '':
|
|
break
|
|
# ACL Table notification. We will take Control Plane ACTION for any ACL Table Event
|
|
# This can be optimize further but we should not have many acl table set/del events in normal
|
|
# scenario
|
|
if acl_rule_table_seprator not in key:
|
|
ctrl_plane_acl_notification.add(namespace)
|
|
# Check ACL Rule notification and make sure Rule point to ACL Table which is Controlplane
|
|
else:
|
|
acl_table = key.split(acl_rule_table_seprator)[0]
|
|
if self.config_db_map[namespace].get_table(self.ACL_TABLE)[acl_table]["type"] == self.ACL_TABLE_TYPE_CTRLPLANE:
|
|
ctrl_plane_acl_notification.add(namespace)
|
|
|
|
# Update the Control Plane ACL of the namespace that got config db acl table event
|
|
for namespace in ctrl_plane_acl_notification:
|
|
with self.lock[namespace]:
|
|
if self.num_changes[namespace] == 0:
|
|
self.log_info("ACL change detected for namespace '{}'".format(namespace))
|
|
|
|
# Increment the number of change events we've received for this namespace
|
|
self.num_changes[namespace] += 1
|
|
|
|
# If an update thread is not already spawned for the namespace which we received
|
|
# the ACL table update event, spawn one now
|
|
if not self.update_thread[namespace]:
|
|
self.log_info("Spawning ACL update thread for namepsace '{}' ...".format(namespace))
|
|
self.update_thread[namespace] = threading.Thread(target=self.check_and_update_control_plane_acls,
|
|
args=(namespace, self.num_changes[namespace]))
|
|
self.update_thread[namespace].start()
|
|
|
|
# ============================= Functions =============================
|
|
|
|
|
|
def main():
|
|
# Instantiate a ControlPlaneAclManager object
|
|
caclmgr = ControlPlaneAclManager(SYSLOG_IDENTIFIER)
|
|
|
|
# Log all messages from INFO level and higher
|
|
caclmgr.set_min_log_priority_info()
|
|
|
|
caclmgr.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|