diff --git a/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/__init__.py b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/__init__.py new file mode 100644 index 000000000..299d82632 --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2015 +# 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. + +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import firewall + +OVSFirewallDriver = firewall.OVSFirewallDriver diff --git a/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/constants.py b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/constants.py new file mode 100644 index 000000000..4d148e0c7 --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/constants.py @@ -0,0 +1,64 @@ +# Copyright 2015 +# 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. + +from neutron_lib import constants + +from neutron.common import constants as n_const + +OF_STATE_NOT_TRACKED = "-trk" +OF_STATE_TRACKED = "+trk" +OF_STATE_NEW_NOT_ESTABLISHED = "+new-est" +OF_STATE_NOT_ESTABLISHED = "-est" +OF_STATE_ESTABLISHED = "+est" +OF_STATE_ESTABLISHED_NOT_REPLY = "+est-rel-rpl" +OF_STATE_ESTABLISHED_REPLY = "+est-rel+rpl" +OF_STATE_RELATED = "-new-est+rel-inv" +OF_STATE_INVALID = "+trk+inv" +OF_STATE_NEW = "+new" +OF_STATE_NOT_REPLY_NOT_NEW = "-new-rpl" + +CT_MARK_NORMAL = '0x0' +CT_MARK_INVALID = '0x1' + +REG_PORT = 5 +REG_NET = 6 + +FW_BASE_EGRESS_TABLE = 64 +FW_RULES_EGRESS_TABLE = 65 +FW_ACCEPT_OR_INGRESS_TABLE = 66 +FW_BASE_INGRESS_TABLE = 68 +FW_RULES_INGRESS_TABLE = 69 + +OVS_FIREWALL_TABLES = ( + FW_BASE_EGRESS_TABLE, + FW_RULES_EGRESS_TABLE, + FW_ACCEPT_OR_INGRESS_TABLE, + FW_BASE_INGRESS_TABLE, + FW_RULES_INGRESS_TABLE, +) + +PROTOCOLS_WITH_PORTS = (constants.PROTO_NAME_SCTP, + constants.PROTO_NAME_TCP, + constants.PROTO_NAME_UDP) + +# Only map protocols that need special handling +REVERSE_IP_PROTOCOL_MAP_WITH_PORTS = { + constants.IP_PROTOCOL_MAP[proto]: proto for proto in + PROTOCOLS_WITH_PORTS} + +ethertype_to_dl_type_map = { + constants.IPv4: n_const.ETHERTYPE_IP, + constants.IPv6: n_const.ETHERTYPE_IPV6, +} diff --git a/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/exceptions.py b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/exceptions.py new file mode 100644 index 000000000..bd7618cf9 --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/exceptions.py @@ -0,0 +1,26 @@ +# Copyright 2016, Red Hat, Inc. +# 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. + +from neutron_lib import exceptions + +from neutron_fwaas._i18n import _ + + +class OVSFWaaSPortNotFound(exceptions.NeutronException): + message = _("Port %(port_id)s is not managed by this agent.") + + +class OVSFWaaSTagNotFound(exceptions.NeutronException): + message = _("Cannot get vlan tag for port %(port_id)s.") diff --git a/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/firewall.py b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/firewall.py new file mode 100644 index 000000000..0ae99f661 --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/firewall.py @@ -0,0 +1,967 @@ +# Copyright 2015 +# 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 netaddr + +from neutron_lib import constants as lib_const +from oslo_log import log as logging +from oslo_utils import netutils + +from neutron.agent import firewall +from neutron.common import constants +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts + +from neutron_fwaas.services.firewall.drivers.linux.l2 import driver_base +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import constants as fwaas_ovs_consts +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import exceptions +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import rules + +LOG = logging.getLogger(__name__) + +ACTION_ALLOW = 'allow' + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver. +def _replace_register(flow_params, register_number, register_value): + """Replace value from flows to given register number + + 'register_value' key in dictionary will be replaced by register number + given by 'register_number' + + :param flow_params: Dictionary containing defined flows + :param register_number: The number of register where value will be stored + :param register_value: Key to be replaced by register number + + """ + try: + reg_port = flow_params[register_value] + del flow_params[register_value] + flow_params['reg{:d}'.format(register_number)] = reg_port + except KeyError: + pass + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# differs only in constants REG_PORT/REG_NET. +def create_reg_numbers(flow_params): + """Replace reg_(port|net) values with defined register numbers""" + _replace_register(flow_params, fwaas_ovs_consts.REG_PORT, 'reg_port') + _replace_register(flow_params, fwaas_ovs_consts.REG_NET, 'reg_net') + + +class FirewallGroup(object): + def __init__(self, id_): + self.id = id_ + self.ingress_rules = [] + self.egress_rules = [] + self.members = {} + self.ports = set() + + def update_rules(self, ingress_rules, egress_rules): + """Update firewall group with ingress/egress rules. + + If a rule has a protocol field, it is normalized to a number + here in order to ease later processing. + """ + def _translate_protocol_to_number(rule): + protocol = rule.get('protocol') + if protocol is not None: + if protocol.isdigit(): + rule['protocol'] = int(protocol) + elif (rule.get('ethertype') == lib_const.IPv6 and + protocol == lib_const.PROTO_NAME_ICMP): + rule['protocol'] = lib_const.PROTO_NUM_IPV6_ICMP + else: + rule['protocol'] = lib_const.IP_PROTOCOL_MAP.get( + protocol, protocol) + return rule + + self.ingress_rules = [_translate_protocol_to_number(ir) + for ir in ingress_rules] + self.egress_rules = [_translate_protocol_to_number(er) + for er in egress_rules] + + def get_ethertype_filtered_addresses(self, ethertype, + exclude_addresses=None): + exclude_addresses = set(exclude_addresses if exclude_addresses else []) + group_addresses = set(self.members.get(ethertype, [])) + return list(group_addresses - exclude_addresses) + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# differs only in firewall groups list field name +class OFPort(object): + def __init__(self, port_dict, ovs_port, vlan_tag): + self.id = port_dict['device'] + self.vlan_tag = vlan_tag + self.mac = ovs_port.vif_mac + self.lla_address = str(netutils.get_ipv6_addr_by_EUI64( + lib_const.IPv6_LLA_PREFIX, self.mac)) + self.ofport = ovs_port.ofport + self.fw_group = None + self.fixed_ips = port_dict.get('fixed_ips', []) + self.neutron_port_dict = port_dict.copy() + self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict, version=4) + self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict, version=6) + + @staticmethod + def _get_allowed_pairs(port_dict, version): + aap_dict = port_dict.get('allowed_address_pairs', set()) + return {(aap['mac_address'], aap['ip_address']) for aap in aap_dict + if netaddr.IPNetwork(aap['ip_address']).version == version} + + @property + def all_allowed_macs(self): + macs = {item[0] for item in self.allowed_pairs_v4.union( + self.allowed_pairs_v6)} + macs.add(self.mac) + return macs + + @property + def ipv4_addresses(self): + return [ip_addr for ip_addr in + [fixed_ip['ip_address'] for fixed_ip in self.fixed_ips] + if netaddr.IPAddress(ip_addr).version == 4] + + @property + def ipv6_addresses(self): + return [ip_addr for ip_addr in + [fixed_ip['ip_address'] for fixed_ip in self.fixed_ips] + if netaddr.IPAddress(ip_addr).version == 6] + + def update(self, port_dict): + self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict, + version=4) + self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict, + version=6) + # Neighbour discovery uses LLA + self.allowed_pairs_v6.add((self.mac, self.lla_address)) + self.fixed_ips = port_dict.get('fixed_ips', []) + self.neutron_port_dict = port_dict.copy() + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# differs in methods name [s/sg/fwg] and update_rules method. +class FWGPortMap(object): + def __init__(self): + self.ports = {} + self.fw_groups = {} + # Maps port_id to ofport number + self.unfiltered = {} + + def get_fwg(self, fwg_id): + return self.fw_groups.get(fwg_id, None) + + def get_or_create_fwg(self, fwg_id): + fw_group = self.get_fwg(fwg_id) + if not fw_group: + fw_group = FirewallGroup(fwg_id) + self.fw_groups[fwg_id] = fw_group + return fw_group + + def delete_fwg(self, fwg_id): + del self.fw_groups[fwg_id] + + # XXX NOTE(ivasilevskaya) couldn't find any logical definition why + # firewall_group should come as 3rd argument instead of adding fwg_id + # to port_dict. Removed in favor of SG api + def create_port(self, port, port_dict): + self.ports[port.id] = port + self.update_port(port, port_dict) + + # XXX NOTE(ivasilevskaya) couldn't find any logical definition why + # firewall_group should come as 3rd argument instead of adding fwg_id + # to port_dict. Removed in favor of SG api + def update_port(self, port, port_dict): + for fw_group in self.fw_groups.values(): + fw_group.ports.discard(port) + + fw_group = self.get_or_create_fwg(port_dict['firewall_group']) + port.fw_group = fw_group + fw_group.ports.add(port) + port.update(port_dict) + + def remove_port(self, port): + if port.fw_group: + port.fw_group.ports.discard(port) + del self.ports[port.id] + + def update_rules(self, fwg_id, ingress_rules, egress_rules): + fw_group = self.get_or_create_fwg(fwg_id) + fw_group.update_rules(ingress_rules, egress_rules) + + def update_members(self, fwg_id, members): + fw_group = self.get_or_create_fwg(fwg_id) + fw_group.members = members + + +# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that +# doesn't have a conjunction manager because no remote_group_id concept is +# applicable to firewall groups +class OVSFirewallDriver(driver_base.FirewallL2DriverBase): + REQUIRED_PROTOCOLS = [ + ovs_consts.OPENFLOW10, + ovs_consts.OPENFLOW11, + ovs_consts.OPENFLOW12, + ovs_consts.OPENFLOW13, + ovs_consts.OPENFLOW14, + ] + + provides_arp_spoofing_protection = True + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver. + # This driver won't have any conj_manager logic because there is no concept + # of remote_group_id for firewall groups (that I know of at least) + def __init__(self, integration_bridge, sg_enabled=False): + """Initialize object + + :param integration_bridge: Bridge on which openflow rules will be + applied + + """ + self.int_br = self.initialize_bridge(integration_bridge) + self.fwg_port_map = FWGPortMap() + self.fwg_to_delete = set() + self._deferred = False + self.sg_enabled = sg_enabled + self._drop_all_unmatched_flows() + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _accept_flow(self, **flow): + for f in rules.create_accept_flows(flow): + self._add_flow(**f) + + def _drop_flow(self, **flow): + for f in rules.create_drop_flows(flow): + self._add_flow(**f) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _add_flow(self, **kwargs): + dl_type = kwargs.get('dl_type') + create_reg_numbers(kwargs) + if isinstance(dl_type, int): + kwargs['dl_type'] = "0x{:04x}".format(dl_type) + if self._deferred: + self.int_br.add_flow(**kwargs) + else: + self.int_br.br.add_flow(**kwargs) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _delete_flows(self, **kwargs): + create_reg_numbers(kwargs) + if self._deferred: + self.int_br.delete_flows(**kwargs) + else: + self.int_br.br.delete_flows(**kwargs) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def _strict_delete_flow(self, **kwargs): + """Delete given flow right away even if bridge is deferred. + + Delete command will use strict delete. + """ + create_reg_numbers(kwargs) + self.int_br.br.delete_flows(strict=True, **kwargs) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + @staticmethod + def initialize_bridge(int_br): + int_br.add_protocols(*OVSFirewallDriver.REQUIRED_PROTOCOLS) + return int_br.deferred(full_ordered=True) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver, + # differs in constants + def _drop_all_unmatched_flows(self): + for table in fwaas_ovs_consts.OVS_FIREWALL_TABLES: + self.int_br.br.add_flow(table=table, priority=0, actions='drop') + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def get_ovs_port(self, port_id): + ovs_port = self.int_br.br.get_vif_port_by_id(port_id) + if not ovs_port: + raise exceptions.OVSFWaaSPortNotFound(port_id=port_id) + return ovs_port + + def _get_port_vlan_tag(self, port): + vlan_tag = port.get('lvlan', None) + if not vlan_tag: + raise exceptions.OVSFWaaSTagNotFound(port_id=port['device']) + return vlan_tag + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def get_ofport(self, port): + port_id = port['device'] + return self.fwg_port_map.ports.get(port_id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver, + # self.sg_port_map -> self.fwg_port_map + def get_or_create_ofport(self, port): + """Get ofport specified by port['device'], checking and reflecting + ofport changes. + If ofport is nonexistent, create and return one. + """ + port_id = port['device'] + ovs_port = self.get_ovs_port(port_id) + try: + of_port = self.fwg_port_map.ports[port_id] + except KeyError: + port_vlan_id = self._get_port_vlan_tag(port) + of_port = OFPort(port, ovs_port, port_vlan_id) + self.fwg_port_map.create_port(of_port, port) + else: + if of_port.ofport != ovs_port.ofport: + self.fwg_port_map.remove_port(of_port) + of_port = OFPort(port, ovs_port, of_port.vlan_tag) + self.fwg_port_map.update_port(of_port, port) + + return of_port + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def is_port_managed(self, port): + return port['device'] in self.fwg_port_map.ports + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def prepare_port_filter(self, port): + # NOTE(annp): port no security should be handled by security group in + # co-existence mode, otherwise(standalone mode) fwg will handle it. + if not firewall.port_sec_enabled(port) and not self.sg_enabled: + self._initialize_egress_no_port_security(port) + return + old_of_port = self.get_ofport(port) + # Make sure delete old allow_address_pair MACs because + # allow_address_pair MACs will be updated in + # self.get_or_create_ofport(port) + if old_of_port: + LOG.error("Initializing port %s that was already " + "initialized.", + port['device']) + self.delete_all_port_flows(old_of_port) + of_port = self.get_or_create_ofport(port) + self.initialize_port_flows(of_port) + self.add_flows_from_rules(of_port) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def update_port_filter(self, port): + """Update rules for given port + + Current existing filtering rules are removed and new ones are generated + based on current loaded firewall group rules and members. + + Note: port no security should be handled by security group in + co-existence mode, otherwise fwg will handle it. + + """ + if not firewall.port_sec_enabled(port) and not self.sg_enabled: + self.remove_port_filter(port) + self._initialize_egress_no_port_security(port) + return + elif not self.is_port_managed(port): + if not self.sg_enabled: + self._remove_egress_no_port_security(port['device']) + self.prepare_port_filter(port) + return + + old_of_port = self.get_ofport(port) + of_port = self.get_or_create_ofport(port) + # TODO(jlibosva): Handle firewall blink + self.delete_all_port_flows(old_of_port) + self.initialize_port_flows(of_port) + self.add_flows_from_rules(of_port) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver, + # sg_port_map -> fwg_port_map + def remove_port_filter(self, port): + """Remove port from firewall + + All flows related to this port are removed from ovs. Port is also + removed from ports managed by this firewall. + + """ + if self.is_port_managed(port): + of_port = self.get_ofport(port) + self.delete_all_port_flows(of_port) + self.fwg_port_map.remove_port(of_port) + self._schedule_fwg_deletion_maybe(of_port.fw_group.id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with ingress\egress rules arguments instead of single rules + def update_firewall_group_rules(self, fwg_id, ingress_rules, egress_rules): + self.fwg_port_map.update_rules(fwg_id, ingress_rules, egress_rules) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with sg_port_map -> fwg_port_map + def _schedule_fwg_deletion_maybe(self, fwg_id): + """Schedule possible deletion of the given firewall group. + + This function must be called when the number of ports + associated to fwg_id drops to zero, as it isn't possible + to know FWG deletions from agents due to RPC API design. + """ + fwg_group = self.fwg_port_map.get_or_create_fwg(fwg_id) + if not fwg_group.members or not fwg_group.ports: + self.fwg_to_delete.add(fwg_id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with sg_port_map -> fwg_port_map + def _cleanup_stale_fwg(self): + fwg_to_delete = self.fwg_to_delete + self.fwg_to_delete = set() + + for fwg_id in fwg_to_delete: + fw_group = self.fwg_port_map.get_fwg(fwg_id) + if fw_group.members and fw_group.ports: + # firewall group is still in use + continue + + self.fwg_port_map.delete_fwg(fwg_id) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def filter_defer_apply_on(self): + self._deferred = True + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + def filter_defer_apply_off(self): + if self._deferred: + self._cleanup_stale_fwg() + self.int_br.apply_flows() + self._deferred = False + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # with sg_port_map -> fwg_port_map + @property + def ports(self): + return {id_: port.neutron_port_dict + for id_, port in self.fwg_port_map.ports.items()} + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def initialize_port_flows(self, port): + """Set base flows for port + + :param port: OFPort instance + + """ + # Identify egress flow + self._add_flow( + table=ovs_consts.TRANSIENT_TABLE, + priority=105, + in_port=port.ofport, + actions='set_field:{:d}->reg{:d},' + 'set_field:{:d}->reg{:d},' + 'resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + port.vlan_tag, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_BASE_EGRESS_TABLE) + ) + + # Identify ingress flows after egress filtering + for mac_addr in port.all_allowed_macs: + self._add_flow( + table=ovs_consts.TRANSIENT_TABLE, + priority=95, + dl_dst=mac_addr, + dl_vlan='0x%x' % port.vlan_tag, + actions='set_field:{:d}->reg{:d},' + 'set_field:{:d}->reg{:d},' + 'strip_vlan,resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + port.vlan_tag, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + ) + + self._initialize_egress(port) + self._initialize_ingress(port) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_egress_ipv6_icmp(self, port): + for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=constants.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=icmp_type, + actions='normal' + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) and exception classes + def _initialize_egress_no_port_security(self, port): + port_id = port['device'] + try: + ovs_port = self.get_ovs_port(port_id) + vlan_tag = self._get_port_vlan_tag(port) + except exceptions.OVSFWaaSTagNotFound: + # It's a patch port, don't set anything + return + except exceptions.OVSFWaaSPortNotFound as not_found_e: + LOG.error("Initializing unfiltered port %(port_id)s that does not " + "exist in ovsdb: %(err)s.", + {'port_id': port_id, + 'err': not_found_e}) + return + self.fwg_port_map.unfiltered[port_id] = ovs_port.ofport + self._add_flow( + table=ovs_consts.TRANSIENT_TABLE, + priority=100, + in_port=ovs_port.ofport, + actions='set_field:%d->reg%d,' + 'set_field:%d->reg%d,' + 'resubmit(,%d)' % ( + ovs_port.ofport, + fwaas_ovs_consts.REG_PORT, + vlan_tag, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=80, + reg_port=ovs_port.ofport, + actions='normal' + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _remove_egress_no_port_security(self, port_id): + try: + ofport = self.fwg_port_map.unfiltered[port_id] + except KeyError: + LOG.debug("Port %s is not handled by the firewall.", port_id) + return + self._delete_flows( + table=ovs_consts.TRANSIENT_TABLE, + in_port=ofport + ) + self._delete_flows( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + reg_port=ofport + ) + del self.fwg_port_map.unfiltered[port_id] + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_egress(self, port): + """Identify egress traffic and send it to egress base""" + self._initialize_egress_ipv6_icmp(port) + + # Apply mac/ip pairs for IPv4 + allowed_pairs = port.allowed_pairs_v4.union( + {(port.mac, ip_addr) for ip_addr in port.ipv4_addresses}) + for mac_addr, ip_addr in allowed_pairs: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg_port=port.ofport, + dl_src=mac_addr, + dl_type=constants.ETHERTYPE_ARP, + arp_spa=ip_addr, + actions='normal' + ) + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=65, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + dl_type=constants.ETHERTYPE_IP, + in_port=port.ofport, + dl_src=mac_addr, + nw_src=ip_addr, + actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format( + fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + fwaas_ovs_consts.REG_NET) + ) + + # Apply mac/ip pairs for IPv6 + allowed_pairs = port.allowed_pairs_v6.union( + {(port.mac, ip_addr) for ip_addr in port.ipv6_addresses}) + for mac_addr, ip_addr in allowed_pairs: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=65, + reg_port=port.ofport, + in_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + dl_type=constants.ETHERTYPE_IPV6, + dl_src=mac_addr, + ipv6_src=ip_addr, + actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format( + fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + fwaas_ovs_consts.REG_NET) + ) + + # DHCP discovery + for dl_type, src_port, dst_port in ( + (constants.ETHERTYPE_IP, 68, 67), + (constants.ETHERTYPE_IPV6, 546, 547)): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=80, + reg_port=port.ofport, + in_port=port.ofport, + dl_type=dl_type, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + ) + # Ban dhcp service running on an instance + for dl_type, src_port, dst_port in ( + (constants.ETHERTYPE_IP, 67, 68), + (constants.ETHERTYPE_IPV6, 547, 546)): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=70, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=dl_type, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='drop' + ) + + # Drop Router Advertisements from instances + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=70, + in_port=port.ofport, + reg_port=port.ofport, + dl_type=constants.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=lib_const.ICMPV6_TYPE_RA, + actions='drop' + ) + + # Drop all remaining not tracked egress connections + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE, + priority=10, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + in_port=port.ofport, + reg_port=port.ofport, + actions='drop' + ) + + # Fill in accept_or_ingress table by checking that traffic is ingress + # and if not, accept it + for mac_addr in port.all_allowed_macs: + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=100, + dl_dst=mac_addr, + reg_net=port.vlan_tag, + actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format( + port.ofport, + fwaas_ovs_consts.REG_PORT, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + ) + for ethertype in [constants.ETHERTYPE_IP, constants.ETHERTYPE_IPV6]: + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=90, + dl_type=ethertype, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + actions='ct(commit,zone=NXM_NX_REG{:d}[0..15]),normal'.format( + fwaas_ovs_consts.REG_NET) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + priority=80, + reg_port=port.ofport, + actions='normal' + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_tracked_egress(self, port): + # Drop invalid packets + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=50, + ct_state=fwaas_ovs_consts.OF_STATE_INVALID, + actions='drop' + ) + # Drop traffic for removed fwg rules + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=50, + reg_port=port.ofport, + ct_mark=fwaas_ovs_consts.CT_MARK_INVALID, + actions='drop' + ) + + for state in ( + fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY, + fwaas_ovs_consts.OF_STATE_RELATED, + ): + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=50, + ct_state=state, + ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL, + reg_port=port.ofport, + ct_zone=port.vlan_tag, + actions='normal' + ) + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=40, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED, + actions='drop' + ) + for ethertype in [constants.ETHERTYPE_IP, constants.ETHERTYPE_IPV6]: + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + priority=40, + dl_type=ethertype, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED, + actions="ct(commit,zone=NXM_NX_REG{:d}[0..15]," + "exec(set_field:{:s}->ct_mark))".format( + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.CT_MARK_INVALID) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_ingress_ipv6_icmp(self, port): + for icmp_type in firewall.ICMPV6_ALLOWED_INGRESS_TYPES: + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=100, + reg_port=port.ofport, + dl_dst=port.mac, + dl_type=constants.ETHERTYPE_IPV6, + nw_proto=lib_const.PROTO_NUM_IPV6_ICMP, + icmp_type=icmp_type, + actions='output:{:d}'.format(port.ofport), + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_ingress(self, port): + # Allow incoming ARPs + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=100, + dl_type=constants.ETHERTYPE_ARP, + reg_port=port.ofport, + actions='output:{:d}'.format(port.ofport), + ) + self._initialize_ingress_ipv6_icmp(port) + + # DHCP offers + for dl_type, src_port, dst_port in ( + (constants.ETHERTYPE_IP, 67, 68), + (constants.ETHERTYPE_IPV6, 547, 546)): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=95, + reg_port=port.ofport, + dl_type=dl_type, + nw_proto=lib_const.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='output:{:d}'.format(port.ofport), + ) + + # Track untracked + for dl_type in (constants.ETHERTYPE_IP, constants.ETHERTYPE_IPV6): + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + priority=90, + reg_port=port.ofport, + dl_type=dl_type, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED, + actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format( + fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + fwaas_ovs_consts.REG_NET) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE, + ct_state=fwaas_ovs_consts.OF_STATE_TRACKED, + priority=80, + reg_port=port.ofport, + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_RULES_INGRESS_TABLE) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def _initialize_tracked_ingress(self, port): + # Drop invalid packets + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=50, + ct_state=fwaas_ovs_consts.OF_STATE_INVALID, + actions='drop' + ) + # Drop traffic for removed fwg rules + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=50, + reg_port=port.ofport, + ct_mark=fwaas_ovs_consts.CT_MARK_INVALID, + actions='drop' + ) + + # Allow established and related connections + for state in (fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY, + fwaas_ovs_consts.OF_STATE_RELATED): + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=50, + reg_port=port.ofport, + ct_state=state, + ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL, + ct_zone=port.vlan_tag, + actions='output:{:d}'.format(port.ofport) + ) + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=40, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED, + actions='drop' + ) + for ethertype in [constants.ETHERTYPE_IP, constants.ETHERTYPE_IPV6]: + self._add_flow( + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + priority=40, + dl_type=ethertype, + reg_port=port.ofport, + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED, + actions="ct(commit,zone=NXM_NX_REG{:d}[0..15]," + "exec(set_field:{:s}->ct_mark))".format( + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.CT_MARK_INVALID) + ) + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) and rules_generator method + def add_flows_from_rules(self, port): + self._initialize_tracked_ingress(port) + self._initialize_tracked_egress(port) + LOG.debug('Creating flow rules for port %s that is port %d in OVS', + port.id, port.ofport) + for rule in self.create_rules_generator_for_port(port): + flows = rules.create_flows_from_rule_and_port(rule, port) + LOG.debug("RULGEN: Rules generated for flow %s are %s", + rule, flows) + for flow in flows: + if rule.get('action') == ACTION_ALLOW: + self._accept_flow(**flow) + else: + self._drop_flow(**flow) + + def create_rules_generator_for_port(self, port): + """Returns a generator emitting rules valid for further processing + + Injects necessary fields to feed one-by-one to rules module to + transform into valid openflow rules. + """ + + def inject_fields(rule, direction, offset=0): + """Add fields to rule dict to be able to utilize rules module + + Currently such fields are added: + 'offset', 'direction', 'ethertype', 'source_port_range_min', + 'source_port_range_max', 'port_range_min', 'port_range_max' + """ + # XXX NOTE(ivasilevskaya) maybe there's a clever way to do that + version_ethertype_map = {lib_const.IP_VERSION_4: lib_const.IPv4, + lib_const.IP_VERSION_6: lib_const.IPv6} + + rule['direction'] = direction + rule['ethertype'] = version_ethertype_map[rule['ip_version']] + rule['offset'] = offset + + # transfer destination_port into port_range_min/port_range_max + def add_range(range_key, key_min, key_max): + range_str = rule.get(range_key) + if not range_str: + return + ports = range_str.split(':', 1) + rule[key_min] = int(ports[0]) + rule['port_range_max'] = ( + int(ports[1]) if len(ports) == 2 else int(ports[0])) + + add_range('destination_port', 'port_range_min', 'port_range_max') + add_range('source_port', 'source_port_range_min', + 'source_port_range_max') + + # add direction field + offset = len(port.fw_group.ingress_rules) - 1 + for rule in port.fw_group.ingress_rules: + inject_fields(rule, firewall.INGRESS_DIRECTION, offset) + offset -= 1 + yield rule + + offset = len(port.fw_group.egress_rules) - 1 + for rule in port.fw_group.egress_rules: + inject_fields(rule, firewall.EGRESS_DIRECTION, offset) + offset -= 1 + yield rule + + # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver + # which differs in constants (table numbers) + def delete_all_port_flows(self, port): + """Delete all flows for given port""" + for mac_addr in port.all_allowed_macs: + self._strict_delete_flow(priority=95, + table=ovs_consts.TRANSIENT_TABLE, + dl_dst=mac_addr, + dl_vlan=port.vlan_tag) + self._delete_flows( + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE, + dl_dst=mac_addr, reg_net=port.vlan_tag) + self._strict_delete_flow(priority=105, + table=ovs_consts.TRANSIENT_TABLE, + in_port=port.ofport) + self._delete_flows(reg_port=port.ofport) + + def create_firewall_group(self, ports_for_fwg, firewall_group): + ingress_rules = firewall_group['egress_rule_list'] + egress_rules = firewall_group['ingress_rule_list'] + fwg_id = firewall_group['id'] + + self.update_firewall_group_rules(fwg_id, ingress_rules, egress_rules) + for port in ports_for_fwg: + port['firewall_group'] = fwg_id + self.update_port_filter(port) + + def update_firewall_group(self, ports_for_fwg, firewall_group): + self.create_firewall_group(ports_for_fwg, firewall_group) + + def delete_firewall_group(self, ports_for_fwg, firewall_group): + for port in ports_for_fwg: + port['firewall_group'] = firewall_group['id'] + self.remove_port_filter(port) diff --git a/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/rules.py b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/rules.py new file mode 100644 index 000000000..a614e244f --- /dev/null +++ b/neutron_fwaas/services/firewall/drivers/linux/l2/openvswitch_firewall/rules.py @@ -0,0 +1,186 @@ +# Copyright 2015 Red Hat, Inc. +# 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 netaddr +from neutron_lib import constants as n_consts +from oslo_log import log as logging + +from neutron.agent import firewall +from neutron.common import utils +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import constants as fwaas_ovs_consts + + +LOG = logging.getLogger(__name__) + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver, differs in +# constants +CT_STATES = [ + fwaas_ovs_consts.OF_STATE_ESTABLISHED_NOT_REPLY, + fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED] + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +FLOW_FIELD_FOR_IPVER_AND_DIRECTION = { + (n_consts.IP_VERSION_4, firewall.EGRESS_DIRECTION): 'nw_dst', + (n_consts.IP_VERSION_6, firewall.EGRESS_DIRECTION): 'ipv6_dst', + (n_consts.IP_VERSION_4, firewall.INGRESS_DIRECTION): 'nw_src', + (n_consts.IP_VERSION_6, firewall.INGRESS_DIRECTION): 'ipv6_src', +} + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +FORBIDDEN_PREFIXES = (n_consts.IPv4_ANY, n_consts.IPv6_ANY) + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def is_valid_prefix(ip_prefix): + # IPv6 have multiple ways how to describe ::/0 network, converting to + # IPNetwork and back to string unifies it + return (ip_prefix and + str(netaddr.IPNetwork(ip_prefix)) not in FORBIDDEN_PREFIXES) + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def create_flows_from_rule_and_port(rule, port): + ethertype = rule['ethertype'] + direction = rule['direction'] + dst_ip_prefix = rule.get('dest_ip_prefix') + src_ip_prefix = rule.get('source_ip_prefix') + offset = int(rule.get('offset', 0)) + + flow_template = { + 'priority': 70 + offset, + 'dl_type': fwaas_ovs_consts.ethertype_to_dl_type_map[ethertype], + 'reg_port': port.ofport, + } + + if is_valid_prefix(dst_ip_prefix): + flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[( + utils.get_ip_version(dst_ip_prefix), firewall.EGRESS_DIRECTION)] + ] = dst_ip_prefix + + if is_valid_prefix(src_ip_prefix): + flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[( + utils.get_ip_version(src_ip_prefix), firewall.INGRESS_DIRECTION)] + ] = src_ip_prefix + + flows = create_protocol_flows(direction, flow_template, port, rule) + + return flows + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver, differs in +# constants +def populate_flow_common(direction, flow_template, port): + """Initialize common flow fields.""" + if direction == firewall.INGRESS_DIRECTION: + flow_template['table'] = fwaas_ovs_consts.FW_RULES_INGRESS_TABLE + flow_template['actions'] = "output:{:d}".format(port.ofport) + elif direction == firewall.EGRESS_DIRECTION: + flow_template['table'] = fwaas_ovs_consts.FW_RULES_EGRESS_TABLE + # Traffic can be both ingress and egress, check that no ingress rules + # should be applied + flow_template['actions'] = 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + return flow_template + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def create_protocol_flows(direction, flow_template, port, rule): + flow_template = populate_flow_common(direction, + flow_template.copy(), + port) + protocol = rule.get('protocol') + if protocol is not None: + flow_template['nw_proto'] = protocol + + if protocol in [n_consts.PROTO_NUM_ICMP, n_consts.PROTO_NUM_IPV6_ICMP]: + flows = create_icmp_flows(flow_template, rule) + else: + flows = create_port_range_flows(flow_template, rule) + return flows or [flow_template] + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver, differs only in +# constant +def create_port_range_flows(flow_template, rule): + protocol = fwaas_ovs_consts.REVERSE_IP_PROTOCOL_MAP_WITH_PORTS.get( + rule.get('protocol')) + if protocol is None: + return [] + flows = [] + src_port_match = '{:s}_src'.format(protocol) + src_port_min = rule.get('source_port_range_min') + src_port_max = rule.get('source_port_range_max') + dst_port_match = '{:s}_dst'.format(protocol) + dst_port_min = rule.get('port_range_min') + dst_port_max = rule.get('port_range_max') + + dst_port_range = [] + if dst_port_min and dst_port_max: + dst_port_range = utils.port_rule_masking(dst_port_min, dst_port_max) + + src_port_range = [] + if src_port_min and src_port_max: + src_port_range = utils.port_rule_masking(src_port_min, src_port_max) + for port in src_port_range: + flow = flow_template.copy() + flow[src_port_match] = port + if dst_port_range: + for port in dst_port_range: + dst_flow = flow.copy() + dst_flow[dst_port_match] = port + flows.append(dst_flow) + else: + flows.append(flow) + else: + for port in dst_port_range: + flow = flow_template.copy() + flow[dst_port_match] = port + flows.append(flow) + + return flows + + +# NOTE(ivasilevskaya) copy-paste from neutron ovsfw driver +def create_icmp_flows(flow_template, rule): + icmp_type = rule.get('port_range_min') + if icmp_type is None: + return + flow = flow_template.copy() + flow['icmp_type'] = icmp_type + + icmp_code = rule.get('port_range_max') + if icmp_code is not None: + flow['icmp_code'] = icmp_code + return [flow] + + +def create_accept_flows(flow): + flow['ct_state'] = CT_STATES[0] + result = [flow.copy()] + flow['ct_state'] = CT_STATES[1] + if flow['table'] == fwaas_ovs_consts.FW_RULES_INGRESS_TABLE: + flow['actions'] = ( + 'ct(commit,zone=NXM_NX_REG{:d}[0..15]),{:s}'.format( + fwaas_ovs_consts.REG_NET, flow['actions'])) + result.append(flow) + return result + + +def create_drop_flows(flow): + if flow['table'] in [fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + fwaas_ovs_consts.FW_RULES_EGRESS_TABLE]: + flow['actions'] = 'drop' + return [flow] diff --git a/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/__init__.py b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/test_firewall.py b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/test_firewall.py new file mode 100644 index 000000000..78653452a --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/test_firewall.py @@ -0,0 +1,611 @@ +# Copyright 2017 Mirantis, Inc. +# +# 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 mock +from neutron_lib import constants +import testtools + +from neutron.agent.common import ovs_lib +from neutron.common import constants as n_const +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts +from neutron.tests import base + +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import constants as fwaas_ovs_consts +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import exceptions +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import firewall as ovsfw + +TESTING_VLAN_TAG = 1 + + +def create_ofport(port_dict): + ovs_port = mock.Mock(vif_mac='00:00:00:00:00:00', ofport=1, + port_name="port-name") + return ovsfw.OFPort(port_dict, ovs_port, vlan_tag=TESTING_VLAN_TAG) + + +class TestCreateRegNumbers(base.BaseTestCase): + def test_no_registers_defined(self): + flow = {'foo': 'bar'} + ovsfw.create_reg_numbers(flow) + self.assertEqual({'foo': 'bar'}, flow) + + def test_both_registers_defined(self): + flow = {'foo': 'bar', 'reg_port': 1, 'reg_net': 2} + expected_flow = {'foo': 'bar', + 'reg{:d}'.format(fwaas_ovs_consts.REG_PORT): 1, + 'reg{:d}'.format(fwaas_ovs_consts.REG_NET): 2} + ovsfw.create_reg_numbers(flow) + self.assertEqual(expected_flow, flow) + + +class TestFirewallGroup(base.BaseTestCase): + def setUp(self): + super(TestFirewallGroup, self).setUp() + self.fwg = ovsfw.FirewallGroup('123') + self.fwg.members = {'type': [1, 2, 3, 4]} + + def test_update_rules(self): + ingress_rules = [{'foo-ingress': 'bar', 'rule': 'all'}, + {'bar-ingress': 'foo'}] + egress_rules = [{'foo-egress': '123456'}, {'bar-egress': 'bar'}] + self.fwg.update_rules(ingress_rules, egress_rules) + + self.assertEqual(ingress_rules, self.fwg.ingress_rules) + self.assertEqual(egress_rules, self.fwg.egress_rules) + + def test_update_rules_protocols(self): + # XXX FIXME(ivasilevskaya) figure out what this test does and fix + # appropriately + # leaving failing as it may be important + rules = [ + {'foo': 'bar', 'protocol': constants.PROTO_NAME_ICMP, + 'ethertype': constants.IPv4}, + {'foo': 'bar', 'protocol': constants.PROTO_NAME_ICMP, + 'ethertype': constants.IPv6}, + {'foo': 'bar', 'protocol': constants.PROTO_NAME_IPV6_ICMP_LEGACY, + 'ethertype': constants.IPv6}, + {'foo': 'bar', 'protocol': constants.PROTO_NAME_TCP}, + {'foo': 'bar', 'protocol': '94'}, + {'foo': 'bar', 'protocol': 'baz'}, + {'foo': 'no_proto'}] + self.fwg.update_rules(rules, []) + + self.assertEqual({'foo': 'no_proto'}, self.fwg.ingress_rules.pop()) + protos = [rule['protocol'] for rule in self.fwg.ingress_rules] + self.assertEqual([constants.PROTO_NUM_ICMP, + constants.PROTO_NUM_IPV6_ICMP, + constants.PROTO_NUM_IPV6_ICMP, + constants.PROTO_NUM_TCP, + 94, + 'baz'], protos) + + def test_get_ethertype_filtered_addresses(self): + addresses = self.fwg.get_ethertype_filtered_addresses('type') + expected_addresses = [1, 2, 3, 4] + self.assertEqual(expected_addresses, addresses) + + +class TestOFPort(base.BaseTestCase): + def setUp(self): + super(TestOFPort, self).setUp() + self.ipv4_addresses = ['10.0.0.1', '192.168.0.1'] + self.ipv6_addresses = ['fe80::f816:3eff:fe2e:1'] + port_dict = {'device': 1, + 'fixed_ips': [ + {'subnet_id': 's_%s' % ip, 'ip_address': ip} + for ip in self.ipv4_addresses + self.ipv6_addresses]} + self.port = create_ofport(port_dict) + + def test_ipv4_address(self): + ipv4_addresses = self.port.ipv4_addresses + self.assertEqual(self.ipv4_addresses, ipv4_addresses) + + def test_ipv6_address(self): + ipv6_addresses = self.port.ipv6_addresses + self.assertEqual(self.ipv6_addresses, ipv6_addresses) + + def test__get_allowed_pairs(self): + port = { + 'allowed_address_pairs': [ + {'mac_address': 'foo', 'ip_address': '10.0.0.1'}, + {'mac_address': 'bar', 'ip_address': '192.168.0.1'}, + {'mac_address': 'qux', 'ip_address': '169.254.0.0/16'}, + {'mac_address': 'baz', 'ip_address': '2003::f'}, + ]} + allowed_pairs_v4 = ovsfw.OFPort._get_allowed_pairs(port, version=4) + allowed_pairs_v6 = ovsfw.OFPort._get_allowed_pairs(port, version=6) + expected_aap_v4 = {('foo', '10.0.0.1'), ('bar', '192.168.0.1'), + ('qux', '169.254.0.0/16')} + expected_aap_v6 = {('baz', '2003::f')} + self.assertEqual(expected_aap_v4, allowed_pairs_v4) + self.assertEqual(expected_aap_v6, allowed_pairs_v6) + + def test__get_allowed_pairs_empty(self): + port = {} + allowed_pairs = ovsfw.OFPort._get_allowed_pairs(port, version=4) + self.assertFalse(allowed_pairs) + + def test_update(self): + old_port_dict = self.port.neutron_port_dict + new_port_dict = old_port_dict.copy() + added_ips = [1, 2, 3] + new_port_dict.update({ + 'fixed_ips': added_ips, + 'allowed_address_pairs': [ + {'mac_address': '00:00:00:00:00:01', + 'ip_address': '192.168.0.1'}, + {'mac_address': '00:00:00:00:00:01', + 'ip_address': '2003::f'}], + }) + self.port.update(new_port_dict) + self.assertEqual(new_port_dict, self.port.neutron_port_dict) + self.assertIsNot(new_port_dict, self.port.neutron_port_dict) + self.assertEqual(added_ips, self.port.fixed_ips) + self.assertEqual({('00:00:00:00:00:01', '192.168.0.1')}, + self.port.allowed_pairs_v4) + self.assertIn(('00:00:00:00:00:01', '2003::f'), + self.port.allowed_pairs_v6) + + +class TestFWGPortMap(base.BaseTestCase): + def setUp(self): + super(TestFWGPortMap, self).setUp() + self.map = ovsfw.FWGPortMap() + + def test_get_or_create_fwg_existing_fwg(self): + self.map.fw_groups['id'] = mock.sentinel + fwg = self.map.get_or_create_fwg('id') + self.assertIs(mock.sentinel, fwg) + + def test_get_or_create_fwg_nonexisting_fwg(self): + with mock.patch.object(ovsfw, 'FirewallGroup') as fwg_mock: + fwg = self.map.get_or_create_fwg('id') + self.assertEqual(fwg_mock.return_value, fwg) + + def _check_port(self, port_id, expected_id): + port = self.map.ports[port_id] + expected_fwg = self.map.fw_groups[expected_id] + self.assertEqual(expected_fwg, port.fw_group) + + def _check_fwg(self, fwg_id, expected_port_ids): + fwg = self.map.fw_groups[fwg_id] + expected_ports = {self.map.ports[port_id] + for port_id in expected_port_ids} + self.assertEqual(expected_ports, fwg.ports) + + def _create_ports_and_fwgs(self): + fwg_1 = ovsfw.FirewallGroup(1) + fwg_2 = ovsfw.FirewallGroup(2) + fwg_3 = ovsfw.FirewallGroup(3) + port_a = create_ofport({'device': 'a'}) + port_b = create_ofport({'device': 'b'}) + port_c = create_ofport({'device': 'c'}) + self.map.ports = {'a': port_a, 'b': port_b, 'c': port_c} + self.map.fw_groups = {1: fwg_1, 2: fwg_2, 3: fwg_3} + # XXX FIXME(ivasilevskaya) see note for OFPORT + port_a.fw_group = fwg_1 + port_b.fw_group = fwg_2 + port_c.fw_group = fwg_2 + fwg_1.ports = {port_a} + fwg_2.ports = {port_b, port_c} + + def test_create_port(self): + """Create a port and assign it to firewall group + + It is implied that 1 port can be assigned to one firewall group only + """ + port = create_ofport({'device': 'a'}) + port_dict = {'some-port-attributes-go-here': 42, + 'firewall_group': 1} + self.map.create_port(port, port_dict) + self._check_port('a', 1) + self._check_fwg(1, ['a']) + + def test_update_port_another_fwg_added(self): + """Update a port with new firewall group id + + It is implied that 1 port can be assigned to one firewall group only + """ + self._create_ports_and_fwgs() + self._check_port('b', 2) + port_dict = {'firewall_group': 3} + self.map.update_port(self.map.ports['b'], port_dict) + self._check_port('a', 1) + self._check_port('b', 3) + self._check_port('c', 2) + self._check_fwg(1, ['a']) + self._check_fwg(2, ['c']) + self._check_fwg(3, ['b']) + + def test_remove_port(self): + self._create_ports_and_fwgs() + self.map.remove_port(self.map.ports['c']) + self._check_port('b', 2) + self._check_fwg(1, ['a']) + self._check_fwg(2, ['b']) + self.assertNotIn('c', self.map.ports) + + def test_update_rules(self): + """Just make sure it doesn't crash""" + self.map.update_rules(42, [], []) + + def test_update_members(self): + """Just make sure it doesn't crash""" + self.map.update_members(42, []) + + +class FakeOVSPort(object): + def __init__(self, name, port, mac): + self.port_name = name + self.ofport = port + self.vif_mac = mac + + +class TestOVSFirewallDriver(base.BaseTestCase): + def setUp(self): + super(TestOVSFirewallDriver, self).setUp() + mock_bridge = mock.patch.object( + ovs_lib, 'OVSBridge', autospec=True).start() + self.firewall = ovsfw.OVSFirewallDriver(mock_bridge) + self.mock_bridge = self.firewall.int_br + self.mock_bridge.reset_mock() + self.fake_ovs_port = FakeOVSPort('port', 1, '00:00:00:00:00:00') + self.mock_bridge.br.get_vif_port_by_id.return_value = \ + self.fake_ovs_port + + def _prepare_firewall_group(self): + ingress_rules = [ + {'position': '1', + 'protocol': 'tcp', + 'ip_version': 4, + 'destination_port': '123', + 'enabled': True, + 'action': 'allow', + 'id': 'fake-fw-rule1'} + ] + egress_rules = [ + {'position': '2', + 'protocol': 'udp', + 'ip_version': 4, + 'enabled': True, + 'action': 'allow', + 'id': 'fake-fw-rule2'}, + {'position': '3', + 'protocol': 'tcp', + 'ip_version': 6, + 'enabled': True, + 'action': 'allow', + 'id': 'fake-fw-rule3'}] + self.firewall.update_firewall_group_rules(1, ingress_rules, []) + self.firewall.update_firewall_group_rules(2, [], egress_rules) + + @property + def port_ofport(self): + return self.mock_bridge.br.get_vif_port_by_id.return_value.ofport + + @property + def port_mac(self): + return self.mock_bridge.br.get_vif_port_by_id.return_value.vif_mac + + def test_initialize_bridge(self): + br = self.firewall.initialize_bridge(self.mock_bridge) + self.assertEqual(br, self.mock_bridge.deferred.return_value) + + def test__add_flow_dl_type_formatted_to_string(self): + dl_type = 0x0800 + self.firewall._add_flow(dl_type=dl_type) + + def test__add_flow_registers_are_replaced(self): + self.firewall._add_flow(in_port=1, reg_port=1, reg_net=2) + expected_calls = {'in_port': 1, + 'reg{:d}'.format(fwaas_ovs_consts.REG_PORT): 1, + 'reg{:d}'.format(fwaas_ovs_consts.REG_NET): 2} + self.mock_bridge.br.add_flow.assert_called_once_with( + **expected_calls) + + def test__drop_all_unmatched_flows(self): + self.firewall._drop_all_unmatched_flows() + expected_calls = [ + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE)] + actual_calls = self.firewall.int_br.br.add_flow.call_args_list + self.assertEqual(expected_calls, actual_calls) + + def test_get_or_create_ofport_non_existing(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123, + 'lvlan': TESTING_VLAN_TAG, + } + port = self.firewall.get_or_create_ofport(port_dict) + port_dict = { + 'device': 'port-id', + 'firewall_group': 456, + 'lvlan': TESTING_VLAN_TAG, + } + port = self.firewall.get_or_create_ofport(port_dict) + sg1, sg2 = sorted( + self.firewall.fwg_port_map.fw_groups.values(), + key=lambda x: x.id) + self.assertIn(port, self.firewall.fwg_port_map.ports.values()) + self.assertEqual(port.fw_group, sg2) + self.assertEqual(set(), sg1.ports) + self.assertIn(port, sg2.ports) + + def test_get_or_create_ofport_existing(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + of_port = create_ofport(port_dict) + self.firewall.fwg_port_map.ports[of_port.id] = of_port + port = self.firewall.get_or_create_ofport(port_dict) + [sg1] = sorted(self.firewall.fwg_port_map.fw_groups.values(), + key=lambda x: x.id) + self.assertIs(of_port, port) + self.assertIn(port, self.firewall.fwg_port_map.ports.values()) + self.assertEqual(port.fw_group, sg1) + self.assertIn(port, sg1.ports) + + def test_get_or_create_ofport_changed(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + of_port = create_ofport(port_dict) + self.firewall.fwg_port_map.ports[of_port.id] = of_port + fake_ovs_port = FakeOVSPort('port', 2, '00:00:00:00:00:00') + self.mock_bridge.br.get_vif_port_by_id.return_value = \ + fake_ovs_port + port = self.firewall.get_or_create_ofport(port_dict) + self.assertEqual(port.ofport, 2) + + def test_get_or_create_ofport_missing(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + self.mock_bridge.br.get_vif_port_by_id.return_value = None + with testtools.ExpectedException(exceptions.OVSFWaaSPortNotFound): + self.firewall.get_or_create_ofport(port_dict) + + def test_get_or_create_ofport_missing_nocreate(self): + port_dict = { + 'device': 'port-id', + 'firewall_group': 123} + self.mock_bridge.br.get_vif_port_by_id.return_value = None + self.assertIsNone(self.firewall.get_ofport(port_dict)) + self.assertFalse(self.mock_bridge.br.get_vif_port_by_id.called) + + def test_is_port_managed_managed_port(self): + port_dict = {'device': 'port-id'} + self.firewall.fwg_port_map.ports[port_dict['device']] = object() + is_managed = self.firewall.is_port_managed(port_dict) + self.assertTrue(is_managed) + + def test_is_port_managed_not_managed_port(self): + port_dict = {'device': 'port-id'} + is_managed = self.firewall.is_port_managed(port_dict) + self.assertFalse(is_managed) + + def test_prepare_port_filter(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'fixed_ips': [{'subnet_id': "some_subnet_id_here", + 'ip_address': "10.0.0.1"}], + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + exp_egress_classifier = mock.call( + actions='set_field:{:d}->reg5,set_field:{:d}->reg6,' + 'resubmit(,{:d})'.format( + self.port_ofport, TESTING_VLAN_TAG, + fwaas_ovs_consts.FW_BASE_EGRESS_TABLE), + in_port=self.port_ofport, + priority=105, + table=ovs_consts.TRANSIENT_TABLE) + exp_ingress_classifier = mock.call( + actions='set_field:{:d}->reg5,set_field:{:d}->reg6,' + 'strip_vlan,resubmit(,{:d})'.format( + self.port_ofport, TESTING_VLAN_TAG, + fwaas_ovs_consts.FW_BASE_INGRESS_TABLE), + dl_dst=self.port_mac, + dl_vlan='0x%x' % TESTING_VLAN_TAG, + priority=95, + table=ovs_consts.TRANSIENT_TABLE) + filter_rule = mock.call( + actions='ct(commit,zone=NXM_NX_REG6[0..15]),' + 'output:{:d}'.format(self.port_ofport), + dl_type="0x{:04x}".format(n_const.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_TCP, + priority=70, + reg5=self.port_ofport, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + tcp_dst='0x007b') + calls = self.mock_bridge.br.add_flow.call_args_list + for call in exp_ingress_classifier, exp_egress_classifier, filter_rule: + self.assertIn(call, calls) + + def test_prepare_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'port_security_enabled': False} + self._prepare_firewall_group() + with mock.patch.object( + self.firewall, 'initialize_port_flows') as m_init_flows: + self.firewall.prepare_port_filter(port_dict) + self.assertFalse(m_init_flows.called) + + def test_prepare_port_filter_initialized_port(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + self.assertFalse(self.mock_bridge.br.delete_flows.called) + self.firewall.prepare_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + + def test_update_port_filter(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + port_dict['firewall_group'] = 2 + self.mock_bridge.reset_mock() + + self.firewall.update_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + filter_rules = [ + mock.call( + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + dl_type="0x{:04x}".format(n_const.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_UDP, + priority=71, + ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED, + reg5=self.port_ofport, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE), + # XXX FIXME NOTE(ivasilevskaya) this test originally tested that + # flows for SG with remote_group=this group were generated with + # proper conjunction action. If the original idea that conj_manager + # isn't needed for firewall groups proves to be wrong this needs to + # be revizited and properly fixed/covered with tests + mock.call( + actions='resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED_NOT_REPLY, + dl_type=mock.ANY, + nw_proto=6, + priority=70, reg5=self.port_ofport, + table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE)] + self.mock_bridge.br.add_flow.assert_has_calls(filter_rules, + any_order=True) + + def test_update_port_filter_create_new_port_if_not_present(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1} + self._prepare_firewall_group() + with mock.patch.object( + self.firewall, 'prepare_port_filter') as prepare_mock: + self.firewall.update_port_filter(port_dict) + self.assertTrue(prepare_mock.called) + + def test_update_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + port_dict['port_security_enabled'] = False + self.firewall.update_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + + def test_remove_port_filter(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self._prepare_firewall_group() + self.firewall.prepare_port_filter(port_dict) + self.firewall.remove_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + self.assertIn(1, self.firewall.fwg_to_delete) + + def test_remove_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1} + self.firewall.remove_port_filter(port_dict) + self.assertFalse(self.mock_bridge.br.delete_flows.called) + + def test_update_firewall_group_rules(self): + """Just make sure it doesn't crash""" + new_rules_ingress = [ + {'ip_version': 4, + 'action': 'allow', + 'protocol': constants.PROTO_NAME_ICMP}, + {'ip_version': 4, + 'direction': 'deny'}] + self.firewall.update_firewall_group_rules(1, new_rules_ingress, []) + + def test__cleanup_stale_sg(self): + self._prepare_firewall_group() + self.firewall.fwg_to_delete = {1} + with mock.patch.object(self.firewall.fwg_port_map, + 'delete_fwg') as delete_fwg_mock: + self.firewall._cleanup_stale_fwg() + delete_fwg_mock.assert_called_once_with(1) + + def test_get_ovs_port(self): + ovs_port = self.firewall.get_ovs_port('port_id') + self.assertEqual(self.fake_ovs_port, ovs_port) + + def test_get_ovs_port_non_existent(self): + self.mock_bridge.br.get_vif_port_by_id.return_value = None + with testtools.ExpectedException(exceptions.OVSFWaaSPortNotFound): + self.firewall.get_ovs_port('port_id') + + def test__initialize_egress_no_port_security_sends_to_egress(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': TESTING_VLAN_TAG} + self.firewall._initialize_egress_no_port_security(port_dict) + expected_call = mock.call( + table=ovs_consts.TRANSIENT_TABLE, + priority=100, + in_port=self.fake_ovs_port.ofport, + actions='set_field:%d->reg%d,' + 'set_field:%d->reg%d,' + 'resubmit(,%d)' % ( + self.fake_ovs_port.ofport, + fwaas_ovs_consts.REG_PORT, + TESTING_VLAN_TAG, + fwaas_ovs_consts.REG_NET, + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE) + ) + calls = self.mock_bridge.br.add_flow.call_args_list + self.assertIn(expected_call, calls) + + def test__initialize_egress_no_port_security_no_tag(self): + port_dict = {'device': 'port-id', + 'firewall_group': 1, + 'lvlan': None} + self.firewall._initialize_egress_no_port_security(port_dict) + self.assertFalse(self.mock_bridge.br.add_flow.called) + + def test__remove_egress_no_port_security_deletes_flow(self): + self.mock_bridge.br.db_get_val.return_value = {'tag': TESTING_VLAN_TAG} + self.firewall.fwg_port_map.unfiltered['port_id'] = 1 + self.firewall._remove_egress_no_port_security('port_id') + expected_call = mock.call( + table=ovs_consts.TRANSIENT_TABLE, + in_port=self.fake_ovs_port.ofport, + ) + calls = self.mock_bridge.br.delete_flows.call_args_list + self.assertIn(expected_call, calls) + + def test__remove_egress_no_port_security_no_tag(self): + self.mock_bridge.br.db_get_val.return_value = {} + self.firewall._remove_egress_no_port_security('port_id') + self.assertFalse(self.mock_bridge.br.delete_flows.called) diff --git a/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/test_rules.py b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/test_rules.py new file mode 100644 index 000000000..efae394ab --- /dev/null +++ b/neutron_fwaas/tests/unit/services/firewall/drivers/linux/l2/openvswitch_firewall/test_rules.py @@ -0,0 +1,336 @@ +# Copyright 2015 Red Hat, Inc. +# +# 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 mock +from neutron_lib import constants + +from neutron.agent import firewall +from neutron.common import constants as n_const +from neutron.tests import base + +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import constants as fwaas_ovs_consts +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import firewall as ovsfw +from neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall \ + import rules + +TESTING_VLAN_TAG = 1 + + +class TestIsValidPrefix(base.BaseTestCase): + def test_valid_prefix_ipv4(self): + is_valid = rules.is_valid_prefix('10.0.0.0/0') + self.assertTrue(is_valid) + + def test_invalid_prefix_ipv4(self): + is_valid = rules.is_valid_prefix('0.0.0.0/0') + self.assertFalse(is_valid) + + def test_valid_prefix_ipv6(self): + is_valid = rules.is_valid_prefix('ffff::0/0') + self.assertTrue(is_valid) + + def test_invalid_prefix_ipv6(self): + is_valid = rules.is_valid_prefix('0000:0::0/0') + self.assertFalse(is_valid) + is_valid = rules.is_valid_prefix('::0/0') + self.assertFalse(is_valid) + is_valid = rules.is_valid_prefix('::/0') + self.assertFalse(is_valid) + + +class TestCreateFlowsFromRuleAndPort(base.BaseTestCase): + def setUp(self): + super(TestCreateFlowsFromRuleAndPort, self).setUp() + ovs_port = mock.Mock(vif_mac='00:00:00:00:00:00') + ovs_port.ofport = 1 + port_dict = {'device': 'port_id'} + self.port = ovsfw.OFPort( + port_dict, ovs_port, vlan_tag=TESTING_VLAN_TAG) + + self.create_flows_mock = mock.patch.object( + rules, 'create_protocol_flows').start() + + @property + def passed_flow_template(self): + return self.create_flows_mock.call_args[0][1] + + def _test_create_flows_from_rule_and_port_helper( + self, rule, expected_template): + rules.create_flows_from_rule_and_port(rule, self.port) + + self.assertEqual(expected_template, self.passed_flow_template) + + def test_create_flows_from_rule_and_port_no_ip_ipv4(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': firewall.INGRESS_DIRECTION, + } + expected_template = { + 'priority': 70, + 'dl_type': n_const.ETHERTYPE_IP, + 'reg_port': self.port.ofport, + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_ipv4(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': firewall.INGRESS_DIRECTION, + 'source_ip_prefix': '192.168.0.0/24', + 'dest_ip_prefix': '10.0.0.1/32', + } + expected_template = { + 'priority': 70, + 'dl_type': n_const.ETHERTYPE_IP, + 'reg_port': self.port.ofport, + 'nw_src': '192.168.0.0/24', + 'nw_dst': '10.0.0.1/32', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_with_zero_ipv4(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': firewall.INGRESS_DIRECTION, + 'source_ip_prefix': '192.168.0.0/24', + 'dest_ip_prefix': '0.0.0.0/0', + } + expected_template = { + 'priority': 70, + 'dl_type': n_const.ETHERTYPE_IP, + 'reg_port': self.port.ofport, + 'nw_src': '192.168.0.0/24', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_no_ip_ipv6(self): + rule = { + 'ethertype': constants.IPv6, + 'direction': firewall.INGRESS_DIRECTION, + } + expected_template = { + 'priority': 70, + 'dl_type': n_const.ETHERTYPE_IPV6, + 'reg_port': self.port.ofport, + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_ipv6(self): + rule = { + 'ethertype': constants.IPv6, + 'direction': firewall.INGRESS_DIRECTION, + 'source_ip_prefix': '2001:db8:bbbb::1/64', + 'dest_ip_prefix': '2001:db8:aaaa::1/64', + } + expected_template = { + 'priority': 70, + 'dl_type': n_const.ETHERTYPE_IPV6, + 'reg_port': self.port.ofport, + 'ipv6_src': '2001:db8:bbbb::1/64', + 'ipv6_dst': '2001:db8:aaaa::1/64', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + def test_create_flows_from_rule_and_port_src_and_dst_with_zero_ipv6(self): + rule = { + 'ethertype': constants.IPv6, + 'direction': firewall.INGRESS_DIRECTION, + 'source_ip_prefix': '2001:db8:bbbb::1/64', + 'dest_ip_prefix': '::/0', + } + expected_template = { + 'priority': 70, + 'dl_type': n_const.ETHERTYPE_IPV6, + 'reg_port': self.port.ofport, + 'ipv6_src': '2001:db8:bbbb::1/64', + } + self._test_create_flows_from_rule_and_port_helper(rule, + expected_template) + + +class TestCreateProtocolFlows(base.BaseTestCase): + def setUp(self): + super(TestCreateProtocolFlows, self).setUp() + ovs_port = mock.Mock(vif_mac='00:00:00:00:00:00') + ovs_port.ofport = 1 + port_dict = {'device': 'port_id'} + self.port = ovsfw.OFPort( + port_dict, ovs_port, vlan_tag=TESTING_VLAN_TAG) + + def _test_create_protocol_flows_helper(self, direction, rule, + expected_flows): + flow_template = {'some_settings': 'foo'} + for flow in expected_flows: + flow.update(flow_template) + flows = rules.create_protocol_flows( + direction, flow_template, self.port, rule) + self.assertEqual(expected_flows, flows) + + def test_create_protocol_flows_ingress(self): + rule = {'protocol': constants.PROTO_NUM_TCP} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_INGRESS_TABLE, + 'actions': 'output:1', + 'nw_proto': constants.PROTO_NUM_TCP, + }] + self._test_create_protocol_flows_helper( + firewall.INGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_egress(self): + rule = {'protocol': constants.PROTO_NUM_TCP} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_TCP, + }] + self._test_create_protocol_flows_helper( + firewall.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_no_protocol(self): + rule = {} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + }] + self._test_create_protocol_flows_helper( + firewall.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_icmp6(self): + rule = {'ethertype': constants.IPv6, + 'protocol': constants.PROTO_NUM_IPV6_ICMP} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_IPV6_ICMP, + }] + self._test_create_protocol_flows_helper( + firewall.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_port_range(self): + rule = {'ethertype': constants.IPv4, + 'protocol': constants.PROTO_NUM_TCP, + 'port_range_min': 22, + 'port_range_max': 23} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_TCP, + 'tcp_dst': '0x0016/0xfffe' + }] + self._test_create_protocol_flows_helper( + firewall.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_icmp(self): + rule = {'ethertype': constants.IPv4, + 'protocol': constants.PROTO_NUM_ICMP, + 'port_range_min': 0} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_ICMP, + 'icmp_type': 0 + }] + self._test_create_protocol_flows_helper( + firewall.EGRESS_DIRECTION, rule, expected_flows) + + def test_create_protocol_flows_ipv6_icmp(self): + rule = {'ethertype': constants.IPv6, + 'protocol': constants.PROTO_NUM_IPV6_ICMP, + 'port_range_min': 5, + 'port_range_max': 0} + expected_flows = [{ + 'table': fwaas_ovs_consts.FW_RULES_EGRESS_TABLE, + 'actions': 'resubmit(,{:d})'.format( + fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_IPV6_ICMP, + 'icmp_type': 5, + 'icmp_code': 0, + }] + self._test_create_protocol_flows_helper( + firewall.EGRESS_DIRECTION, rule, expected_flows) + + +class TestCreatePortRangeFlows(base.BaseTestCase): + def _test_create_port_range_flows_helper(self, expected_flows, rule): + flow_template = {'some_settings': 'foo'} + for flow in expected_flows: + flow.update(flow_template) + port_range_flows = rules.create_port_range_flows(flow_template, rule) + self.assertEqual(expected_flows, port_range_flows) + + def test_create_port_range_flows_with_source_and_destination(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + 'source_port_range_min': 123, + 'source_port_range_max': 124, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [ + {'tcp_src': '0x007b', 'tcp_dst': '0x000a/0xfffe'}, + {'tcp_src': '0x007c', 'tcp_dst': '0x000a/0xfffe'}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_with_source(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + 'source_port_range_min': 123, + 'source_port_range_max': 124, + } + expected_flows = [ + {'tcp_src': '0x007b'}, + {'tcp_src': '0x007c'}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_with_destination(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [ + {'tcp_dst': '0x000a/0xfffe'}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_without_port_range(self): + rule = { + 'protocol': constants.PROTO_NUM_TCP, + } + expected_flows = [] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_with_icmp_protocol(self): + # NOTE: such call is prevented by create_protocols_flows + rule = { + 'protocol': constants.PROTO_NUM_ICMP, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [] + self._test_create_port_range_flows_helper(expected_flows, rule) diff --git a/setup.cfg b/setup.cfg index 6841d588d..2ef25c230 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ neutron.agent.l2.extensions = fwaas_v2 = neutron_fwaas.services.firewall.agents.l2.fwaas_v2:FWaaSV2AgentExtension neutron.agent.l2.firewall_drivers = noop = neutron_fwaas.services.firewall.drivers.linux.l2.noop.noop_driver:NoopFirewallL2Driver + ovs = neutron_fwaas.services.firewall.drivers.linux.l2.openvswitch_firewall.firewall:OVSFirewallDriver neutron.agent.l3.extensions = fwaas = neutron_fwaas.services.firewall.agents.l3reference.firewall_l3_agent:L3WithFWaaS fwaas_v2 = neutron_fwaas.services.firewall.agents.l3reference.firewall_l3_agent_v2:L3WithFWaaS