components/openstack/neutron/files/agent/solaris/pf_firewall.py
changeset 7315 5cc40226273b
equal deleted inserted replaced
7314:014a673c1f62 7315:5cc40226273b
       
     1 # Copyright 2012, Nachi Ueno, NTT MCL, Inc.
       
     2 # All Rights Reserved.
       
     3 #
       
     4 # Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
       
     5 #
       
     6 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
       
     7 #    not use this file except in compliance with the License. You may obtain
       
     8 #    a copy of the License at
       
     9 #
       
    10 #         http://www.apache.org/licenses/LICENSE-2.0
       
    11 #
       
    12 #    Unless required by applicable law or agreed to in writing, software
       
    13 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
       
    14 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
       
    15 #    License for the specific language governing permissions and limitations
       
    16 #    under the License.
       
    17 
       
    18 import collections
       
    19 
       
    20 import netaddr
       
    21 from oslo_config import cfg
       
    22 from oslo_log import log as logging
       
    23 import six
       
    24 
       
    25 from neutron._i18n import _LI
       
    26 from neutron.agent.common import ovs_lib
       
    27 from neutron.agent import firewall
       
    28 from neutron.agent.solaris import packetfilter
       
    29 from neutron.common import constants
       
    30 from neutron.common import utils as c_utils
       
    31 
       
    32 
       
    33 LOG = logging.getLogger(__name__)
       
    34 ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES = [131, 135, 143]
       
    35 
       
    36 DIRECTION_PF_PARAM = {firewall.INGRESS_DIRECTION: 'in',
       
    37                       firewall.EGRESS_DIRECTION: 'out'}
       
    38 
       
    39 
       
    40 class PFBaseOVS(ovs_lib.BaseOVS):
       
    41     def get_port_by_id(self, port_id):
       
    42         ports = self.ovsdb.db_find(
       
    43             'Interface', ('external_ids', '=', {'iface-id': port_id}),
       
    44             columns=['name']).execute()
       
    45         if ports:
       
    46             return ports[0]['name']
       
    47         return None
       
    48 
       
    49 
       
    50 class PFFirewallDriver(firewall.FirewallDriver):
       
    51     """Driver which enforces security groups through PF rules.
       
    52 
       
    53     Please look at neutron.agent.firewall.FirewallDriver for more information
       
    54     on how the methods below are called from the Neutron Open vSwitch agent. It
       
    55     all starts at prepare_port_filter() and then _setup_pf_rules() has all the
       
    56     PF based logic to add correct rules on guest instance's port.
       
    57     """
       
    58 
       
    59     def __init__(self):
       
    60         self.pf = packetfilter.PacketFilter("_auto/neutron:ovs:agent",
       
    61                                             layer2=True)
       
    62         # List of port which has security group
       
    63         self.filtered_ports = {}
       
    64         self.unfiltered_ports = {}
       
    65         # List of security group rules for ports residing on this host
       
    66         self.sg_rules = {}
       
    67         # List of security group member ips for ports residing on this host
       
    68         self.sg_members = collections.defaultdict(
       
    69             lambda: collections.defaultdict(list))
       
    70         # Every PF rule needs to be labeled so that we can later kill the state
       
    71         # associated with that rule (using pfctl -k label -k 110). It is hard
       
    72         # to come up with a meaningfully named label for each PF rule, so we
       
    73         # are resorting to numbers here.
       
    74         self.label_num = 100
       
    75         self.portid_to_devname = {}
       
    76 
       
    77     def prepare_port_filter(self, port):
       
    78         LOG.debug("Preparing device (%s) filter", port['device'])
       
    79         self._setup_pf_rules(port)
       
    80 
       
    81     def apply_port_filter(self, port):
       
    82         """We never call this method
       
    83 
       
    84         It exists here to override abstract method of parent abstract class.
       
    85         """
       
    86         pass
       
    87 
       
    88     def update_port_filter(self, port):
       
    89         LOG.debug("Updating device (%s) filter", port['device'])
       
    90         self._setup_pf_rules(port, update=True)
       
    91 
       
    92     def remove_port_filter(self, port):
       
    93         LOG.debug("Removing device (%s) filter", port['device'])
       
    94         self.unfiltered_ports.pop(port['device'], None)
       
    95         self.filtered_ports.pop(port['device'], None)
       
    96         self._remove_rule_port_sec(port)
       
    97 
       
    98     @property
       
    99     def ports(self):
       
   100         return dict(self.filtered_ports, **self.unfiltered_ports)
       
   101 
       
   102     def update_security_group_rules(self, sg_id, sg_rules):
       
   103         LOG.debug("Update rules of security group %s(%s)" % (sg_id, sg_rules))
       
   104         self.sg_rules[sg_id] = sg_rules
       
   105 
       
   106     def update_security_group_members(self, sg_id, sg_members):
       
   107         LOG.debug("Update members of security group %s(%s)" %
       
   108                   (sg_id, sg_members))
       
   109         self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
       
   110 
       
   111     def security_group_updated(self, action_type, sec_group_ids,
       
   112                                device_ids=None):
       
   113         # TODO(gmoodalb): Extend this later to optimize handling of security
       
   114         # groups update
       
   115         pass
       
   116 
       
   117     def _get_label_number(self):
       
   118         self.label_num += 1
       
   119         return self.label_num
       
   120 
       
   121     def _remove_rule_port_sec(self, port):
       
   122         device_name = self.portid_to_devname.pop(port['id'], None)
       
   123         if not device_name:
       
   124             LOG.info(_LI("Could not find port: %s. Failed to remove PF rules "
       
   125                          "for that port"), port['id'])
       
   126             return
       
   127         LOG.debug("Removing PF rules for device_name(%s)" % device_name)
       
   128         # we need to remove both ingress and egress
       
   129         if '/' in device_name:
       
   130             instance_name, datalink = device_name.split('/')
       
   131             instance_name = instance_name.split(':')[1]
       
   132             ingress = '%s_in' % datalink
       
   133             egress = '%s_out' % datalink
       
   134         else:
       
   135             instance_name = device_name
       
   136             ingress = 'in'
       
   137             egress = 'out'
       
   138         existing_anchor_rules = set(self.pf.list_anchor_rules([instance_name]))
       
   139         existing_anchor_rules.discard('anchor "%s" in on %s all' %
       
   140                                       (ingress, device_name))
       
   141         existing_anchor_rules.discard('anchor "%s" out on %s all' %
       
   142                                       (egress, device_name))
       
   143         self.pf.add_rules(list(existing_anchor_rules), [instance_name])
       
   144         if existing_anchor_rules:
       
   145             self.pf.remove_anchor_recursively([instance_name, ingress])
       
   146             self.pf.remove_anchor_recursively([instance_name, egress])
       
   147         else:
       
   148             self.pf.remove_anchor_recursively([instance_name])
       
   149 
       
   150     def _setup_pf_rules(self, port, update=False):
       
   151         if not firewall.port_sec_enabled(port):
       
   152             self.unfiltered_ports[port['device']] = port
       
   153             self.filtered_ports.pop(port['device'], None)
       
   154             self._remove_rule_port_sec(port)
       
   155         else:
       
   156             self.filtered_ports[port['device']] = port
       
   157             self.unfiltered_ports.pop(port['device'], None)
       
   158             if update:
       
   159                 self._remove_rule_port_sec(port)
       
   160             self._add_rules_by_security_group(port, firewall.INGRESS_DIRECTION)
       
   161             self._add_rules_by_security_group(port, firewall.EGRESS_DIRECTION)
       
   162 
       
   163     def _get_device_name(self, port):
       
   164         bridge = PFBaseOVS()
       
   165         device_name = bridge.get_port_by_id(port['id'])
       
   166         if '/' in device_name:
       
   167             device_name = 'dl:' + device_name
       
   168         return device_name
       
   169 
       
   170     def _split_sgr_by_ethertype(self, security_group_rules):
       
   171         ipv4_sg_rules = []
       
   172         ipv6_sg_rules = []
       
   173         for rule in security_group_rules:
       
   174             if rule.get('ethertype') == constants.IPv4:
       
   175                 ipv4_sg_rules.append(rule)
       
   176             elif rule.get('ethertype') == constants.IPv6:
       
   177                 if rule.get('protocol') in ['icmp', 'icmp6']:
       
   178                     rule['protocol'] = 'ipv6-icmp'
       
   179                 ipv6_sg_rules.append(rule)
       
   180         return ipv4_sg_rules, ipv6_sg_rules
       
   181 
       
   182     def _select_sgr_by_direction(self, port, direction):
       
   183         return [rule
       
   184                 for rule in port.get('security_group_rules', [])
       
   185                 if rule['direction'] == direction]
       
   186 
       
   187     def _spoofing_rule(self, port, device_name, ipv4_rules, ipv6_rules):
       
   188         # Fixed rules for traffic sourced from unspecified addresses: 0.0.0.0
       
   189         # and ::
       
   190         # Allow dhcp client discovery and request
       
   191         ipv4_rules.append('pass out on %s proto udp from 0.0.0.0/32 port 68 '
       
   192                           'to 255.255.255.255/32 port 67 label "%s"' %
       
   193                           (device_name, self._get_label_number()))
       
   194 
       
   195         # Allow neighbor solicitation and multicast listener discovery
       
   196         # from the unspecified address for duplicate address detection
       
   197         for icmp6_type in ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES:
       
   198             ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp '
       
   199                               'from ::/128 to ff02::/16 icmp6-type %s '
       
   200                               'label "%s"' % (device_name, icmp6_type,
       
   201                                               self._get_label_number()))
       
   202 
       
   203         # Fixed rules for traffic after source address is verified
       
   204         # Allow dhcp client renewal and rebinding
       
   205         ipv4_rules.append('pass out on %s proto udp from port 68 to port 67 '
       
   206                           'label "%s"' % (device_name,
       
   207                                           self._get_label_number()))
       
   208 
       
   209         # Drop Router Advts from the port.
       
   210         ipv6_rules.append('block out quick on %s inet6 proto ipv6-icmp '
       
   211                           'icmp6-type %s label "%s"' %
       
   212                           (device_name, constants.ICMPV6_TYPE_RA,
       
   213                            self._get_label_number()))
       
   214         # Allow IPv6 ICMP traffic
       
   215         ipv6_rules.append('pass out on %s inet6 proto ipv6-icmp label "%s"' %
       
   216                           (device_name, self._get_label_number()))
       
   217         # Allow IPv6 DHCP Client traffic
       
   218         ipv6_rules.append('pass out on %s inet6 proto udp from port 546 '
       
   219                           'to port 547 label "%s"' %
       
   220                           (device_name, self._get_label_number()))
       
   221 
       
   222     def _drop_dhcp_rule(self, device_name, ipv4_rules, ipv6_rules):
       
   223         # Drop dhcp packet from VM
       
   224         ipv4_rules.append('block out quick on %s proto udp from port 67 '
       
   225                           'to port 68 label "%s"' %
       
   226                           (device_name, self._get_label_number()))
       
   227         ipv6_rules.append('block out quick on %s inet6 proto udp '
       
   228                           'from port 547 to port 546 label "%s"' %
       
   229                           (device_name, self._get_label_number()))
       
   230 
       
   231     def _accept_inbound_icmpv6(self, device_name, ipv6_pf_rules):
       
   232         # Allow multicast listener, neighbor solicitation and
       
   233         # neighbor advertisement into the instance
       
   234         for icmp6_type in constants.ICMPV6_ALLOWED_TYPES:
       
   235             ipv6_pf_rules.append('pass in on %s inet6 proto ipv6-icmp '
       
   236                                  'icmp6-type %s label "%s"' %
       
   237                                  (device_name, icmp6_type,
       
   238                                   self._get_label_number()))
       
   239 
       
   240     def _select_sg_rules_for_port(self, port, direction):
       
   241         """Select rules from the security groups the port is member of."""
       
   242         port_sg_ids = port.get('security_groups', [])
       
   243         port_rules = []
       
   244 
       
   245         for sg_id in port_sg_ids:
       
   246             for rule in self.sg_rules.get(sg_id, []):
       
   247                 if rule['direction'] == direction:
       
   248                     port_rules.extend(
       
   249                         self._expand_sg_rule_with_remote_ips(
       
   250                             rule, port, direction))
       
   251         return port_rules
       
   252 
       
   253     def _expand_sg_rule_with_remote_ips(self, rule, port, direction):
       
   254         """Expand a remote group rule to rule per remote group IP."""
       
   255         remote_group_id = rule.get('remote_group_id')
       
   256         if remote_group_id:
       
   257             ethertype = rule['ethertype']
       
   258             port_ips = port.get('fixed_ips', [])
       
   259             LOG.debug("Expanding rule: %s with remote IPs: %s" %
       
   260                       (rule, self.sg_members[remote_group_id][ethertype]))
       
   261             for ip in self.sg_members[remote_group_id][ethertype]:
       
   262                 if ip not in port_ips:
       
   263                     ip_rule = rule.copy()
       
   264                     direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[
       
   265                         direction]
       
   266                     ip_prefix = str(netaddr.IPNetwork(ip).cidr)
       
   267                     ip_rule[direction_ip_prefix] = ip_prefix
       
   268                     yield ip_rule
       
   269         else:
       
   270             yield rule
       
   271 
       
   272     def _get_remote_sg_ids(self, port, direction=None):
       
   273         sg_ids = port.get('security_groups', [])
       
   274         remote_sg_ids = {constants.IPv4: set(), constants.IPv6: set()}
       
   275         for sg_id in sg_ids:
       
   276             for rule in self.sg_rules.get(sg_id, []):
       
   277                 if not direction or rule['direction'] == direction:
       
   278                     remote_sg_id = rule.get('remote_group_id')
       
   279                     ether_type = rule.get('ethertype')
       
   280                     if remote_sg_id and ether_type:
       
   281                         remote_sg_ids[ether_type].add(remote_sg_id)
       
   282         return remote_sg_ids
       
   283 
       
   284     def _add_pf_rules(self, port, device_name, direction, ipv4_pf_rules,
       
   285                       ipv6_pf_rules):
       
   286         if '/' in device_name:
       
   287             instance_name, datalink = device_name.split('/')
       
   288             instance_name = instance_name.split(':')[1]
       
   289         else:
       
   290             instance_name, datalink = (device_name, "")
       
   291         self.pf.add_nested_anchor_rule(None, instance_name, None)
       
   292         if direction == firewall.INGRESS_DIRECTION:
       
   293             subanchor = '%s%s' % (datalink, '_in' if datalink else 'in')
       
   294             new_anchor_rule = ['anchor "%s" in on %s all' % (subanchor,
       
   295                                                              device_name)]
       
   296         else:
       
   297             subanchor = '%s%s' % (datalink, '_out' if datalink else 'out')
       
   298             new_anchor_rule = ['anchor "%s" out on %s all' % (subanchor,
       
   299                                                               device_name)]
       
   300         existing_anchor_rules = self.pf.list_anchor_rules([instance_name])
       
   301         final_anchor_rules = set(existing_anchor_rules) | set(new_anchor_rule)
       
   302         self.pf.add_rules(list(final_anchor_rules), [instance_name])
       
   303 
       
   304         # self.pf.add_nested_anchor_rule(None, anchor_name, anchor_option)
       
   305         self.pf.add_rules(ipv4_pf_rules + ipv6_pf_rules,
       
   306                           [instance_name, subanchor])
       
   307         self.portid_to_devname[port['id']] = device_name
       
   308 
       
   309     def _add_block_everything(self, device_name, direction, ipv4_pf_rules,
       
   310                               ipv6_pf_rules):
       
   311         ''' Add a generic block everything rule. The default security group
       
   312         in OpenStack adds 'pass all egress traffic' and prevents all the
       
   313         incoming traffic'''
       
   314         ipv4_pf_rules.append('block %s on %s label "%s"' %
       
   315                              (DIRECTION_PF_PARAM[direction], device_name,
       
   316                               self._get_label_number()))
       
   317         ipv6_pf_rules.append('block %s on %s inet6 label "%s"' %
       
   318                              (DIRECTION_PF_PARAM[direction], device_name,
       
   319                               self._get_label_number()))
       
   320 
       
   321     def _add_rules_by_security_group(self, port, direction):
       
   322         LOG.debug("Adding rules for Port: %s", port)
       
   323 
       
   324         device_name = self._get_device_name(port)
       
   325         if not device_name:
       
   326             LOG.info(_LI("Could not find port: %s on the OVS bridge. Failed "
       
   327                          "to add PF rules for that port"), port['id'])
       
   328             return
       
   329         # select rules for current port and direction
       
   330         security_group_rules = self._select_sgr_by_direction(port, direction)
       
   331         security_group_rules += self._select_sg_rules_for_port(port, direction)
       
   332         # split groups by ip version
       
   333         # for ipv4, 'pass' will be used
       
   334         # for ipv6, 'pass inet6' will be used
       
   335         ipv4_sg_rules, ipv6_sg_rules = self._split_sgr_by_ethertype(
       
   336             security_group_rules)
       
   337         ipv4_pf_rules = []
       
   338         ipv6_pf_rules = []
       
   339         self._add_block_everything(device_name, direction, ipv4_pf_rules,
       
   340                                    ipv6_pf_rules)
       
   341         # include fixed egress/ingress rules
       
   342         if direction == firewall.EGRESS_DIRECTION:
       
   343             self._add_fixed_egress_rules(port, device_name, ipv4_pf_rules,
       
   344                                          ipv6_pf_rules)
       
   345         elif direction == firewall.INGRESS_DIRECTION:
       
   346             self._accept_inbound_icmpv6(device_name, ipv6_pf_rules)
       
   347         # include IPv4 and IPv6 iptable rules from security group
       
   348         LOG.debug("Converting %s IPv4 SG rules: %s" %
       
   349                   (direction, ipv4_sg_rules))
       
   350         ipv4_pf_rules += self._convert_sgr_to_pfr(device_name, direction,
       
   351                                                   ipv4_sg_rules)
       
   352         LOG.debug("... to %s IPv4 PF rules: %s" % (direction, ipv4_pf_rules))
       
   353         LOG.debug("Converting %s IPv6 SG rules: %s" %
       
   354                   (direction, ipv6_sg_rules))
       
   355         ipv6_pf_rules += self._convert_sgr_to_pfr(device_name, direction,
       
   356                                                   ipv6_sg_rules)
       
   357         LOG.debug("... to %s IPv6 PF rules: %s" % (direction, ipv6_pf_rules))
       
   358 
       
   359         self._add_pf_rules(port, device_name, direction, ipv4_pf_rules,
       
   360                            ipv6_pf_rules)
       
   361 
       
   362     def _add_fixed_egress_rules(self, port, device_name, ipv4_pf_rules,
       
   363                                 ipv6_pf_rules):
       
   364         self._spoofing_rule(port, device_name, ipv4_pf_rules, ipv6_pf_rules)
       
   365         self._drop_dhcp_rule(device_name, ipv4_pf_rules, ipv6_pf_rules)
       
   366 
       
   367     def _protocol_param(self, protocol, pf_rule):
       
   368         if protocol:
       
   369             pf_rule.append('proto %s' % protocol)
       
   370 
       
   371     def _port_param(self, protocol, port_range_min, port_range_max, pf_rule):
       
   372         if port_range_min is None:
       
   373             return
       
   374         if protocol in ('tcp', 'udp'):
       
   375             if port_range_min == port_range_max:
       
   376                 pf_rule.append('port %s' % port_range_min)
       
   377             else:
       
   378                 pf_rule.append('port %s:%s' % (port_range_min,
       
   379                                                port_range_max))
       
   380         elif protocol in ('icmp', 'ipv6-icmp'):
       
   381             icmp_type = 'icmp-type' if protocol == 'icmp' else 'icmp6-type'
       
   382             pf_rule.append('%s %s' % (icmp_type, port_range_min))
       
   383             if port_range_max is not None:
       
   384                 pf_rule.append('code %s' % port_range_max)
       
   385 
       
   386     def _ip_prefix_param(self, direction, ip_prefix, pf_rule):
       
   387         if ip_prefix != 'any':
       
   388             if '/' not in ip_prefix:
       
   389                 # we need to convert it into a cidr
       
   390                 ip_prefix = c_utils.ip_to_cidr(ip_prefix)
       
   391             elif ip_prefix.endswith('/0'):
       
   392                 ip_prefix = 'any'
       
   393         direction = 'from' if direction == firewall.INGRESS_DIRECTION else 'to'
       
   394         pf_rule.append('%s %s' % (direction, ip_prefix))
       
   395 
       
   396     def _ip_prefix_port_param(self, direction, sg_rule, pf_rule):
       
   397         protocol = sg_rule.get('protocol')
       
   398         if direction == firewall.INGRESS_DIRECTION:
       
   399             ip_prefix = sg_rule.get('source_ip_prefix')
       
   400             ip_prefix = ip_prefix if ip_prefix else 'any'
       
   401             self._ip_prefix_param(direction, ip_prefix, pf_rule)
       
   402             self._port_param(protocol,
       
   403                              sg_rule.get('source_port_range_min'),
       
   404                              sg_rule.get('source_port_range_max'), pf_rule)
       
   405             self._ip_prefix_param(firewall.EGRESS_DIRECTION, 'any', pf_rule)
       
   406             self._port_param(protocol,
       
   407                              sg_rule.get('port_range_min'),
       
   408                              sg_rule.get('port_range_max'), pf_rule)
       
   409         else:
       
   410             self._ip_prefix_param(firewall.INGRESS_DIRECTION, 'any', pf_rule)
       
   411             self._port_param(protocol,
       
   412                              sg_rule.get('source_port_range_min'),
       
   413                              sg_rule.get('source_port_range_max'), pf_rule)
       
   414 
       
   415             ip_prefix = sg_rule.get('dest_ip_prefix')
       
   416             ip_prefix = ip_prefix if ip_prefix else 'any'
       
   417             self._ip_prefix_param(direction, ip_prefix, pf_rule)
       
   418             self._port_param(protocol,
       
   419                              sg_rule.get('port_range_min'),
       
   420                              sg_rule.get('port_range_max'), pf_rule)
       
   421 
       
   422     def _convert_sgr_to_pfr(self, device_name, direction,
       
   423                             security_group_rules):
       
   424         pf_rules = []
       
   425         for sg_rule in security_group_rules:
       
   426             pf_rule = ['pass']
       
   427             pf_rule.append("%s on %s" % (DIRECTION_PF_PARAM[direction],
       
   428                                          device_name))
       
   429             if sg_rule.get('ethertype') == constants.IPv6:
       
   430                 pf_rule.append('inet6')
       
   431             else:
       
   432                 pf_rule.append('inet')
       
   433             protocol = sg_rule.get('protocol')
       
   434             self._protocol_param(protocol, pf_rule)
       
   435             self._ip_prefix_port_param(direction, sg_rule, pf_rule)
       
   436             pf_rule.append('label "%s"' % self._get_label_number())
       
   437             pf_rules.append(' '.join(pf_rule))
       
   438         return pf_rules