From ef29f7eb9a2a37133eacdb7f019b48ec3f9a42c3 Mon Sep 17 00:00:00 2001 From: Jakub Libosvar Date: Tue, 1 Sep 2015 15:50:48 +0000 Subject: [PATCH] Open vSwitch conntrack based firewall driver This firewall requires OVS 2.5+ version supporting conntrack and kernel conntrack datapath support (kernel>=4.3). For more information, see https://github.com/openvswitch/ovs/blob/master/FAQ.md As part of this new entry points for current reference firewalls were added. Configuration: in openvswitch_agent.ini: - in securitygroup section set firewall_driver to openvswitch DocImpact Closes-bug: #1461000 Co-Authored-By: Miguel Angel Ajo Pelayo Co-Authored-By: Amir Sadoughi Change-Id: I13e5cda8b5f3a13a60b14d80e54f198f32d7a529 --- doc/source/devref/index.rst | 1 + doc/source/devref/openvswitch_firewall.rst | 174 ++++++ neutron/agent/firewall.py | 18 + neutron/agent/linux/iptables_firewall.py | 11 +- .../linux/openvswitch_firewall/__init__.py | 18 + .../linux/openvswitch_firewall/constants.py | 34 ++ .../linux/openvswitch_firewall/firewall.py | 546 ++++++++++++++++++ .../agent/linux/openvswitch_firewall/rules.py | 122 ++++ neutron/agent/securitygroups_rpc.py | 24 +- neutron/cmd/sanity/checks.py | 14 + neutron/cmd/sanity_check.py | 14 + neutron/common/constants.py | 5 +- .../openvswitch/agent/common/constants.py | 22 + .../agent/openflow/native/ovs_bridge.py | 4 +- .../agent/openflow/ovs_ofctl/ovs_bridge.py | 4 +- .../openvswitch/agent/ovs_neutron_agent.py | 10 +- .../mech_driver/mech_openvswitch.py | 9 +- neutron/tests/common/conn_testers.py | 60 +- .../tests/functional/agent/test_firewall.py | 77 ++- .../linux/openvswitch_firewall/__init__.py | 0 .../openvswitch_firewall/test_firewall.py | 429 ++++++++++++++ .../linux/openvswitch_firewall/test_rules.py | 254 ++++++++ .../mech_driver/test_mech_openvswitch.py | 2 + ...s-ct-firewall-driver-52a70a6a16d06f59.yaml | 11 + setup.cfg | 5 + 25 files changed, 1820 insertions(+), 48 deletions(-) create mode 100644 doc/source/devref/openvswitch_firewall.rst create mode 100644 neutron/agent/linux/openvswitch_firewall/__init__.py create mode 100644 neutron/agent/linux/openvswitch_firewall/constants.py create mode 100644 neutron/agent/linux/openvswitch_firewall/firewall.py create mode 100644 neutron/agent/linux/openvswitch_firewall/rules.py create mode 100644 neutron/tests/unit/agent/linux/openvswitch_firewall/__init__.py create mode 100644 neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py create mode 100644 neutron/tests/unit/agent/linux/openvswitch_firewall/test_rules.py create mode 100644 releasenotes/notes/ovs-ct-firewall-driver-52a70a6a16d06f59.yaml diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index bb5a79257f0..3a00d87b3b1 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -75,6 +75,7 @@ Neutron Internals i18n instrumentation address_scopes + openvswitch_firewall Testing ------- diff --git a/doc/source/devref/openvswitch_firewall.rst b/doc/source/devref/openvswitch_firewall.rst new file mode 100644 index 00000000000..6e25a1ee5e4 --- /dev/null +++ b/doc/source/devref/openvswitch_firewall.rst @@ -0,0 +1,174 @@ +.. + 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. + + + Convention for heading levels in Neutron devref: + ======= Heading 0 (reserved for the title in a document) + ------- Heading 1 + ~~~~~~~ Heading 2 + +++++++ Heading 3 + ''''''' Heading 4 + (Avoid deeper levels because they do not render well.) + + +Open vSwitch Firewall Driver +=========================== + +The OVS driver has the same API as the current iptables firewall driver, +keeping the state of security groups and ports inside of the firewall. +Class ``SGPortMap`` was created to keep state consistent, and maps from ports +to security groups and vice-versa. Every port and security group is represented +by its own object encapsulating the necessary information. + + +Firewall API calls +------------------ + +There are two main calls performed by the firewall driver in order to either +create or update a port with security groups - ``prepare_port_filter`` and +``update_port_filter``. Both methods rely on the security group objects that +are already defined in the driver and work similarly to their iptables +counterparts. The definition of the objects will be described later in this +document. ``prepare_port_filter`` must be called only once during port +creation, and it defines the initial rules for the port. When the port is +updated, all filtering rules are removed, and new rules are generated based on +the available information about security groups in the driver. + +Security group rules can be defined in the firewall driver by calling +``update_security_group_rules``, which rewrites all the rules for a given +security group. If a remote security group is changed, then +``update_security_group_members`` is called to determine the set of IP +addresses that should be allowed for this remote security group. Calling this +method will not have any effect on existing instance ports. In other words, if +the port is using security groups and its rules are changed by calling one of +the above methods, then no new rules are generated for this port. +``update_port_filter`` must be called for the changes to take effect. + +All the machinery above is controlled by security group RPC methods, which mean +the firewall driver doesn't have any logic of which port should be updated +based on the provided changes, it only accomplishes actions when called from +the controller. + + +OpenFlow rules +-------------- + +At first, every connection is split into ingress and egress processes based on +the input or output port respectively. Each port contains the initial +hardcoded flows for ARP, DHCP and established connections, which are accepted +by default. To detect established connections, a flow must by marked by +conntrack first with an ``action=ct()`` rule. An accepted flow means that +ingress packets for the connection are directly sent to the port, and egress +packets are left to be normally switched by the integration bridge. + +Connections that are not matched by the above rules are sent to either the +ingress or egress filtering table, depending on its direction. The reason the +rules are based on security group rules in separate tables is to make it easy +to detect these rules during removal. + +The firewall driver method ``create_rules_generator_for_port`` creates a +generator that builds a single security group rule either from rules belonging +to a given group, or rules allowing connections to remote groups. Every rule is +then expanded into several OpenFlow rules by the method +``create_flows_from_rule_and_port``. + + +Rules example with explanation: +------------------------------- + +TODO: Rules below will be awesomly explained + +:: + + table=0, priority=100,in_port=2 actions=load:0x2->NXM_NX_REG5[],resubmit(,71) + table=0, priority=100,in_port=1 actions=load:0x1->NXM_NX_REG5[],resubmit(,71) + table=0, priority=90,dl_dst=fa:16:3e:9b:67:b2 actions=load:0x2->NXM_NX_REG5[],resubmit(,81) + table=0, priority=90,dl_dst=fa:16:3e:44:de:7a actions=load:0x1->NXM_NX_REG5[],resubmit(,81) + table=0, priority=0 actions=NORMAL + table=0, priority=1 actions=NORMAL + table=71, priority=95,arp,in_port=2,dl_src=fa:16:3e:9b:67:b2,arp_spa=192.168.0.2 actions=NORMAL + table=71, priority=95,arp,in_port=1,dl_src=fa:16:3e:44:de:7a,arp_spa=192.168.0.1 actions=NORMAL + table=71, priority=90,ct_state=-trk,in_port=2,dl_src=fa:16:3e:9b:67:b2 actions=ct(table=72,zone=NXM_NX_REG5[0..15]) + table=71, priority=90,ct_state=-trk,in_port=1,dl_src=fa:16:3e:44:de:7a actions=ct(table=72,zone=NXM_NX_REG5[0..15]) + table=71, priority=70,udp,in_port=2,tp_src=68,tp_dst=67 actions=NORMAL + table=71, priority=70,udp6,in_port=2,tp_src=546,tp_dst=547 actions=NORMAL + table=71, priority=60,udp,in_port=2,tp_src=67,tp_dst=68 actions=drop + table=71, priority=60,udp6,in_port=2,tp_src=547,tp_dst=546 actions=drop + table=71, priority=70,udp,in_port=1,tp_src=68,tp_dst=67 actions=NORMAL + table=71, priority=70,udp6,in_port=1,tp_src=546,tp_dst=547 actions=NORMAL + table=71, priority=60,udp,in_port=1,tp_src=67,tp_dst=68 actions=drop + table=71, priority=60,udp6,in_port=1,tp_src=547,tp_dst=546 actions=drop + table=71, priority=10,ct_state=-trk,in_port=2 actions=drop + table=71, priority=10,ct_state=-trk,in_port=1 actions=drop + table=71, priority=0 actions=drop + table=72, priority=90,ct_state=+inv+trk actions=drop + table=72, priority=80,ct_state=+est-rel-inv+trk actions=NORMAL + table=72, priority=80,ct_state=-est+rel-inv+trk actions=NORMAL + table=72, priority=70,icmp,dl_src=fa:16:3e:44:de:7a,nw_src=192.168.0.1 actions=resubmit(,73) + table=72, priority=0 actions=drop + table=73, priority=100,dl_dst=fa:16:3e:9b:67:b2 actions=resubmit(,81) + table=73, priority=100,dl_dst=fa:16:3e:44:de:7a actions=resubmit(,81) + table=73, priority=90,in_port=2 actions=ct(commit,zone=NXM_NX_REG5[0..15]) + table=73, priority=90,in_port=1 actions=ct(commit,zone=NXM_NX_REG5[0..15]) + table=81, priority=100,arp,dl_dst=fa:16:3e:9b:67:b2 actions=output:2 + table=81, priority=100,arp,dl_dst=fa:16:3e:44:de:7a actions=output:1 + table=81, priority=95,ct_state=-trk,ip actions=ct(table=82,zone=NXM_NX_REG5[0..15]) + table=81, priority=95,ct_state=-trk,ipv6 actions=ct(table=82,zone=NXM_NX_REG5[0..15]) + table=81, priority=80,dl_dst=fa:16:3e:9b:67:b2 actions=resubmit(,82) + table=81, priority=80,dl_dst=fa:16:3e:44:de:7a actions=resubmit(,82) + table=81, priority=0 actions=drop + table=82, priority=100,ct_state=+inv+trk actions=drop + table=82, priority=80,ct_state=+est-rel-inv+trk,dl_dst=fa:16:3e:44:de:7a actions=output:1 + table=82, priority=80,ct_state=-est+rel-inv+trk,dl_dst=fa:16:3e:44:de:7a actions=output:1 + table=82, priority=80,ct_state=+est-rel-inv+trk,dl_dst=fa:16:3e:9b:67:b2 actions=output:2 + table=82, priority=80,ct_state=-est+rel-inv+trk,dl_dst=fa:16:3e:9b:67:b2 actions=output:2 + table=82, priority=70,icmp,dl_dst=fa:16:3e:9b:67:b2,nw_src=192.168.0.1,nw_dst=192.168.0.2 actions=ct(commit,zone=NXM_NX_REG5[0..15]),output:2 + table=82, priority=0 actions=drop + + +Future work +----------- + + - Conjunctions in Openflow rules can be created to decrease the number of + rules needed for remote security groups + - Masking the port range can be used to avoid generating a single rule per + port number being filtered. For example, if the port range is 1 to 5, one + rule can be generated instead of 5. + e.g. tcp,tcp_src=0x03e8/0xfff8 + - During the update of firewall rules, we can use bundles to make the changes + atomic + +Upgrade path from iptables hybrid driver +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +During an upgrade, the agent will need to re-plug each instance's tap device +into the integration bridge while trying to not break existing connections. One +of the following approaches can be taken: + +1) Pause the running instance in order to prevent a short period of time where +its network interface does not have firewall rules. This can happen due to +the firewall driver calling OVS to obtain information about OVS the port. Once +the instance is paused and no traffic is flowing, we can delete the qvo +interface from integration bridge, detach the tap device from the qbr bridge +and plug the tap device back into the integration bridge. Once this is done, +the firewall rules are applied for the OVS tap interface and the instance is +started from its paused state. + +2) Set drop rules for the instance's tap interface, delete the qbr bridge and +related veths, plug the tap device into the integration bridge, apply the OVS +firewall rules and finally remove the drop rules for the instance. + +3) Compute nodes can be upgraded one at a time. A free node can be switched to +use the OVS firewall, and instances from other nodes can be live-migrated to +it. Once the first node is evacuated, its firewall driver can be then be +switched to the OVS driver. diff --git a/neutron/agent/firewall.py b/neutron/agent/firewall.py index 42f4a01c3a6..caeecb018b4 100644 --- a/neutron/agent/firewall.py +++ b/neutron/agent/firewall.py @@ -18,10 +18,24 @@ import contextlib import six +from neutron.common import utils +from neutron.extensions import portsecurity as psec INGRESS_DIRECTION = 'ingress' EGRESS_DIRECTION = 'egress' +DIRECTION_IP_PREFIX = {INGRESS_DIRECTION: 'source_ip_prefix', + EGRESS_DIRECTION: 'dest_ip_prefix'} + + +def port_sec_enabled(port): + return port.get(psec.PORTSECURITY, True) + + +def load_firewall_driver_class(driver): + return utils.load_class_by_alias_or_classname( + 'neutron.agent.firewall_drivers', driver) + @six.add_metaclass(abc.ABCMeta) class FirewallDriver(object): @@ -57,6 +71,10 @@ class FirewallDriver(object): remote_group_id will also remaining membership update management """ + # OVS agent installs arp spoofing openflow rules. If firewall is capable + # of handling that, ovs agent doesn't need to install the protection. + provides_arp_spoofing_protection = False + @abc.abstractmethod def prepare_port_filter(self, port): """Prepare filters for the port. diff --git a/neutron/agent/linux/iptables_firewall.py b/neutron/agent/linux/iptables_firewall.py index f34c83b73e8..36a1feccb0f 100644 --- a/neutron/agent/linux/iptables_firewall.py +++ b/neutron/agent/linux/iptables_firewall.py @@ -32,7 +32,6 @@ from neutron.common import constants from neutron.common import exceptions as n_exc from neutron.common import ipv6_utils from neutron.common import utils as c_utils -from neutron.extensions import portsecurity as psec LOG = logging.getLogger(__name__) @@ -41,8 +40,6 @@ SPOOF_FILTER = 'spoof-filter' CHAIN_NAME_PREFIX = {firewall.INGRESS_DIRECTION: 'i', firewall.EGRESS_DIRECTION: 'o', SPOOF_FILTER: 's'} -DIRECTION_IP_PREFIX = {firewall.INGRESS_DIRECTION: 'source_ip_prefix', - firewall.EGRESS_DIRECTION: 'dest_ip_prefix'} IPSET_DIRECTION = {firewall.INGRESS_DIRECTION: 'src', firewall.EGRESS_DIRECTION: 'dst'} # length of all device prefixes (e.g. qvo, tap, qvb) @@ -146,11 +143,8 @@ class IptablesFirewallDriver(firewall.FirewallDriver): LOG.debug("Update members of security group (%s)", sg_id) self.sg_members[sg_id] = collections.defaultdict(list, sg_members) - def _ps_enabled(self, port): - return port.get(psec.PORTSECURITY, True) - def _set_ports(self, port): - if not self._ps_enabled(port): + if not firewall.port_sec_enabled(port): self.unfiltered_ports[port['device']] = port self.filtered_ports.pop(port['device'], None) else: @@ -466,7 +460,8 @@ class IptablesFirewallDriver(firewall.FirewallDriver): for ip in self.sg_members[remote_group_id][ethertype]: if ip not in port_ips: ip_rule = rule.copy() - direction_ip_prefix = DIRECTION_IP_PREFIX[direction] + direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[ + direction] ip_prefix = str(netaddr.IPNetwork(ip).cidr) ip_rule[direction_ip_prefix] = ip_prefix yield ip_rule diff --git a/neutron/agent/linux/openvswitch_firewall/__init__.py b/neutron/agent/linux/openvswitch_firewall/__init__.py new file mode 100644 index 00000000000..ea2e15399f2 --- /dev/null +++ b/neutron/agent/linux/openvswitch_firewall/__init__.py @@ -0,0 +1,18 @@ +# 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.agent.linux.openvswitch_firewall import firewall + +OVSFirewallDriver = firewall.OVSFirewallDriver diff --git a/neutron/agent/linux/openvswitch_firewall/constants.py b/neutron/agent/linux/openvswitch_firewall/constants.py new file mode 100644 index 00000000000..347db037a5f --- /dev/null +++ b/neutron/agent/linux/openvswitch_firewall/constants.py @@ -0,0 +1,34 @@ +# 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.common import constants + +OF_STATE_NOT_TRACKED = "-trk" +OF_STATE_ESTABLISHED = "+trk+est-rel-inv" +OF_STATE_RELATED = "+trk+rel-est-inv" +OF_STATE_INVALID = "+trk+inv" + +protocol_to_nw_proto = { + constants.PROTO_NAME_ICMP: constants.PROTO_NUM_ICMP, + constants.PROTO_NAME_TCP: constants.PROTO_NUM_TCP, + constants.PROTO_NAME_UDP: constants.PROTO_NUM_UDP, +} + +PROTOCOLS_WITH_PORTS = (constants.PROTO_NAME_TCP, constants.PROTO_NAME_UDP) + +ethertype_to_dl_type_map = { + constants.IPv4: constants.ETHERTYPE_IP, + constants.IPv6: constants.ETHERTYPE_IPV6, +} diff --git a/neutron/agent/linux/openvswitch_firewall/firewall.py b/neutron/agent/linux/openvswitch_firewall/firewall.py new file mode 100644 index 00000000000..7b0eb9e9196 --- /dev/null +++ b/neutron/agent/linux/openvswitch_firewall/firewall.py @@ -0,0 +1,546 @@ +# 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 oslo_log import log as logging + +from neutron._i18n import _, _LE +from neutron.agent import firewall +from neutron.agent.linux.openvswitch_firewall import constants as ovsfw_consts +from neutron.agent.linux.openvswitch_firewall import rules +from neutron.common import constants +from neutron.common import exceptions +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts + +LOG = logging.getLogger(__name__) + + +class OVSFWPortNotFound(exceptions.NeutronException): + message = _("Port %(port_id)s is not managed by this agent. ") + + +class SecurityGroup(object): + def __init__(self, id_): + self.id = id_ + self.raw_rules = [] + self.remote_rules = [] + self.members = {} + self.ports = set() + + def update_rules(self, rules): + """Separate raw and remote rules.""" + self.raw_rules = [rule for rule in rules + if 'remote_group_id' not in rule] + self.remote_rules = [rule for rule in rules + if 'remote_group_id' in rule] + + def get_ethertype_filtered_addresses(self, ethertype, + exclude_addresses=None): + exclude_addresses = set(exclude_addresses) or set() + group_addresses = set(self.members.get(ethertype, [])) + return list(group_addresses - exclude_addresses) + + +class OFPort(object): + def __init__(self, port_dict, ovs_port): + self.id = port_dict['device'] + self.mac = ovs_port.vif_mac + self.ofport = ovs_port.ofport + self.sec_groups = list() + 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.IPAddress(aap['ip_address']).version == version} + + @property + def ipv4_addresses(self): + return [ip_addr for ip_addr in self.fixed_ips + if netaddr.IPAddress(ip_addr).version == 4] + + @property + def ipv6_addresses(self): + return [ip_addr for ip_addr 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) + self.fixed_ips = port_dict.get('fixed_ips', []) + self.neutron_port_dict = port_dict.copy() + + +class SGPortMap(object): + def __init__(self): + self.ports = {} + self.sec_groups = {} + + def get_or_create_sg(self, sg_id): + try: + sec_group = self.sec_groups[sg_id] + except KeyError: + sec_group = SecurityGroup(sg_id) + self.sec_groups[sg_id] = sec_group + return sec_group + + def create_port(self, port, port_dict): + self.ports[port.id] = port + self.update_port(port, port_dict) + + def update_port(self, port, port_dict): + for sec_group in self.sec_groups.values(): + sec_group.ports.discard(port) + + port.sec_groups = [self.get_or_create_sg(sg_id) + for sg_id in port_dict['security_groups']] + for sec_group in port.sec_groups: + sec_group.ports.add(port) + port.update(port_dict) + + def remove_port(self, port): + for sec_group in port.sec_groups: + sec_group.ports.discard(port) + del self.ports[port.id] + + def update_rules(self, sg_id, rules): + sec_group = self.get_or_create_sg(sg_id) + sec_group.update_rules(rules) + + def update_members(self, sg_id, members): + sec_group = self.get_or_create_sg(sg_id) + sec_group.members = members + + +class OVSFirewallDriver(firewall.FirewallDriver): + REQUIRED_PROTOCOLS = ",".join([ + ovs_consts.OPENFLOW10, + ovs_consts.OPENFLOW11, + ovs_consts.OPENFLOW12, + ovs_consts.OPENFLOW13, + ovs_consts.OPENFLOW14, + ]) + + provides_arp_spoofing_protection = True + + def __init__(self, integration_bridge): + """Initialize object + + :param integration_bridge: Bridge on which openflow rules will be + applied + + """ + self.int_br = self.initialize_bridge(integration_bridge) + self.sg_port_map = SGPortMap() + self._deferred = False + self._drop_all_unmatched_flows() + + def apply_port_filter(self, port): + """We never call this method + + It exists here to override abstract method of parent abstract class. + """ + + def security_group_updated(self, action_type, sec_group_ids, + device_ids=None): + """This method is obsolete + + The current driver only supports enhanced rpc calls into security group + agent. This method is never called from that place. + """ + + def _add_flow(self, **kwargs): + dl_type = kwargs.get('dl_type') + 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) + + def _delete_flows(self, **kwargs): + if self._deferred: + self.int_br.delete_flows(**kwargs) + else: + self.int_br.br.delete_flows(**kwargs) + + @staticmethod + def initialize_bridge(int_br): + int_br.set_protocols(OVSFirewallDriver.REQUIRED_PROTOCOLS) + return int_br.deferred(full_ordered=True) + + def _drop_all_unmatched_flows(self): + for table in ovs_consts.OVS_FIREWALL_TABLES: + self.int_br.br.add_flow(table=table, priority=0, actions='drop') + + def get_or_create_ofport(self, port): + port_id = port['device'] + try: + of_port = self.sg_port_map.ports[port_id] + except KeyError: + ovs_port = self.int_br.br.get_vif_port_by_id(port_id) + if not ovs_port: + raise OVSFWPortNotFound(port_id=port_id) + of_port = OFPort(port, ovs_port) + self.sg_port_map.create_port(of_port, port) + else: + self.sg_port_map.update_port(of_port, port) + + return of_port + + def is_port_managed(self, port): + return port['device'] in self.sg_port_map.ports + + def prepare_port_filter(self, port): + if not firewall.port_sec_enabled(port): + return + port_exists = self.is_port_managed(port) + of_port = self.get_or_create_ofport(port) + if port_exists: + LOG.error(_LE("Initializing port %s that was already " + "initialized."), + port['device']) + self.delete_all_port_flows(of_port) + self.initialize_port_flows(of_port) + self.add_flows_from_rules(of_port) + + 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 security group rules and members. + + """ + if not firewall.port_sec_enabled(port): + self.remove_port_filter(port) + return + elif not self.is_port_managed(port): + self.prepare_port_filter(port) + return + of_port = self.get_or_create_ofport(port) + # TODO(jlibosva): Handle firewall blink + self.delete_all_port_flows(of_port) + self.initialize_port_flows(of_port) + self.add_flows_from_rules(of_port) + + 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_or_create_ofport(port) + self.delete_all_port_flows(of_port) + self.sg_port_map.remove_port(of_port) + + def update_security_group_rules(self, sg_id, rules): + self.sg_port_map.update_rules(sg_id, rules) + + def update_security_group_members(self, sg_id, member_ips): + self.sg_port_map.update_members(sg_id, member_ips) + + def filter_defer_apply_on(self): + self._deferred = True + + def filter_defer_apply_off(self): + if self._deferred: + self.int_br.apply_flows() + self._deferred = False + + @property + def ports(self): + return {id_: port.neutron_port_dict + for id_, port in self.sg_port_map.ports.items()} + + def initialize_port_flows(self, port): + """Set base flows for port + + :param port: OFPort instance + + """ + # Identify egress flow + self._add_flow( + table=ovs_consts.LOCAL_SWITCHING, + priority=100, + in_port=port.ofport, + actions='set_field:{:d}->reg5,resubmit(,{:d})'.format( + port.ofport, ovs_consts.BASE_EGRESS_TABLE) + ) + + # Identify ingress flows after egress filtering + self._add_flow( + table=ovs_consts.LOCAL_SWITCHING, + priority=90, + dl_dst=port.mac, + actions='set_field:{:d}->reg5,resubmit(,{:d})'.format( + port.ofport, ovs_consts.BASE_INGRESS_TABLE), + ) + + self._initialize_egress(port) + self._initialize_ingress(port) + + def _initialize_egress(self, port): + """Identify egress traffic and send it to egress base""" + + # 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=ovs_consts.BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg5=port.ofport, + dl_src=mac_addr, + dl_type=constants.ETHERTYPE_ARP, + arp_spa=ip_addr, + actions='normal' + ) + self._add_flow( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=65, + reg5=port.ofport, + ct_state=ovsfw_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_REG5[0..15])'.format( + ovs_consts.RULES_EGRESS_TABLE) + ) + + # 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=ovs_consts.BASE_EGRESS_TABLE, + priority=95, + in_port=port.ofport, + reg5=port.ofport, + dl_type=constants.ETHERTYPE_IPV6, + nw_proto=constants.PROTO_NUM_IPV6_ICMP, + icmp_type=constants.ICMPV6_TYPE_NA, + actions='normal' + ) + self._add_flow( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=65, + reg5=port.ofport, + in_port=port.ofport, + ct_state=ovsfw_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_REG5[0..15])'.format( + ovs_consts.RULES_EGRESS_TABLE) + ) + + # DHCP discovery + for dl_type, src_port, dst_port in ( + (constants.ETHERTYPE_IP, 68, 67), + (constants.ETHERTYPE_IPV6, 546, 547)): + self._add_flow( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=80, + reg5=port.ofport, + in_port=port.ofport, + dl_type=dl_type, + nw_proto=constants.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='resubmit(,{:d})'.format( + ovs_consts.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=ovs_consts.BASE_EGRESS_TABLE, + priority=70, + in_port=port.ofport, + reg5=port.ofport, + dl_type=dl_type, + nw_proto=constants.PROTO_NUM_UDP, + tp_src=src_port, + tp_dst=dst_port, + actions='drop' + ) + + # Drop all remaining not tracked egress connections + self._add_flow( + table=ovs_consts.BASE_EGRESS_TABLE, + priority=10, + ct_state=ovsfw_consts.OF_STATE_NOT_TRACKED, + in_port=port.ofport, + reg5=port.ofport, + actions='drop' + ) + + # Fill in accept_or_ingress table by checking that traffic is ingress + # and if not, accept it + self._add_flow( + table=ovs_consts.ACCEPT_OR_INGRESS_TABLE, + priority=100, + dl_dst=port.mac, + actions='set_field:{:d}->reg5,resubmit(,{:d})'.format( + port.ofport, ovs_consts.BASE_INGRESS_TABLE), + ) + self._add_flow( + table=ovs_consts.ACCEPT_OR_INGRESS_TABLE, + priority=90, + reg5=port.ofport, + in_port=port.ofport, + actions='ct(commit,zone=NXM_NX_REG5[0..15]),normal' + ) + + def _initialize_tracked_egress(self, port): + self._add_flow( + table=ovs_consts.RULES_EGRESS_TABLE, + priority=90, + ct_state=ovsfw_consts.OF_STATE_INVALID, + actions='drop', + ) + for state in ( + ovsfw_consts.OF_STATE_ESTABLISHED, + ovsfw_consts.OF_STATE_RELATED, + ): + self._add_flow( + table=ovs_consts.RULES_EGRESS_TABLE, + priority=80, + ct_state=state, + reg5=port.ofport, + ct_zone=port.ofport, + actions='normal' + ) + + def _initialize_ingress(self, port): + # Allow incoming ARPs + self._add_flow( + table=ovs_consts.BASE_INGRESS_TABLE, + priority=100, + dl_type=constants.ETHERTYPE_ARP, + reg5=port.ofport, + dl_dst=port.mac, + actions='output:{:d}'.format(port.ofport), + ) + # Neighbor soliciation + self._add_flow( + table=ovs_consts.BASE_INGRESS_TABLE, + priority=100, + reg5=port.ofport, + dl_dst=port.mac, + dl_type=constants.ETHERTYPE_IPV6, + nw_proto=constants.PROTO_NUM_IPV6_ICMP, + icmp_type=constants.ICMPV6_TYPE_NC, + actions='output:{:d}'.format(port.ofport), + ) + # DHCP offers + for dl_type, src_port, dst_port in ( + (constants.ETHERTYPE_IP, 67, 68), + (constants.ETHERTYPE_IPV6, 547, 546)): + self._add_flow( + table=ovs_consts.BASE_INGRESS_TABLE, + priority=95, + reg5=port.ofport, + dl_type=dl_type, + nw_proto=constants.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=ovs_consts.BASE_INGRESS_TABLE, + priority=90, + reg5=port.ofport, + dl_type=dl_type, + ct_state=ovsfw_consts.OF_STATE_NOT_TRACKED, + actions='ct(table={:d},zone=NXM_NX_REG5[0..15])'.format( + ovs_consts.RULES_INGRESS_TABLE) + ) + self._add_flow( + table=ovs_consts.BASE_INGRESS_TABLE, + priority=80, + reg5=port.ofport, + dl_dst=port.mac, + actions='resubmit(,{:d})'.format(ovs_consts.RULES_INGRESS_TABLE) + ) + + def _initialize_tracked_ingress(self, port): + # Drop invalid packets + self._add_flow( + table=ovs_consts.RULES_INGRESS_TABLE, + priority=100, + ct_state=ovsfw_consts.OF_STATE_INVALID, + actions='drop' + ) + # Allow established and related connections + for state in (ovsfw_consts.OF_STATE_ESTABLISHED, + ovsfw_consts.OF_STATE_RELATED): + self._add_flow( + table=ovs_consts.RULES_INGRESS_TABLE, + priority=80, + dl_dst=port.mac, + reg5=port.ofport, + ct_state=state, + ct_zone=port.ofport, + actions='output:{:d}'.format(port.ofport) + ) + + 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) + rules_generator = self.create_rules_generator_for_port(port) + for rule in rules_generator: + 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: + self._add_flow(**flow) + + def create_rules_generator_for_port(self, port): + for sec_group in port.sec_groups: + for rule in sec_group.raw_rules: + yield rule + for rule in sec_group.remote_rules: + remote_group = self.sg_port_map.sec_groups[ + rule['remote_group_id']] + for ip_addr in remote_group.get_ethertype_filtered_addresses( + rule['ethertype'], port.fixed_ips): + yield rules.create_rule_for_ip_address(ip_addr, rule) + + def delete_all_port_flows(self, port): + """Delete all flows for given port""" + self._delete_flows(table=ovs_consts.LOCAL_SWITCHING, dl_dst=port.mac) + self._delete_flows(table=ovs_consts.LOCAL_SWITCHING, + in_port=port.ofport) + self._delete_flows(reg5=port.ofport) + self._delete_flows(table=ovs_consts.ACCEPT_OR_INGRESS_TABLE, + dl_dst=port.mac) diff --git a/neutron/agent/linux/openvswitch_firewall/rules.py b/neutron/agent/linux/openvswitch_firewall/rules.py new file mode 100644 index 00000000000..32404033d9e --- /dev/null +++ b/neutron/agent/linux/openvswitch_firewall/rules.py @@ -0,0 +1,122 @@ +# 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 oslo_log import log as logging +import six + +from neutron.agent import firewall +from neutron.agent.linux.openvswitch_firewall import constants as ovsfw_consts +from neutron.common import constants +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts + +LOG = logging.getLogger(__name__) + + +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') + + flow_template = { + 'priority': 70, + 'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype], + 'reg5': port.ofport, + } + + if dst_ip_prefix and dst_ip_prefix != "0.0.0.0/0": + flow_template["nw_dst"] = dst_ip_prefix + + if src_ip_prefix and src_ip_prefix != "0.0.0.0/0": + flow_template["nw_src"] = src_ip_prefix + + flows = create_protocol_flows(direction, flow_template, port, rule) + + return flows + + +def create_protocol_flows(direction, flow_template, port, rule): + flow_template = flow_template.copy() + if direction == firewall.INGRESS_DIRECTION: + flow_template['table'] = ovs_consts.RULES_INGRESS_TABLE + flow_template['dl_dst'] = port.mac + flow_template['actions'] = ('ct(commit,zone=NXM_NX_REG5[0..15]),' + 'output:{:d}'.format(port.ofport)) + elif direction == firewall.EGRESS_DIRECTION: + flow_template['table'] = ovs_consts.RULES_EGRESS_TABLE + flow_template['dl_src'] = port.mac + # Traffic can be both ingress and egress, check that no ingress rules + # should be applied + flow_template['actions'] = 'resubmit(,{:d})'.format( + ovs_consts.ACCEPT_OR_INGRESS_TABLE) + protocol = rule.get('protocol') + try: + flow_template['nw_proto'] = ovsfw_consts.protocol_to_nw_proto[protocol] + if rule['ethertype'] == constants.IPv6 and protocol == 'icmp': + flow_template['nw_proto'] = constants.PROTO_NUM_IPV6_ICMP + except KeyError: + pass + + flows = create_port_range_flows(flow_template, rule) + if not flows: + return [flow_template] + return flows + + +def create_port_range_flows(flow_template, rule): + protocol = rule.get('protocol') + if protocol not in ovsfw_consts.PROTOCOLS_WITH_PORTS: + return [] + flows = [] + src_port_match = '{:s}_src'.format(protocol) + #FIXME(jlibosva): Actually source_port_range_min is just a dead code in + # security groups rpc layer and should be removed + 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') + + if src_port_min and src_port_max: + for port in six.moves.range(src_port_min, src_port_max + 1): + flow = flow_template.copy() + flow[src_port_match] = port + try: + for port in six.moves.range(dst_port_min, dst_port_max + 1): + dst_flow = flow.copy() + dst_flow[dst_port_match] = port + flows.append(dst_flow) + except TypeError: + flows.append(flow) + elif dst_port_min and dst_port_max: + for port in six.moves.range(dst_port_min, dst_port_max + 1): + flow = flow_template.copy() + flow[dst_port_match] = port + flows.append(flow) + + return flows + + +def create_rule_for_ip_address(ip_address, rule): + new_rule = rule.copy() + del new_rule['remote_group_id'] + direction = rule['direction'] + ip_prefix = str(netaddr.IPNetwork(ip_address).cidr) + new_rule[firewall.DIRECTION_IP_PREFIX[direction]] = ip_prefix + LOG.debug('RULGEN: From rule %s with IP %s created new rule %s', + rule, ip_address, new_rule) + return new_rule diff --git a/neutron/agent/securitygroups_rpc.py b/neutron/agent/securitygroups_rpc.py index e849b8b21f9..26bbbbf5763 100644 --- a/neutron/agent/securitygroups_rpc.py +++ b/neutron/agent/securitygroups_rpc.py @@ -19,7 +19,6 @@ import functools from oslo_config import cfg from oslo_log import log as logging import oslo_messaging -from oslo_utils import importutils from neutron._i18n import _, _LI, _LW from neutron.agent import firewall @@ -87,21 +86,30 @@ class SecurityGroupAgentRpc(object): """Enables SecurityGroup agent support in agent implementations.""" def __init__(self, context, plugin_rpc, local_vlan_map=None, - defer_refresh_firewall=False,): + defer_refresh_firewall=False, integration_bridge=None): self.context = context self.plugin_rpc = plugin_rpc - self.init_firewall(defer_refresh_firewall) + self.init_firewall(defer_refresh_firewall, integration_bridge) self.local_vlan_map = local_vlan_map - def init_firewall(self, defer_refresh_firewall=False): - firewall_driver = cfg.CONF.SECURITYGROUP.firewall_driver + def init_firewall(self, defer_refresh_firewall=False, + integration_bridge=None): + firewall_driver = cfg.CONF.SECURITYGROUP.firewall_driver or 'noop' LOG.debug("Init firewall settings (driver=%s)", firewall_driver) if not _is_valid_driver_combination(): LOG.warn(_LW("Driver configuration doesn't match " "with enable_security_group")) - if not firewall_driver: - firewall_driver = 'neutron.agent.firewall.NoopFirewallDriver' - self.firewall = importutils.import_object(firewall_driver) + firewall_class = firewall.load_firewall_driver_class(firewall_driver) + try: + self.firewall = firewall_class( + integration_bridge=integration_bridge) + except TypeError as e: + LOG.warning(_LW("Firewall driver {fw_driver} doesn't accept " + "integration_bridge parameter in __init__(): " + "{err}"), + fw_driver=firewall_driver, + err=e) + self.firewall = firewall_class() # The following flag will be set to true if port filter must not be # applied as soon as a rule or membership notification is received self.defer_refresh_firewall = defer_refresh_firewall diff --git a/neutron/cmd/sanity/checks.py b/neutron/cmd/sanity/checks.py index 3825a680309..21eb5aca7c1 100644 --- a/neutron/cmd/sanity/checks.py +++ b/neutron/cmd/sanity/checks.py @@ -332,6 +332,20 @@ def ovsdb_native_supported(): return False +def ovs_conntrack_supported(): + random_str = utils.get_random_string(6) + br_name = "ovs-test-" + random_str + + with ovs_lib.OVSBridge(br_name) as br: + try: + br.set_protocols( + "OpenFlow10,OpenFlow11,OpenFlow12,OpenFlow13,OpenFlow14") + except RuntimeError as e: + LOG.debug("Exception while checking ovs conntrack support: %s", e) + return False + return ofctl_arg_supported(cmd='add-flow', ct_state='+trk', actions='drop') + + def ebtables_supported(): try: cmd = ['ebtables', '--version'] diff --git a/neutron/cmd/sanity_check.py b/neutron/cmd/sanity_check.py index c5d8b1281bb..2d96058f0ad 100644 --- a/neutron/cmd/sanity_check.py +++ b/neutron/cmd/sanity_check.py @@ -193,6 +193,18 @@ def check_ovsdb_native(): return result +def check_ovs_conntrack(): + result = checks.ovs_conntrack_supported() + if not result: + LOG.error(_LE('Check for Open vSwitch support of conntrack support ' + 'failed. OVS/CT firewall will not work. A newer ' + 'version of OVS (2.5+) and linux kernel (4.3+) are ' + 'required. See ' + 'https://github.com/openvswitch/ovs/blob/master/FAQ.md' + 'for more information.')) + return result + + def check_ebtables(): result = checks.ebtables_supported() if not result: @@ -242,6 +254,8 @@ OPTS = [ help=_('Check minimal dnsmasq version')), BoolOptCallback('ovsdb_native', check_ovsdb_native, help=_('Check ovsdb native interface support')), + BoolOptCallback('ovs_conntrack', check_ovs_conntrack, + help=_('Check ovs conntrack support')), BoolOptCallback('ebtables_installed', check_ebtables, help=_('Check ebtables installation')), BoolOptCallback('keepalived_ipv6_support', check_keepalived_ipv6_support, diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 3607c821685..ac98c302dbd 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -48,6 +48,8 @@ SORT_DIRECTION_ASC = 'asc' SORT_DIRECTION_DESC = 'desc' ETHERTYPE_NAME_ARP = 'arp' +ETHERTYPE_ARP = 0x0806 +ETHERTYPE_IP = 0x0800 ETHERTYPE_IPV6 = 0x86DD # Protocol names and numbers for Security Groups/Firewalls @@ -122,10 +124,11 @@ IP_PROTOCOL_MAP = {PROTO_NAME_AH: PROTO_NUM_AH, # Multicast Listener Report (131), # Multicast Listener Done (132), # Neighbor Solicitation (135), +ICMPV6_TYPE_NC = 135 # Neighbor Advertisement (136) +ICMPV6_TYPE_NA = 136 ICMPV6_ALLOWED_TYPES = [130, 131, 132, 135, 136] ICMPV6_TYPE_RA = 134 -ICMPV6_TYPE_NA = 136 DHCPV6_STATEFUL = 'dhcpv6-stateful' DHCPV6_STATELESS = 'dhcpv6-stateless' diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py b/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py index bc6dafbb35f..650397027a2 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py @@ -50,6 +50,21 @@ CANARY_TABLE = 23 # Table for ARP poison/spoofing prevention rules ARP_SPOOF_TABLE = 24 +# Tables used for ovs firewall +BASE_EGRESS_TABLE = 71 +RULES_EGRESS_TABLE = 72 +ACCEPT_OR_INGRESS_TABLE = 73 +BASE_INGRESS_TABLE = 81 +RULES_INGRESS_TABLE = 82 + +OVS_FIREWALL_TABLES = ( + BASE_EGRESS_TABLE, + RULES_EGRESS_TABLE, + ACCEPT_OR_INGRESS_TABLE, + BASE_INGRESS_TABLE, + RULES_INGRESS_TABLE, +) + ## Tunnel bridge (tun_br) # Various tables for tunneling flows @@ -114,3 +129,10 @@ OVS_DPDK_VHOST_USER = 'dpdkvhostuser' VHOST_USER_SOCKET_DIR = '/var/run/openvswitch' MAX_DEVICE_RETRIES = 5 + +# OpenFlow version constants +OPENFLOW10 = "OpenFlow10" +OPENFLOW11 = "OpenFlow11" +OPENFLOW12 = "OpenFlow12" +OPENFLOW13 = "OpenFlow13" +OPENFLOW14 = "OpenFlow14" diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/ovs_bridge.py b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/ovs_bridge.py index 85173a9d0b8..1c4f8a894c9 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/ovs_bridge.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/ovs_bridge.py @@ -19,6 +19,8 @@ from oslo_utils import excutils from neutron._i18n import _LI from neutron.agent.common import ovs_lib +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \ import ofswitch @@ -72,7 +74,7 @@ class OVSAgentBridge(ofswitch.OpenFlowSwitchMixin, ovs_lib.OVSBridge): "port": conf.OVS.of_listen_port, } ] - self.set_protocols("OpenFlow13") + self.set_protocols(ovs_consts.OPENFLOW13) self.set_controller(controllers) def drop_port(self, in_port): diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/ovs_bridge.py b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/ovs_bridge.py index 6e957534dc3..428d8a71a5c 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/ovs_bridge.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/ovs_ofctl/ovs_bridge.py @@ -16,6 +16,8 @@ from neutron.agent.common import ovs_lib +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl \ import ofswitch @@ -24,7 +26,7 @@ class OVSAgentBridge(ofswitch.OpenFlowSwitchMixin, ovs_lib.OVSBridge): """Common code for bridges used by OVS agent""" def setup_controllers(self, conf): - self.set_protocols("[OpenFlow10]") + self.set_protocols(ovs_consts.OPENFLOW10) self.del_controller() def drop_port(self, in_port): diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index d7a200616fe..09ebb7a3c61 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -163,7 +163,6 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin, # ML2 l2 population mechanism driver. self.enable_distributed_routing = agent_conf.enable_distributed_routing self.arp_responder_enabled = agent_conf.arp_responder and self.l2_pop - self.prevent_arp_spoofing = agent_conf.prevent_arp_spoofing host = self.conf.host self.agent_id = 'ovs-agent-%s' % host @@ -277,7 +276,11 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin, # Security group agent support self.sg_agent = sg_rpc.SecurityGroupAgentRpc(self.context, self.sg_plugin_rpc, self.local_vlan_map, - defer_refresh_firewall=True) + defer_refresh_firewall=True, integration_bridge=self.int_br) + + self.prevent_arp_spoofing = ( + agent_conf.prevent_arp_spoofing and + not self.sg_agent.firewall.provides_arp_spoofing_protection) # Initialize iteration counter self.iter_num = 0 @@ -819,7 +822,8 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin, LOG.debug("Port %s was deleted concurrently, skipping it", port.port_name) continue - if cur_tag != lvm.vlan: + # Uninitialized port has tag set to [] + if cur_tag and cur_tag != lvm.vlan: self.int_br.delete_flows(in_port=port.ofport) if self.prevent_arp_spoofing: self.setup_arp_spoofing_protection(self.int_br, diff --git a/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py b/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py index 0a835243bbc..da3f6377c94 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py +++ b/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py @@ -15,6 +15,8 @@ import os +from oslo_config import cfg + from neutron.agent import securitygroups_rpc from neutron.common import constants from neutron.extensions import portbindings @@ -25,6 +27,9 @@ from neutron.plugins.ml2.drivers.openvswitch.agent.common \ import constants as a_const from neutron.services.qos import qos_consts +IPTABLES_FW_DRIVER_FULL = ("neutron.agent.linux.iptables_firewall." + "OVSHybridIptablesFirewallDriver") + class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): """Attach to networks using openvswitch L2 agent. @@ -40,8 +45,10 @@ class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): def __init__(self): sg_enabled = securitygroups_rpc.is_firewall_enabled() + hybrid_plug_required = (cfg.CONF.SECURITYGROUP.firewall_driver in ( + IPTABLES_FW_DRIVER_FULL, 'iptables_hybrid')) and sg_enabled vif_details = {portbindings.CAP_PORT_FILTER: sg_enabled, - portbindings.OVS_HYBRID_PLUG: sg_enabled} + portbindings.OVS_HYBRID_PLUG: hybrid_plug_required} super(OpenvswitchMechanismDriver, self).__init__( constants.AGENT_TYPE_OVS, portbindings.VIF_TYPE_OVS, diff --git a/neutron/tests/common/conn_testers.py b/neutron/tests/common/conn_testers.py index 13063734ebe..a33f7807494 100644 --- a/neutron/tests/common/conn_testers.py +++ b/neutron/tests/common/conn_testers.py @@ -16,6 +16,7 @@ import functools import fixtures from oslo_log import log as logging +from oslo_utils import uuidutils from neutron.agent import firewall from neutron.agent.linux import ip_lib @@ -64,8 +65,8 @@ class ConnectionTester(fixtures.Fixture): self.TCP: self._test_transport_connectivity, self.ICMP: self._test_icmp_connectivity, self.ARP: self._test_arp_connectivity} - self._nc_testers = dict() - self._pingers = dict() + self._nc_testers = {} + self._pingers = {} self.addCleanup(self.cleanup) def cleanup(self): @@ -288,6 +289,53 @@ class ConnectionTester(fixtures.Fixture): return pinger.received +class OVSConnectionTester(ConnectionTester): + """Tester with OVS bridge in the middle + + The endpoints are created as OVS ports attached to the OVS bridge. + + NOTE: The OVS ports are connected from the namespace. This connection is + currently not supported in OVS and may lead to unpredicted behavior: + https://bugzilla.redhat.com/show_bug.cgi?id=1160340 + + """ + + def setUp(self): + super(OVSConnectionTester, self).setUp() + self.bridge = self.useFixture(net_helpers.OVSBridgeFixture()).bridge + self._peer, self._vm = self.useFixture( + machine_fixtures.PeerMachines(self.bridge)).machines + self._set_port_attrs(self._peer.port) + self._set_port_attrs(self._vm.port) + + def _set_port_attrs(self, port): + port.id = uuidutils.generate_uuid() + attrs = [('type', 'internal'), + ('external_ids', { + 'iface-id': port.id, + 'iface-status': 'active', + 'attached-mac': port.link.address})] + for column, value in attrs: + self.bridge.set_db_attribute('Interface', port.name, column, value) + + @property + def peer_port_id(self): + return self._peer.port.id + + @property + def vm_port_id(self): + return self._vm.port.id + + def set_tag(self, port_name, tag): + self.bridge.set_db_attribute('Port', port_name, 'tag', tag) + + def set_vm_tag(self, tag): + self.set_tag(self._vm.port.name, tag) + + def set_peer_tag(self, tag): + self.set_tag(self._peer.port.name, tag) + + class LinuxBridgeConnectionTester(ConnectionTester): """Tester with linux bridge in the middle @@ -298,13 +346,13 @@ class LinuxBridgeConnectionTester(ConnectionTester): def _setUp(self): super(LinuxBridgeConnectionTester, self)._setUp() - self._bridge = self.useFixture(net_helpers.LinuxBridgeFixture()).bridge + self.bridge = self.useFixture(net_helpers.LinuxBridgeFixture()).bridge self._peer, self._vm = self.useFixture( - machine_fixtures.PeerMachines(self._bridge)).machines + machine_fixtures.PeerMachines(self.bridge)).machines @property def bridge_namespace(self): - return self._bridge.namespace + return self.bridge.namespace @property def vm_port_id(self): @@ -315,7 +363,7 @@ class LinuxBridgeConnectionTester(ConnectionTester): return net_helpers.VethFixture.get_peer_name(self._peer.port.name) def flush_arp_tables(self): - self._bridge.neigh.flush(4, 'all') + self.bridge.neigh.flush(4, 'all') super(LinuxBridgeConnectionTester, self).flush_arp_tables() def collect_debug_info(self, exc_info): diff --git a/neutron/tests/functional/agent/test_firewall.py b/neutron/tests/functional/agent/test_firewall.py index d6165bc89b3..b54bd04ef38 100644 --- a/neutron/tests/functional/agent/test_firewall.py +++ b/neutron/tests/functional/agent/test_firewall.py @@ -18,6 +18,7 @@ # under the License. import copy +import functools import netaddr from oslo_config import cfg @@ -25,7 +26,9 @@ import testscenarios from neutron.agent import firewall from neutron.agent.linux import iptables_firewall +from neutron.agent.linux import openvswitch_firewall from neutron.agent import securitygroups_rpc as sg_cfg +from neutron.cmd.sanity import checks from neutron.common import constants from neutron.tests.common import conn_testers from neutron.tests.functional import base @@ -47,6 +50,15 @@ reverse_transport_protocol = { DEVICE_OWNER_COMPUTE = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'fake' +def skip_if_not_iptables(f): + @functools.wraps(f) + def wrap(self, *args, **kwargs): + if not hasattr(self, 'enable_ipset'): + self.skipTest("This test doesn't use iptables") + return f(self, *args, **kwargs) + return wrap + + def _add_rule(sg_rules, base, port_range_min=None, port_range_max=None): rule = copy.copy(base) if port_range_min: @@ -60,15 +72,34 @@ class FirewallTestCase(base.BaseSudoTestCase): FAKE_SECURITY_GROUP_ID = 'fake_sg_id' MAC_SPOOFED = "fa:16:3e:9a:2f:48" scenarios = [('IptablesFirewallDriver without ipset', - {'enable_ipset': False}), + {'enable_ipset': False, + 'initialize': 'initialize_iptables'}), ('IptablesFirewallDriver with ipset', - {'enable_ipset': True})] + {'enable_ipset': True, + 'initialize': 'initialize_iptables'}), + ('OVS Firewall Driver', + {'initialize': 'initialize_ovs'})] - def create_iptables_firewall(self): + def initialize_iptables(self): cfg.CONF.set_override('enable_ipset', self.enable_ipset, 'SECURITYGROUP') - return iptables_firewall.IptablesFirewallDriver( - namespace=self.tester.bridge_namespace) + tester = self.useFixture(conn_testers.LinuxBridgeConnectionTester()) + firewall_drv = iptables_firewall.IptablesFirewallDriver( + namespace=tester.bridge_namespace) + return tester, firewall_drv + + def initialize_ovs(self): + # Tests for ovs requires kernel >= 4.3 and OVS >= 2.5 + if not checks.ovs_conntrack_supported(): + self.skipTest("Open vSwitch with conntrack is not installed " + "on this machine. To run tests for OVS/CT firewall," + " please meet the requirements (kernel>=4.3, " + "OVS>=2.5. More info at" + "https://github.com/openvswitch/ovs/blob/master/" + "FAQ.md") + tester = self.useFixture(conn_testers.OVSConnectionTester()) + firewall_drv = openvswitch_firewall.OVSFirewallDriver(tester.bridge) + return tester, firewall_drv @staticmethod def _create_port_description(port_id, ip_addresses, mac_address, sg_ids): @@ -84,21 +115,28 @@ class FirewallTestCase(base.BaseSudoTestCase): def setUp(self): cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP') super(FirewallTestCase, self).setUp() - self.tester = self.useFixture( - conn_testers.LinuxBridgeConnectionTester()) + self.tester, self.firewall = getattr(self, self.initialize)() self.addOnException(self.tester.collect_debug_info) - self.firewall = self.create_iptables_firewall() - vm_mac = self.tester.vm_mac_address - vm_port_id = self.tester.vm_port_id self.src_port_desc = self._create_port_description( - vm_port_id, [self.tester.vm_ip_address], vm_mac, + self.tester.vm_port_id, + [self.tester.vm_ip_address], + self.tester.vm_mac_address, [self.FAKE_SECURITY_GROUP_ID]) + # FIXME(jlibosva): We should consider to call prepare_port_filter with + # deferred bridge depending on its performance self.firewall.prepare_port_filter(self.src_port_desc) def _apply_security_group_rules(self, sg_id, sg_rules): with self.firewall.defer_apply(): self.firewall.update_security_group_rules(sg_id, sg_rules) + self.firewall.update_port_filter(self.src_port_desc) + def _apply_security_group_members(self, sg_id, members): + with self.firewall.defer_apply(): + self.firewall.update_security_group_members(sg_id, members) + self.firewall.update_port_filter(self.src_port_desc) + + @skip_if_not_iptables def test_rule_application_converges(self): sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress'}, {'ethertype': 'IPv6', 'direction': 'egress'}, @@ -163,6 +201,7 @@ class FirewallTestCase(base.BaseSudoTestCase): # and the new one was inserted in the correct position self.assertEqual([], self.firewall.iptables._apply()) + @skip_if_not_iptables def test_rule_ordering_correct(self): sg_rules = [ {'ethertype': 'IPv4', 'direction': 'egress', 'protocol': 'tcp', @@ -235,6 +274,7 @@ class FirewallTestCase(base.BaseSudoTestCase): self.tester.assert_no_connection(protocol=self.tester.ICMP, direction=self.tester.EGRESS) + @skip_if_not_iptables def test_mac_spoofing_works_without_port_security_enabled(self): self.src_port_desc['port_security_enabled'] = False self.firewall.update_port_filter(self.src_port_desc) @@ -286,6 +326,7 @@ class FirewallTestCase(base.BaseSudoTestCase): self.tester.assert_no_connection(protocol=self.tester.ICMP, direction=self.tester.EGRESS) + @skip_if_not_iptables def test_ip_spoofing_works_without_port_security_enabled(self): self.src_port_desc['port_security_enabled'] = False self.firewall.update_port_filter(self.src_port_desc) @@ -437,7 +478,7 @@ class FirewallTestCase(base.BaseSudoTestCase): packets_sent = self.tester.get_sent_icmp_packets(direction) packets_received = self.tester.get_received_icmp_packets(direction) self.assertGreater(packets_sent, 0) - self.assertEqual(0, packets_received) + self.assertEqual(packets_received, 0) def test_remote_security_groups(self): remote_sg_id = 'remote_sg_id' @@ -446,22 +487,20 @@ class FirewallTestCase(base.BaseSudoTestCase): [self.tester.peer_ip_address], self.tester.peer_mac_address, [remote_sg_id]) - self.firewall.prepare_port_filter(peer_port_desc) + vm_sg_members = {'IPv4': [self.tester.peer_ip_address]} peer_sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress', 'protocol': 'icmp'}] - self._apply_security_group_rules(remote_sg_id, peer_sg_rules) + self.firewall.update_security_group_rules(remote_sg_id, peer_sg_rules) + self.firewall.update_security_group_members(remote_sg_id, + vm_sg_members) + self.firewall.prepare_port_filter(peer_port_desc) vm_sg_rules = [{'ethertype': 'IPv4', 'direction': 'ingress', 'protocol': 'icmp', 'remote_group_id': remote_sg_id}] self._apply_security_group_rules(self.FAKE_SECURITY_GROUP_ID, vm_sg_rules) - vm_sg_members = {'IPv4': [self.tester.peer_ip_address]} - with self.firewall.defer_apply(): - self.firewall.update_security_group_members( - remote_sg_id, vm_sg_members) - self.tester.assert_connection(protocol=self.tester.ICMP, direction=self.tester.INGRESS) self.tester.assert_no_connection(protocol=self.tester.TCP, diff --git a/neutron/tests/unit/agent/linux/openvswitch_firewall/__init__.py b/neutron/tests/unit/agent/linux/openvswitch_firewall/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py new file mode 100644 index 00000000000..0de1fdd1a85 --- /dev/null +++ b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_firewall.py @@ -0,0 +1,429 @@ +# 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 +import testtools + +from neutron.agent.common import ovs_lib +from neutron.agent import firewall +from neutron.agent.linux.openvswitch_firewall import firewall as ovsfw +from neutron.common import constants +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts +from neutron.tests import base + + +class TestSecurityGroup(base.BaseTestCase): + def setUp(self): + super(TestSecurityGroup, self).setUp() + self.sg = ovsfw.SecurityGroup('123') + self.sg.members = {'type': [1, 2, 3, 4]} + + def test_update_rules(self): + rules = [ + {'foo': 'bar', 'rule': 'all'}, {'bar': 'foo'}, + {'remote_group_id': '123456', 'foo': 'bar'}] + expected_raw_rules = [{'foo': 'bar', 'rule': 'all'}, {'bar': 'foo'}] + expected_remote_rules = [{'remote_group_id': '123456', 'foo': 'bar'}] + self.sg.update_rules(rules) + + self.assertEqual(expected_raw_rules, self.sg.raw_rules) + self.assertEqual(expected_remote_rules, self.sg.remote_rules) + + def get_ethertype_filtered_addresses(self): + addresses = self.sg.get_ethertype_filtered_addresses('type') + expected_addresses = [1, 2, 3, 4] + self.assertEqual(expected_addresses, addresses) + + def get_ethertype_filtered_addresses_with_excluded_addresses(self): + addresses = self.sg.get_ethertype_filtered_addresses('type', [2, 3]) + expected_addresses = [1, 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': self.ipv4_addresses + self.ipv6_addresses} + self.port = ovsfw.OFPort(port_dict, mock.Mock()) + + 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': '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')} + 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': 'foo', 'ip_address': '192.168.0.1'}, + {'mac_address': 'bar', '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({('foo', '192.168.0.1')}, self.port.allowed_pairs_v4) + self.assertEqual({('bar', '2003::f')}, self.port.allowed_pairs_v6) + + +class TestSGPortMap(base.BaseTestCase): + def setUp(self): + super(TestSGPortMap, self).setUp() + self.map = ovsfw.SGPortMap() + + def test_get_or_create_sg_existing_sg(self): + self.map.sec_groups['id'] = mock.sentinel + sg = self.map.get_or_create_sg('id') + self.assertIs(mock.sentinel, sg) + + def test_get_or_create_sg_nonexisting_sg(self): + with mock.patch.object(ovsfw, 'SecurityGroup') as sg_mock: + sg = self.map.get_or_create_sg('id') + self.assertEqual(sg_mock.return_value, sg) + + def _check_port(self, port_id, expected_sg_ids): + port = self.map.ports[port_id] + expected_sgs = [self.map.sec_groups[sg_id] + for sg_id in expected_sg_ids] + self.assertEqual(port.sec_groups, expected_sgs) + + def _check_sg(self, sg_id, expected_port_ids): + sg = self.map.sec_groups[sg_id] + expected_ports = {self.map.ports[port_id] + for port_id in expected_port_ids} + self.assertEqual(sg.ports, expected_ports) + + def _create_ports_and_sgroups(self): + sg_1 = ovsfw.SecurityGroup(1) + sg_2 = ovsfw.SecurityGroup(2) + sg_3 = ovsfw.SecurityGroup(3) + port_a = ovsfw.OFPort({'device': 'a'}, mock.Mock()) + port_b = ovsfw.OFPort({'device': 'b'}, mock.Mock()) + self.map.ports = {'a': port_a, 'b': port_b} + self.map.sec_groups = {1: sg_1, 2: sg_2, 3: sg_3} + port_a.sec_groups = [sg_1, sg_2] + port_b.sec_groups = [sg_2, sg_3] + sg_1.ports = {port_a} + sg_2.ports = {port_a, port_b} + sg_3.ports = {port_b} + + def test_create_port(self): + port = ovsfw.OFPort({'device': 'a'}, mock.Mock()) + sec_groups = ['1', '2'] + port_dict = {'security_groups': sec_groups} + self.map.create_port(port, port_dict) + self._check_port('a', sec_groups) + self._check_sg('1', ['a']) + self._check_sg('2', ['a']) + + def test_update_port_sg_added(self): + self._create_ports_and_sgroups() + port_dict = {'security_groups': [1, 2, 3]} + self.map.update_port(self.map.ports['b'], port_dict) + self._check_port('a', [1, 2]) + self._check_port('b', [1, 2, 3]) + self._check_sg(1, ['a', 'b']) + self._check_sg(2, ['a', 'b']) + self._check_sg(3, ['b']) + + def test_update_port_sg_removed(self): + self._create_ports_and_sgroups() + port_dict = {'security_groups': [1]} + self.map.update_port(self.map.ports['b'], port_dict) + self._check_port('a', [1, 2]) + self._check_port('b', [1]) + self._check_sg(1, ['a', 'b']) + self._check_sg(2, ['a']) + self._check_sg(3, []) + + def test_remove_port(self): + self._create_ports_and_sgroups() + self.map.remove_port(self.map.ports['a']) + self._check_port('b', [2, 3]) + self._check_sg(1, []) + self._check_sg(2, ['b']) + self._check_sg(3, ['b']) + self.assertNotIn('a', self.map.ports) + + def test_update_rules(self): + """Just make sure it doesn't crash""" + self.map.update_rules(1, []) + + def test_update_members(self): + """Just make sure we doesn't crash""" + self.map.update_members(1, []) + + +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, 'macaddr') + self.mock_bridge.br.get_vif_port_by_id.return_value = \ + self.fake_ovs_port + + def _prepare_security_group(self): + security_group_rules = [ + {'ethertype': constants.IPv4, + 'protocol': constants.PROTO_NAME_TCP, + 'direction': firewall.INGRESS_DIRECTION, + 'port_range_min': 123, + 'port_range_max': 123}] + self.firewall.update_security_group_rules(1, security_group_rules) + security_group_rules = [ + {'ethertype': constants.IPv4, + 'protocol': constants.PROTO_NAME_UDP, + 'direction': firewall.EGRESS_DIRECTION}] + self.firewall.update_security_group_rules(2, security_group_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) + self.mock_bridge.br.add_flow.assert_called_once_with(dl_type="0x0800") + + def test__drop_all_unmatched_flows(self): + self.firewall._drop_all_unmatched_flows() + expected_calls = [ + mock.call(actions='drop', priority=0, + table=ovs_consts.BASE_EGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=ovs_consts.RULES_EGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=ovs_consts.ACCEPT_OR_INGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=ovs_consts.BASE_INGRESS_TABLE), + mock.call(actions='drop', priority=0, + table=ovs_consts.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', + 'security_groups': [123, 456]} + port = self.firewall.get_or_create_ofport(port_dict) + sg1, sg2 = sorted( + self.firewall.sg_port_map.sec_groups.values(), + key=lambda x: x.id) + self.assertIn(port, self.firewall.sg_port_map.ports.values()) + self.assertEqual( + sorted(port.sec_groups, key=lambda x: x.id), [sg1, sg2]) + self.assertIn(port, sg1.ports) + self.assertIn(port, sg2.ports) + + def test_get_or_create_ofport_existing(self): + port_dict = { + 'device': 'port-id', + 'security_groups': [123, 456]} + of_port = ovsfw.OFPort(port_dict, mock.Mock()) + self.firewall.sg_port_map.ports[of_port.id] = of_port + port = self.firewall.get_or_create_ofport(port_dict) + sg1, sg2 = sorted( + self.firewall.sg_port_map.sec_groups.values(), + key=lambda x: x.id) + self.assertIs(of_port, port) + self.assertIn(port, self.firewall.sg_port_map.ports.values()) + self.assertEqual( + sorted(port.sec_groups, key=lambda x: x.id), [sg1, sg2]) + self.assertIn(port, sg1.ports) + self.assertIn(port, sg2.ports) + + def test_get_or_create_ofport_missing(self): + port_dict = { + 'device': 'port-id', + 'security_groups': [123, 456]} + self.mock_bridge.br.get_vif_port_by_id.return_value = None + with testtools.ExpectedException(ovsfw.OVSFWPortNotFound): + self.firewall.get_or_create_ofport(port_dict) + + def test_is_port_managed_managed_port(self): + port_dict = {'device': 'port-id'} + self.firewall.sg_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', + 'security_groups': [1]} + self._prepare_security_group() + self.firewall.prepare_port_filter(port_dict) + exp_ingress_classifier = mock.call( + actions='set_field:{:d}->reg5,resubmit(,{:d})'.format( + self.port_ofport, ovs_consts.BASE_EGRESS_TABLE), + in_port=self.port_ofport, + priority=100, + table=ovs_consts.LOCAL_SWITCHING) + exp_egress_classifier = mock.call( + actions='set_field:{:d}->reg5,resubmit(,{:d})'.format( + self.port_ofport, ovs_consts.BASE_INGRESS_TABLE), + dl_dst=self.port_mac, + priority=90, + table=ovs_consts.LOCAL_SWITCHING) + filter_rule = mock.call( + actions='ct(commit,zone=NXM_NX_REG5[0..15]),output:{:d}'.format( + self.port_ofport), + dl_dst=self.port_mac, + dl_type="0x{:04x}".format(constants.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_TCP, + priority=70, + reg5=self.port_ofport, + table=ovs_consts.RULES_INGRESS_TABLE, + tcp_dst=123) + 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', + 'security_groups': [1], + 'port_security_enabled': False} + self._prepare_security_group() + self.firewall.prepare_port_filter(port_dict) + self.assertFalse(self.mock_bridge.br.add_flow.called) + + def test_prepare_port_filter_initialized_port(self): + port_dict = {'device': 'port-id', + 'security_groups': [1]} + self._prepare_security_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', + 'security_groups': [1]} + self._prepare_security_group() + self.firewall.prepare_port_filter(port_dict) + port_dict['security_groups'] = [2] + self.mock_bridge.reset_mock() + + self.firewall.update_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + add_calls = self.mock_bridge.br.add_flow.call_args_list + filter_rule = mock.call( + actions='resubmit(,{:d})'.format( + ovs_consts.ACCEPT_OR_INGRESS_TABLE), + dl_src=self.port_mac, + dl_type="0x{:04x}".format(constants.ETHERTYPE_IP), + nw_proto=constants.PROTO_NUM_UDP, + priority=70, + reg5=self.port_ofport, + table=ovs_consts.RULES_EGRESS_TABLE) + self.assertIn(filter_rule, add_calls) + + def test_update_port_filter_create_new_port_if_not_present(self): + port_dict = {'device': 'port-id', + 'security_groups': [1]} + self._prepare_security_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', + 'security_groups': [1]} + self._prepare_security_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', + 'security_groups': [1]} + self._prepare_security_group() + self.firewall.prepare_port_filter(port_dict) + self.firewall.remove_port_filter(port_dict) + self.assertTrue(self.mock_bridge.br.delete_flows.called) + + def test_remove_port_filter_port_security_disabled(self): + port_dict = {'device': 'port-id', + 'security_groups': [1]} + self.firewall.remove_port_filter(port_dict) + self.assertFalse(self.mock_bridge.br.delete_flows.called) + + def test_update_security_group_rules(self): + """Just make sure it doesn't crash""" + new_rules = [ + {'ethertype': constants.IPv4, + 'direction': firewall.INGRESS_DIRECTION, + 'protocol': constants.PROTO_NAME_ICMP}, + {'ethertype': constants.IPv4, + 'direction': firewall.EGRESS_DIRECTION, + 'remote_group_id': 2}] + self.firewall.update_security_group_rules(1, new_rules) + + def test_update_security_group_members(self): + """Just make sure it doesn't crash""" + new_members = {constants.IPv4: [1, 2, 3, 4]} + self.firewall.update_security_group_members(2, new_members) diff --git a/neutron/tests/unit/agent/linux/openvswitch_firewall/test_rules.py b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_rules.py new file mode 100644 index 00000000000..73f0d7d5ae7 --- /dev/null +++ b/neutron/tests/unit/agent/linux/openvswitch_firewall/test_rules.py @@ -0,0 +1,254 @@ +# 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.agent import firewall +from neutron.agent.linux.openvswitch_firewall import firewall as ovsfw +from neutron.agent.linux.openvswitch_firewall import rules +from neutron.common import constants +from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \ + as ovs_consts +from neutron.tests import base + + +class TestCreateFlowsFromRuleAndPort(base.BaseTestCase): + def setUp(self): + super(TestCreateFlowsFromRuleAndPort, self).setUp() + ovs_port = mock.Mock() + ovs_port.ofport = 1 + port_dict = {'device': 'port_id'} + self.port = ovsfw.OFPort(port_dict, ovs_port) + + 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(self): + rule = { + 'ethertype': constants.IPv4, + 'direction': firewall.INGRESS_DIRECTION, + } + expected_template = { + 'priority': 70, + 'dl_type': constants.ETHERTYPE_IP, + 'reg5': 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(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': constants.ETHERTYPE_IP, + 'reg5': 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(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': constants.ETHERTYPE_IP, + 'reg5': self.port.ofport, + 'nw_src': '192.168.0.0/24', + } + 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() + ovs_port.ofport = 1 + port_dict = {'device': 'port_id'} + self.port = ovsfw.OFPort(port_dict, ovs_port) + + 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_NAME_TCP} + expected_flows = [{ + 'table': ovs_consts.RULES_INGRESS_TABLE, + 'dl_dst': self.port.mac, + 'actions': 'ct(commit,zone=NXM_NX_REG5[0..15]),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_NAME_TCP} + expected_flows = [{ + 'table': ovs_consts.RULES_EGRESS_TABLE, + 'dl_src': self.port.mac, + 'actions': 'resubmit(,{:d})'.format( + ovs_consts.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': ovs_consts.RULES_EGRESS_TABLE, + 'dl_src': self.port.mac, + 'actions': 'resubmit(,{:d})'.format( + ovs_consts.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_NAME_ICMP} + expected_flows = [{ + 'table': ovs_consts.RULES_EGRESS_TABLE, + 'dl_src': self.port.mac, + 'actions': 'resubmit(,{:d})'.format( + ovs_consts.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_NAME_TCP, + 'port_range_min': 22, + 'port_range_max': 23} + expected_flows = [{ + 'table': ovs_consts.RULES_EGRESS_TABLE, + 'dl_src': self.port.mac, + 'actions': 'resubmit(,{:d})'.format( + ovs_consts.ACCEPT_OR_INGRESS_TABLE), + 'nw_proto': constants.PROTO_NUM_TCP, + 'tcp_dst': port + } for port in range(22, 24)] + 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_NAME_TCP, + 'source_port_range_min': 123, + 'source_port_range_max': 124, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [ + {'tcp_src': 123, 'tcp_dst': 10}, + {'tcp_src': 123, 'tcp_dst': 11}, + {'tcp_src': 124, 'tcp_dst': 10}, + {'tcp_src': 124, 'tcp_dst': 11}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_with_source(self): + rule = { + 'protocol': constants.PROTO_NAME_TCP, + 'source_port_range_min': 123, + 'source_port_range_max': 124, + } + expected_flows = [ + {'tcp_src': 123}, + {'tcp_src': 124}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_with_destination(self): + rule = { + 'protocol': constants.PROTO_NAME_TCP, + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [ + {'tcp_dst': 10}, + {'tcp_dst': 11}, + ] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_flows_without_port_range(self): + rule = { + 'protocol': constants.PROTO_NAME_TCP, + } + expected_flows = [] + self._test_create_port_range_flows_helper(expected_flows, rule) + + def test_create_port_range_with_icmp_protocol(self): + rule = { + 'protocol': 'icmp', + 'port_range_min': 10, + 'port_range_max': 11, + } + expected_flows = [] + self._test_create_port_range_flows_helper(expected_flows, rule) + + +class TestCreateRuleForIpAddress(base.BaseTestCase): + def test_create_rule_for_ip_address(self): + sg_rule = { + 'remote_group_id': 'remote_id', + 'direction': firewall.INGRESS_DIRECTION, + 'some_settings': 'foo', + } + expected_rule = { + 'direction': firewall.INGRESS_DIRECTION, + 'source_ip_prefix': '192.168.0.1/32', + 'some_settings': 'foo', + } + translated_rule = rules.create_rule_for_ip_address( + '192.168.0.1', sg_rule) + self.assertEqual(expected_rule, translated_rule) diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/mech_driver/test_mech_openvswitch.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/mech_driver/test_mech_openvswitch.py index cbef154e4d2..801479a109d 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/mech_driver/test_mech_openvswitch.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/mech_driver/test_mech_openvswitch.py @@ -53,6 +53,8 @@ class OpenvswitchMechanismBaseTestCase(base.AgentMechanismBaseTestCase): def setUp(self): super(OpenvswitchMechanismBaseTestCase, self).setUp() + cfg.CONF.set_override('firewall_driver', 'iptables_hybrid', + 'SECURITYGROUP') self.driver = mech_openvswitch.OpenvswitchMechanismDriver() self.driver.initialize() diff --git a/releasenotes/notes/ovs-ct-firewall-driver-52a70a6a16d06f59.yaml b/releasenotes/notes/ovs-ct-firewall-driver-52a70a6a16d06f59.yaml new file mode 100644 index 00000000000..be949e0c495 --- /dev/null +++ b/releasenotes/notes/ovs-ct-firewall-driver-52a70a6a16d06f59.yaml @@ -0,0 +1,11 @@ +--- +features: + - New security groups firewall driver is introduced. + It's based on OpenFlow using connection tracking. +issues: + - OVS firewall driver doesn't work well with other features + using openflow. +other: + - OVS firewall driver requires OVS 2.5 version or higher + with linux kernel 4.3 or higher. More info at + `OVS github page `_. diff --git a/setup.cfg b/setup.cfg index 922a3e58e27..b3b93487736 100644 --- a/setup.cfg +++ b/setup.cfg @@ -147,6 +147,11 @@ neutron.interface_drivers = linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver null = neutron.agent.linux.interface:NullDriver openvswitch = neutron.agent.linux.interface:OVSInterfaceDriver +neutron.agent.firewall_drivers = + noop = neutron.agent.firewall:NoopFirewallDriver + iptables = neutron.agent.linux.iptables_firewall:IptablesFirewallDriver + iptables_hybrid = neutron.agent.linux.iptables_firewall:OVSHybridIptablesFirewallDriver + openvswitch = neutron.agent.linux.openvswitch_firewall:OVSFirewallDriver [build_sphinx] all_files = 1