components/openstack/neutron/files/agent/solaris/pf_firewall.py
changeset 7315 5cc40226273b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/openstack/neutron/files/agent/solaris/pf_firewall.py	Tue Nov 15 18:00:12 2016 -0800
@@ -0,0 +1,438 @@
+# Copyright 2012, Nachi Ueno, NTT MCL, Inc.
+# All Rights Reserved.
+#
+# Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import collections
+
+import netaddr
+from oslo_config import cfg
+from oslo_log import log as logging
+import six
+
+from neutron._i18n import _LI
+from neutron.agent.common import ovs_lib
+from neutron.agent import firewall
+from neutron.agent.solaris import packetfilter
+from neutron.common import constants
+from neutron.common import utils as c_utils
+
+
+LOG = logging.getLogger(__name__)
+ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES = [131, 135, 143]
+
+DIRECTION_PF_PARAM = {firewall.INGRESS_DIRECTION: 'in',
+                      firewall.EGRESS_DIRECTION: 'out'}
+
+
+class PFBaseOVS(ovs_lib.BaseOVS):
+    def get_port_by_id(self, port_id):
+        ports = self.ovsdb.db_find(
+            'Interface', ('external_ids', '=', {'iface-id': port_id}),
+            columns=['name']).execute()
+        if ports:
+            return ports[0]['name']
+        return None
+
+
+class PFFirewallDriver(firewall.FirewallDriver):
+    """Driver which enforces security groups through PF rules.
+
+    Please look at neutron.agent.firewall.FirewallDriver for more information
+    on how the methods below are called from the Neutron Open vSwitch agent. It
+    all starts at prepare_port_filter() and then _setup_pf_rules() has all the
+    PF based logic to add correct rules on guest instance's port.
+    """
+
+    def __init__(self):
+        self.pf = packetfilter.PacketFilter("_auto/neutron:ovs:agent",
+                                            layer2=True)
+        # List of port which has security group
+        self.filtered_ports = {}
+        self.unfiltered_ports = {}
+        # List of security group rules for ports residing on this host
+        self.sg_rules = {}
+        # List of security group member ips for ports residing on this host
+        self.sg_members = collections.defaultdict(
+            lambda: collections.defaultdict(list))
+        # Every PF rule needs to be labeled so that we can later kill the state
+        # associated with that rule (using pfctl -k label -k 110). It is hard
+        # to come up with a meaningfully named label for each PF rule, so we
+        # are resorting to numbers here.
+        self.label_num = 100
+        self.portid_to_devname = {}
+
+    def prepare_port_filter(self, port):
+        LOG.debug("Preparing device (%s) filter", port['device'])
+        self._setup_pf_rules(port)
+
+    def apply_port_filter(self, port):
+        """We never call this method
+
+        It exists here to override abstract method of parent abstract class.
+        """
+        pass
+
+    def update_port_filter(self, port):
+        LOG.debug("Updating device (%s) filter", port['device'])
+        self._setup_pf_rules(port, update=True)
+
+    def remove_port_filter(self, port):
+        LOG.debug("Removing device (%s) filter", port['device'])
+        self.unfiltered_ports.pop(port['device'], None)
+        self.filtered_ports.pop(port['device'], None)
+        self._remove_rule_port_sec(port)
+
+    @property
+    def ports(self):
+        return dict(self.filtered_ports, **self.unfiltered_ports)
+
+    def update_security_group_rules(self, sg_id, sg_rules):
+        LOG.debug("Update rules of security group %s(%s)" % (sg_id, sg_rules))
+        self.sg_rules[sg_id] = sg_rules
+
+    def update_security_group_members(self, sg_id, sg_members):
+        LOG.debug("Update members of security group %s(%s)" %
+                  (sg_id, sg_members))
+        self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
+
+    def security_group_updated(self, action_type, sec_group_ids,
+                               device_ids=None):
+        # TODO(gmoodalb): Extend this later to optimize handling of security
+        # groups update
+        pass
+
+    def _get_label_number(self):
+        self.label_num += 1
+        return self.label_num
+
+    def _remove_rule_port_sec(self, port):
+        device_name = self.portid_to_devname.pop(port['id'], None)
+        if not device_name:
+            LOG.info(_LI("Could not find port: %s. Failed to remove PF rules "
+                         "for that port"), port['id'])
+            return
+        LOG.debug("Removing PF rules for device_name(%s)" % device_name)
+        # we need to remove both ingress and egress
+        if '/' in device_name:
+            instance_name, datalink = device_name.split('/')
+            instance_name = instance_name.split(':')[1]
+            ingress = '%s_in' % datalink
+            egress = '%s_out' % datalink
+        else:
+            instance_name = device_name
+            ingress = 'in'
+            egress = 'out'
+        existing_anchor_rules = set(self.pf.list_anchor_rules([instance_name]))
+        existing_anchor_rules.discard('anchor "%s" in on %s all' %
+                                      (ingress, device_name))
+        existing_anchor_rules.discard('anchor "%s" out on %s all' %
+                                      (egress, device_name))
+        self.pf.add_rules(list(existing_anchor_rules), [instance_name])
+        if existing_anchor_rules:
+            self.pf.remove_anchor_recursively([instance_name, ingress])
+            self.pf.remove_anchor_recursively([instance_name, egress])
+        else:
+            self.pf.remove_anchor_recursively([instance_name])
+
+    def _setup_pf_rules(self, port, update=False):
+        if not firewall.port_sec_enabled(port):
+            self.unfiltered_ports[port['device']] = port
+            self.filtered_ports.pop(port['device'], None)
+            self._remove_rule_port_sec(port)
+        else:
+            self.filtered_ports[port['device']] = port
+            self.unfiltered_ports.pop(port['device'], None)
+            if update:
+                self._remove_rule_port_sec(port)
+            self._add_rules_by_security_group(port, firewall.INGRESS_DIRECTION)
+            self._add_rules_by_security_group(port, firewall.EGRESS_DIRECTION)
+
+    def _get_device_name(self, port):
+        bridge = PFBaseOVS()
+        device_name = bridge.get_port_by_id(port['id'])
+        if '/' in device_name:
+            device_name = 'dl:' + device_name
+        return device_name
+
+    def _split_sgr_by_ethertype(self, security_group_rules):
+        ipv4_sg_rules = []
+        ipv6_sg_rules = []
+        for rule in security_group_rules:
+            if rule.get('ethertype') == constants.IPv4:
+                ipv4_sg_rules.append(rule)
+            elif rule.get('ethertype') == constants.IPv6:
+                if rule.get('protocol') in ['icmp', 'icmp6']:
+                    rule['protocol'] = 'ipv6-icmp'
+                ipv6_sg_rules.append(rule)
+        return ipv4_sg_rules, ipv6_sg_rules
+
+    def _select_sgr_by_direction(self, port, direction):
+        return [rule
+                for rule in port.get('security_group_rules', [])
+                if rule['direction'] == direction]
+
+    def _spoofing_rule(self, port, device_name, ipv4_rules, ipv6_rules):
+        # Fixed rules for traffic sourced from unspecified addresses: 0.0.0.0
+        # and ::
+        # Allow dhcp client discovery and request
+        ipv4_rules.append('pass out on %s proto udp from 0.0.0.0/32 port 68 '
+                          'to 255.255.255.255/32 port 67 label "%s"' %
+                          (device_name, self._get_label_number()))
+
+        # Allow neighbor solicitation and multicast listener discovery
+        # from the unspecified address for duplicate address detection
+        for icmp6_type in ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES:
+            ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp '
+                              'from ::/128 to ff02::/16 icmp6-type %s '
+                              'label "%s"' % (device_name, icmp6_type,
+                                              self._get_label_number()))
+
+        # Fixed rules for traffic after source address is verified
+        # Allow dhcp client renewal and rebinding
+        ipv4_rules.append('pass out on %s proto udp from port 68 to port 67 '
+                          'label "%s"' % (device_name,
+                                          self._get_label_number()))
+
+        # Drop Router Advts from the port.
+        ipv6_rules.append('block out quick on %s inet6 proto ipv6-icmp '
+                          'icmp6-type %s label "%s"' %
+                          (device_name, constants.ICMPV6_TYPE_RA,
+                           self._get_label_number()))
+        # Allow IPv6 ICMP traffic
+        ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp label "%s"' %
+                          (device_name, self._get_label_number()))
+        # Allow IPv6 DHCP Client traffic
+        ipv6_rules.append('pass out on %s inet6 proto udp from port 546 '
+                          'to port 547 label "%s"' %
+                          (device_name, self._get_label_number()))
+
+    def _drop_dhcp_rule(self, device_name, ipv4_rules, ipv6_rules):
+        # Drop dhcp packet from VM
+        ipv4_rules.append('block out quick on %s proto udp from port 67 '
+                          'to port 68 label "%s"' %
+                          (device_name, self._get_label_number()))
+        ipv6_rules.append('block out quick on %s inet6 proto udp '
+                          'from port 547 to port 546 label "%s"' %
+                          (device_name, self._get_label_number()))
+
+    def _accept_inbound_icmpv6(self, device_name, ipv6_pf_rules):
+        # Allow multicast listener, neighbor solicitation and
+        # neighbor advertisement into the instance
+        for icmp6_type in constants.ICMPV6_ALLOWED_TYPES:
+            ipv6_pf_rules.append('pass in on %s inet6 proto ipv6-icmp '
+                                 'icmp6-type %s label "%s"' %
+                                 (device_name, icmp6_type,
+                                  self._get_label_number()))
+
+    def _select_sg_rules_for_port(self, port, direction):
+        """Select rules from the security groups the port is member of."""
+        port_sg_ids = port.get('security_groups', [])
+        port_rules = []
+
+        for sg_id in port_sg_ids:
+            for rule in self.sg_rules.get(sg_id, []):
+                if rule['direction'] == direction:
+                    port_rules.extend(
+                        self._expand_sg_rule_with_remote_ips(
+                            rule, port, direction))
+        return port_rules
+
+    def _expand_sg_rule_with_remote_ips(self, rule, port, direction):
+        """Expand a remote group rule to rule per remote group IP."""
+        remote_group_id = rule.get('remote_group_id')
+        if remote_group_id:
+            ethertype = rule['ethertype']
+            port_ips = port.get('fixed_ips', [])
+            LOG.debug("Expanding rule: %s with remote IPs: %s" %
+                      (rule, self.sg_members[remote_group_id][ethertype]))
+            for ip in self.sg_members[remote_group_id][ethertype]:
+                if ip not in port_ips:
+                    ip_rule = rule.copy()
+                    direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[
+                        direction]
+                    ip_prefix = str(netaddr.IPNetwork(ip).cidr)
+                    ip_rule[direction_ip_prefix] = ip_prefix
+                    yield ip_rule
+        else:
+            yield rule
+
+    def _get_remote_sg_ids(self, port, direction=None):
+        sg_ids = port.get('security_groups', [])
+        remote_sg_ids = {constants.IPv4: set(), constants.IPv6: set()}
+        for sg_id in sg_ids:
+            for rule in self.sg_rules.get(sg_id, []):
+                if not direction or rule['direction'] == direction:
+                    remote_sg_id = rule.get('remote_group_id')
+                    ether_type = rule.get('ethertype')
+                    if remote_sg_id and ether_type:
+                        remote_sg_ids[ether_type].add(remote_sg_id)
+        return remote_sg_ids
+
+    def _add_pf_rules(self, port, device_name, direction, ipv4_pf_rules,
+                      ipv6_pf_rules):
+        if '/' in device_name:
+            instance_name, datalink = device_name.split('/')
+            instance_name = instance_name.split(':')[1]
+        else:
+            instance_name, datalink = (device_name, "")
+        self.pf.add_nested_anchor_rule(None, instance_name, None)
+        if direction == firewall.INGRESS_DIRECTION:
+            subanchor = '%s%s' % (datalink, '_in' if datalink else 'in')
+            new_anchor_rule = ['anchor "%s" in on %s all' % (subanchor,
+                                                             device_name)]
+        else:
+            subanchor = '%s%s' % (datalink, '_out' if datalink else 'out')
+            new_anchor_rule = ['anchor "%s" out on %s all' % (subanchor,
+                                                              device_name)]
+        existing_anchor_rules = self.pf.list_anchor_rules([instance_name])
+        final_anchor_rules = set(existing_anchor_rules) | set(new_anchor_rule)
+        self.pf.add_rules(list(final_anchor_rules), [instance_name])
+
+        # self.pf.add_nested_anchor_rule(None, anchor_name, anchor_option)
+        self.pf.add_rules(ipv4_pf_rules + ipv6_pf_rules,
+                          [instance_name, subanchor])
+        self.portid_to_devname[port['id']] = device_name
+
+    def _add_block_everything(self, device_name, direction, ipv4_pf_rules,
+                              ipv6_pf_rules):
+        ''' Add a generic block everything rule. The default security group
+        in OpenStack adds 'pass all egress traffic' and prevents all the
+        incoming traffic'''
+        ipv4_pf_rules.append('block %s on %s label "%s"' %
+                             (DIRECTION_PF_PARAM[direction], device_name,
+                              self._get_label_number()))
+        ipv6_pf_rules.append('block %s on %s inet6 label "%s"' %
+                             (DIRECTION_PF_PARAM[direction], device_name,
+                              self._get_label_number()))
+
+    def _add_rules_by_security_group(self, port, direction):
+        LOG.debug("Adding rules for Port: %s", port)
+
+        device_name = self._get_device_name(port)
+        if not device_name:
+            LOG.info(_LI("Could not find port: %s on the OVS bridge. Failed "
+                         "to add PF rules for that port"), port['id'])
+            return
+        # select rules for current port and direction
+        security_group_rules = self._select_sgr_by_direction(port, direction)
+        security_group_rules += self._select_sg_rules_for_port(port, direction)
+        # split groups by ip version
+        # for ipv4, 'pass' will be used
+        # for ipv6, 'pass inet6' will be used
+        ipv4_sg_rules, ipv6_sg_rules = self._split_sgr_by_ethertype(
+            security_group_rules)
+        ipv4_pf_rules = []
+        ipv6_pf_rules = []
+        self._add_block_everything(device_name, direction, ipv4_pf_rules,
+                                   ipv6_pf_rules)
+        # include fixed egress/ingress rules
+        if direction == firewall.EGRESS_DIRECTION:
+            self._add_fixed_egress_rules(port, device_name, ipv4_pf_rules,
+                                         ipv6_pf_rules)
+        elif direction == firewall.INGRESS_DIRECTION:
+            self._accept_inbound_icmpv6(device_name, ipv6_pf_rules)
+        # include IPv4 and IPv6 iptable rules from security group
+        LOG.debug("Converting %s IPv4 SG rules: %s" %
+                  (direction, ipv4_sg_rules))
+        ipv4_pf_rules += self._convert_sgr_to_pfr(device_name, direction,
+                                                  ipv4_sg_rules)
+        LOG.debug("... to %s IPv4 PF rules: %s" % (direction, ipv4_pf_rules))
+        LOG.debug("Converting %s IPv6 SG rules: %s" %
+                  (direction, ipv6_sg_rules))
+        ipv6_pf_rules += self._convert_sgr_to_pfr(device_name, direction,
+                                                  ipv6_sg_rules)
+        LOG.debug("... to %s IPv6 PF rules: %s" % (direction, ipv6_pf_rules))
+
+        self._add_pf_rules(port, device_name, direction, ipv4_pf_rules,
+                           ipv6_pf_rules)
+
+    def _add_fixed_egress_rules(self, port, device_name, ipv4_pf_rules,
+                                ipv6_pf_rules):
+        self._spoofing_rule(port, device_name, ipv4_pf_rules, ipv6_pf_rules)
+        self._drop_dhcp_rule(device_name, ipv4_pf_rules, ipv6_pf_rules)
+
+    def _protocol_param(self, protocol, pf_rule):
+        if protocol:
+            pf_rule.append('proto %s' % protocol)
+
+    def _port_param(self, protocol, port_range_min, port_range_max, pf_rule):
+        if port_range_min is None:
+            return
+        if protocol in ('tcp', 'udp'):
+            if port_range_min == port_range_max:
+                pf_rule.append('port %s' % port_range_min)
+            else:
+                pf_rule.append('port %s:%s' % (port_range_min,
+                                               port_range_max))
+        elif protocol in ('icmp', 'ipv6-icmp'):
+            icmp_type = 'icmp-type' if protocol == 'icmp' else 'icmp6-type'
+            pf_rule.append('%s %s' % (icmp_type, port_range_min))
+            if port_range_max is not None:
+                pf_rule.append('code %s' % port_range_max)
+
+    def _ip_prefix_param(self, direction, ip_prefix, pf_rule):
+        if ip_prefix != 'any':
+            if '/' not in ip_prefix:
+                # we need to convert it into a cidr
+                ip_prefix = c_utils.ip_to_cidr(ip_prefix)
+            elif ip_prefix.endswith('/0'):
+                ip_prefix = 'any'
+        direction = 'from' if direction == firewall.INGRESS_DIRECTION else 'to'
+        pf_rule.append('%s %s' % (direction, ip_prefix))
+
+    def _ip_prefix_port_param(self, direction, sg_rule, pf_rule):
+        protocol = sg_rule.get('protocol')
+        if direction == firewall.INGRESS_DIRECTION:
+            ip_prefix = sg_rule.get('source_ip_prefix')
+            ip_prefix = ip_prefix if ip_prefix else 'any'
+            self._ip_prefix_param(direction, ip_prefix, pf_rule)
+            self._port_param(protocol,
+                             sg_rule.get('source_port_range_min'),
+                             sg_rule.get('source_port_range_max'), pf_rule)
+            self._ip_prefix_param(firewall.EGRESS_DIRECTION, 'any', pf_rule)
+            self._port_param(protocol,
+                             sg_rule.get('port_range_min'),
+                             sg_rule.get('port_range_max'), pf_rule)
+        else:
+            self._ip_prefix_param(firewall.INGRESS_DIRECTION, 'any', pf_rule)
+            self._port_param(protocol,
+                             sg_rule.get('source_port_range_min'),
+                             sg_rule.get('source_port_range_max'), pf_rule)
+
+            ip_prefix = sg_rule.get('dest_ip_prefix')
+            ip_prefix = ip_prefix if ip_prefix else 'any'
+            self._ip_prefix_param(direction, ip_prefix, pf_rule)
+            self._port_param(protocol,
+                             sg_rule.get('port_range_min'),
+                             sg_rule.get('port_range_max'), pf_rule)
+
+    def _convert_sgr_to_pfr(self, device_name, direction,
+                            security_group_rules):
+        pf_rules = []
+        for sg_rule in security_group_rules:
+            pf_rule = ['pass']
+            pf_rule.append("%s on %s" % (DIRECTION_PF_PARAM[direction],
+                                         device_name))
+            if sg_rule.get('ethertype') == constants.IPv6:
+                pf_rule.append('inet6')
+            else:
+                pf_rule.append('inet')
+            protocol = sg_rule.get('protocol')
+            self._protocol_param(protocol, pf_rule)
+            self._ip_prefix_port_param(direction, sg_rule, pf_rule)
+            pf_rule.append('label "%s"' % self._get_label_number())
+            pf_rules.append(' '.join(pf_rule))
+        return pf_rules