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