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 <mangelajo@redhat.com> Co-Authored-By: Amir Sadoughi <amir.sadoughi@rackspace.com> Change-Id: I13e5cda8b5f3a13a60b14d80e54f198f32d7a529
This commit is contained in:
parent
3dec972fcd
commit
ef29f7eb9a
|
@ -75,6 +75,7 @@ Neutron Internals
|
||||||
i18n
|
i18n
|
||||||
instrumentation
|
instrumentation
|
||||||
address_scopes
|
address_scopes
|
||||||
|
openvswitch_firewall
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -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.
|
|
@ -18,10 +18,24 @@ import contextlib
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from neutron.common import utils
|
||||||
|
from neutron.extensions import portsecurity as psec
|
||||||
|
|
||||||
INGRESS_DIRECTION = 'ingress'
|
INGRESS_DIRECTION = 'ingress'
|
||||||
EGRESS_DIRECTION = 'egress'
|
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)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class FirewallDriver(object):
|
class FirewallDriver(object):
|
||||||
|
@ -57,6 +71,10 @@ class FirewallDriver(object):
|
||||||
remote_group_id will also remaining membership update management
|
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
|
@abc.abstractmethod
|
||||||
def prepare_port_filter(self, port):
|
def prepare_port_filter(self, port):
|
||||||
"""Prepare filters for the port.
|
"""Prepare filters for the port.
|
||||||
|
|
|
@ -32,7 +32,6 @@ from neutron.common import constants
|
||||||
from neutron.common import exceptions as n_exc
|
from neutron.common import exceptions as n_exc
|
||||||
from neutron.common import ipv6_utils
|
from neutron.common import ipv6_utils
|
||||||
from neutron.common import utils as c_utils
|
from neutron.common import utils as c_utils
|
||||||
from neutron.extensions import portsecurity as psec
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -41,8 +40,6 @@ SPOOF_FILTER = 'spoof-filter'
|
||||||
CHAIN_NAME_PREFIX = {firewall.INGRESS_DIRECTION: 'i',
|
CHAIN_NAME_PREFIX = {firewall.INGRESS_DIRECTION: 'i',
|
||||||
firewall.EGRESS_DIRECTION: 'o',
|
firewall.EGRESS_DIRECTION: 'o',
|
||||||
SPOOF_FILTER: 's'}
|
SPOOF_FILTER: 's'}
|
||||||
DIRECTION_IP_PREFIX = {firewall.INGRESS_DIRECTION: 'source_ip_prefix',
|
|
||||||
firewall.EGRESS_DIRECTION: 'dest_ip_prefix'}
|
|
||||||
IPSET_DIRECTION = {firewall.INGRESS_DIRECTION: 'src',
|
IPSET_DIRECTION = {firewall.INGRESS_DIRECTION: 'src',
|
||||||
firewall.EGRESS_DIRECTION: 'dst'}
|
firewall.EGRESS_DIRECTION: 'dst'}
|
||||||
# length of all device prefixes (e.g. qvo, tap, qvb)
|
# 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)
|
LOG.debug("Update members of security group (%s)", sg_id)
|
||||||
self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
|
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):
|
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.unfiltered_ports[port['device']] = port
|
||||||
self.filtered_ports.pop(port['device'], None)
|
self.filtered_ports.pop(port['device'], None)
|
||||||
else:
|
else:
|
||||||
|
@ -466,7 +460,8 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||||
for ip in self.sg_members[remote_group_id][ethertype]:
|
for ip in self.sg_members[remote_group_id][ethertype]:
|
||||||
if ip not in port_ips:
|
if ip not in port_ips:
|
||||||
ip_rule = rule.copy()
|
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_prefix = str(netaddr.IPNetwork(ip).cidr)
|
||||||
ip_rule[direction_ip_prefix] = ip_prefix
|
ip_rule[direction_ip_prefix] = ip_prefix
|
||||||
yield ip_rule
|
yield ip_rule
|
||||||
|
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
|
@ -19,7 +19,6 @@ import functools
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import oslo_messaging
|
import oslo_messaging
|
||||||
from oslo_utils import importutils
|
|
||||||
|
|
||||||
from neutron._i18n import _, _LI, _LW
|
from neutron._i18n import _, _LI, _LW
|
||||||
from neutron.agent import firewall
|
from neutron.agent import firewall
|
||||||
|
@ -87,21 +86,30 @@ class SecurityGroupAgentRpc(object):
|
||||||
"""Enables SecurityGroup agent support in agent implementations."""
|
"""Enables SecurityGroup agent support in agent implementations."""
|
||||||
|
|
||||||
def __init__(self, context, plugin_rpc, local_vlan_map=None,
|
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.context = context
|
||||||
self.plugin_rpc = plugin_rpc
|
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
|
self.local_vlan_map = local_vlan_map
|
||||||
|
|
||||||
def init_firewall(self, defer_refresh_firewall=False):
|
def init_firewall(self, defer_refresh_firewall=False,
|
||||||
firewall_driver = cfg.CONF.SECURITYGROUP.firewall_driver
|
integration_bridge=None):
|
||||||
|
firewall_driver = cfg.CONF.SECURITYGROUP.firewall_driver or 'noop'
|
||||||
LOG.debug("Init firewall settings (driver=%s)", firewall_driver)
|
LOG.debug("Init firewall settings (driver=%s)", firewall_driver)
|
||||||
if not _is_valid_driver_combination():
|
if not _is_valid_driver_combination():
|
||||||
LOG.warn(_LW("Driver configuration doesn't match "
|
LOG.warn(_LW("Driver configuration doesn't match "
|
||||||
"with enable_security_group"))
|
"with enable_security_group"))
|
||||||
if not firewall_driver:
|
firewall_class = firewall.load_firewall_driver_class(firewall_driver)
|
||||||
firewall_driver = 'neutron.agent.firewall.NoopFirewallDriver'
|
try:
|
||||||
self.firewall = importutils.import_object(firewall_driver)
|
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
|
# 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
|
# applied as soon as a rule or membership notification is received
|
||||||
self.defer_refresh_firewall = defer_refresh_firewall
|
self.defer_refresh_firewall = defer_refresh_firewall
|
||||||
|
|
|
@ -332,6 +332,20 @@ def ovsdb_native_supported():
|
||||||
return False
|
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():
|
def ebtables_supported():
|
||||||
try:
|
try:
|
||||||
cmd = ['ebtables', '--version']
|
cmd = ['ebtables', '--version']
|
||||||
|
|
|
@ -193,6 +193,18 @@ def check_ovsdb_native():
|
||||||
return result
|
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():
|
def check_ebtables():
|
||||||
result = checks.ebtables_supported()
|
result = checks.ebtables_supported()
|
||||||
if not result:
|
if not result:
|
||||||
|
@ -242,6 +254,8 @@ OPTS = [
|
||||||
help=_('Check minimal dnsmasq version')),
|
help=_('Check minimal dnsmasq version')),
|
||||||
BoolOptCallback('ovsdb_native', check_ovsdb_native,
|
BoolOptCallback('ovsdb_native', check_ovsdb_native,
|
||||||
help=_('Check ovsdb native interface support')),
|
help=_('Check ovsdb native interface support')),
|
||||||
|
BoolOptCallback('ovs_conntrack', check_ovs_conntrack,
|
||||||
|
help=_('Check ovs conntrack support')),
|
||||||
BoolOptCallback('ebtables_installed', check_ebtables,
|
BoolOptCallback('ebtables_installed', check_ebtables,
|
||||||
help=_('Check ebtables installation')),
|
help=_('Check ebtables installation')),
|
||||||
BoolOptCallback('keepalived_ipv6_support', check_keepalived_ipv6_support,
|
BoolOptCallback('keepalived_ipv6_support', check_keepalived_ipv6_support,
|
||||||
|
|
|
@ -48,6 +48,8 @@ SORT_DIRECTION_ASC = 'asc'
|
||||||
SORT_DIRECTION_DESC = 'desc'
|
SORT_DIRECTION_DESC = 'desc'
|
||||||
|
|
||||||
ETHERTYPE_NAME_ARP = 'arp'
|
ETHERTYPE_NAME_ARP = 'arp'
|
||||||
|
ETHERTYPE_ARP = 0x0806
|
||||||
|
ETHERTYPE_IP = 0x0800
|
||||||
ETHERTYPE_IPV6 = 0x86DD
|
ETHERTYPE_IPV6 = 0x86DD
|
||||||
|
|
||||||
# Protocol names and numbers for Security Groups/Firewalls
|
# 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 Report (131),
|
||||||
# Multicast Listener Done (132),
|
# Multicast Listener Done (132),
|
||||||
# Neighbor Solicitation (135),
|
# Neighbor Solicitation (135),
|
||||||
|
ICMPV6_TYPE_NC = 135
|
||||||
# Neighbor Advertisement (136)
|
# Neighbor Advertisement (136)
|
||||||
|
ICMPV6_TYPE_NA = 136
|
||||||
ICMPV6_ALLOWED_TYPES = [130, 131, 132, 135, 136]
|
ICMPV6_ALLOWED_TYPES = [130, 131, 132, 135, 136]
|
||||||
ICMPV6_TYPE_RA = 134
|
ICMPV6_TYPE_RA = 134
|
||||||
ICMPV6_TYPE_NA = 136
|
|
||||||
|
|
||||||
DHCPV6_STATEFUL = 'dhcpv6-stateful'
|
DHCPV6_STATEFUL = 'dhcpv6-stateful'
|
||||||
DHCPV6_STATELESS = 'dhcpv6-stateless'
|
DHCPV6_STATELESS = 'dhcpv6-stateless'
|
||||||
|
|
|
@ -50,6 +50,21 @@ CANARY_TABLE = 23
|
||||||
# Table for ARP poison/spoofing prevention rules
|
# Table for ARP poison/spoofing prevention rules
|
||||||
ARP_SPOOF_TABLE = 24
|
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)
|
## Tunnel bridge (tun_br)
|
||||||
|
|
||||||
# Various tables for tunneling flows
|
# Various tables for tunneling flows
|
||||||
|
@ -114,3 +129,10 @@ OVS_DPDK_VHOST_USER = 'dpdkvhostuser'
|
||||||
VHOST_USER_SOCKET_DIR = '/var/run/openvswitch'
|
VHOST_USER_SOCKET_DIR = '/var/run/openvswitch'
|
||||||
|
|
||||||
MAX_DEVICE_RETRIES = 5
|
MAX_DEVICE_RETRIES = 5
|
||||||
|
|
||||||
|
# OpenFlow version constants
|
||||||
|
OPENFLOW10 = "OpenFlow10"
|
||||||
|
OPENFLOW11 = "OpenFlow11"
|
||||||
|
OPENFLOW12 = "OpenFlow12"
|
||||||
|
OPENFLOW13 = "OpenFlow13"
|
||||||
|
OPENFLOW14 = "OpenFlow14"
|
||||||
|
|
|
@ -19,6 +19,8 @@ from oslo_utils import excutils
|
||||||
|
|
||||||
from neutron._i18n import _LI
|
from neutron._i18n import _LI
|
||||||
from neutron.agent.common import ovs_lib
|
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 \
|
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \
|
||||||
import ofswitch
|
import ofswitch
|
||||||
|
|
||||||
|
@ -72,7 +74,7 @@ class OVSAgentBridge(ofswitch.OpenFlowSwitchMixin, ovs_lib.OVSBridge):
|
||||||
"port": conf.OVS.of_listen_port,
|
"port": conf.OVS.of_listen_port,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
self.set_protocols("OpenFlow13")
|
self.set_protocols(ovs_consts.OPENFLOW13)
|
||||||
self.set_controller(controllers)
|
self.set_controller(controllers)
|
||||||
|
|
||||||
def drop_port(self, in_port):
|
def drop_port(self, in_port):
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
|
|
||||||
from neutron.agent.common import ovs_lib
|
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 \
|
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl \
|
||||||
import ofswitch
|
import ofswitch
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ class OVSAgentBridge(ofswitch.OpenFlowSwitchMixin, ovs_lib.OVSBridge):
|
||||||
"""Common code for bridges used by OVS agent"""
|
"""Common code for bridges used by OVS agent"""
|
||||||
|
|
||||||
def setup_controllers(self, conf):
|
def setup_controllers(self, conf):
|
||||||
self.set_protocols("[OpenFlow10]")
|
self.set_protocols(ovs_consts.OPENFLOW10)
|
||||||
self.del_controller()
|
self.del_controller()
|
||||||
|
|
||||||
def drop_port(self, in_port):
|
def drop_port(self, in_port):
|
||||||
|
|
|
@ -163,7 +163,6 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||||
# ML2 l2 population mechanism driver.
|
# ML2 l2 population mechanism driver.
|
||||||
self.enable_distributed_routing = agent_conf.enable_distributed_routing
|
self.enable_distributed_routing = agent_conf.enable_distributed_routing
|
||||||
self.arp_responder_enabled = agent_conf.arp_responder and self.l2_pop
|
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
|
host = self.conf.host
|
||||||
self.agent_id = 'ovs-agent-%s' % host
|
self.agent_id = 'ovs-agent-%s' % host
|
||||||
|
@ -277,7 +276,11 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||||
# Security group agent support
|
# Security group agent support
|
||||||
self.sg_agent = sg_rpc.SecurityGroupAgentRpc(self.context,
|
self.sg_agent = sg_rpc.SecurityGroupAgentRpc(self.context,
|
||||||
self.sg_plugin_rpc, self.local_vlan_map,
|
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
|
# Initialize iteration counter
|
||||||
self.iter_num = 0
|
self.iter_num = 0
|
||||||
|
@ -819,7 +822,8 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||||
LOG.debug("Port %s was deleted concurrently, skipping it",
|
LOG.debug("Port %s was deleted concurrently, skipping it",
|
||||||
port.port_name)
|
port.port_name)
|
||||||
continue
|
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)
|
self.int_br.delete_flows(in_port=port.ofport)
|
||||||
if self.prevent_arp_spoofing:
|
if self.prevent_arp_spoofing:
|
||||||
self.setup_arp_spoofing_protection(self.int_br,
|
self.setup_arp_spoofing_protection(self.int_br,
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
from neutron.agent import securitygroups_rpc
|
from neutron.agent import securitygroups_rpc
|
||||||
from neutron.common import constants
|
from neutron.common import constants
|
||||||
from neutron.extensions import portbindings
|
from neutron.extensions import portbindings
|
||||||
|
@ -25,6 +27,9 @@ from neutron.plugins.ml2.drivers.openvswitch.agent.common \
|
||||||
import constants as a_const
|
import constants as a_const
|
||||||
from neutron.services.qos import qos_consts
|
from neutron.services.qos import qos_consts
|
||||||
|
|
||||||
|
IPTABLES_FW_DRIVER_FULL = ("neutron.agent.linux.iptables_firewall."
|
||||||
|
"OVSHybridIptablesFirewallDriver")
|
||||||
|
|
||||||
|
|
||||||
class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
|
class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
|
||||||
"""Attach to networks using openvswitch L2 agent.
|
"""Attach to networks using openvswitch L2 agent.
|
||||||
|
@ -40,8 +45,10 @@ class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
sg_enabled = securitygroups_rpc.is_firewall_enabled()
|
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,
|
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__(
|
super(OpenvswitchMechanismDriver, self).__init__(
|
||||||
constants.AGENT_TYPE_OVS,
|
constants.AGENT_TYPE_OVS,
|
||||||
portbindings.VIF_TYPE_OVS,
|
portbindings.VIF_TYPE_OVS,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import functools
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from neutron.agent import firewall
|
from neutron.agent import firewall
|
||||||
from neutron.agent.linux import ip_lib
|
from neutron.agent.linux import ip_lib
|
||||||
|
@ -64,8 +65,8 @@ class ConnectionTester(fixtures.Fixture):
|
||||||
self.TCP: self._test_transport_connectivity,
|
self.TCP: self._test_transport_connectivity,
|
||||||
self.ICMP: self._test_icmp_connectivity,
|
self.ICMP: self._test_icmp_connectivity,
|
||||||
self.ARP: self._test_arp_connectivity}
|
self.ARP: self._test_arp_connectivity}
|
||||||
self._nc_testers = dict()
|
self._nc_testers = {}
|
||||||
self._pingers = dict()
|
self._pingers = {}
|
||||||
self.addCleanup(self.cleanup)
|
self.addCleanup(self.cleanup)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
|
@ -288,6 +289,53 @@ class ConnectionTester(fixtures.Fixture):
|
||||||
return pinger.received
|
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):
|
class LinuxBridgeConnectionTester(ConnectionTester):
|
||||||
"""Tester with linux bridge in the middle
|
"""Tester with linux bridge in the middle
|
||||||
|
|
||||||
|
@ -298,13 +346,13 @@ class LinuxBridgeConnectionTester(ConnectionTester):
|
||||||
|
|
||||||
def _setUp(self):
|
def _setUp(self):
|
||||||
super(LinuxBridgeConnectionTester, self)._setUp()
|
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(
|
self._peer, self._vm = self.useFixture(
|
||||||
machine_fixtures.PeerMachines(self._bridge)).machines
|
machine_fixtures.PeerMachines(self.bridge)).machines
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bridge_namespace(self):
|
def bridge_namespace(self):
|
||||||
return self._bridge.namespace
|
return self.bridge.namespace
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vm_port_id(self):
|
def vm_port_id(self):
|
||||||
|
@ -315,7 +363,7 @@ class LinuxBridgeConnectionTester(ConnectionTester):
|
||||||
return net_helpers.VethFixture.get_peer_name(self._peer.port.name)
|
return net_helpers.VethFixture.get_peer_name(self._peer.port.name)
|
||||||
|
|
||||||
def flush_arp_tables(self):
|
def flush_arp_tables(self):
|
||||||
self._bridge.neigh.flush(4, 'all')
|
self.bridge.neigh.flush(4, 'all')
|
||||||
super(LinuxBridgeConnectionTester, self).flush_arp_tables()
|
super(LinuxBridgeConnectionTester, self).flush_arp_tables()
|
||||||
|
|
||||||
def collect_debug_info(self, exc_info):
|
def collect_debug_info(self, exc_info):
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import functools
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
@ -25,7 +26,9 @@ import testscenarios
|
||||||
|
|
||||||
from neutron.agent import firewall
|
from neutron.agent import firewall
|
||||||
from neutron.agent.linux import iptables_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.agent import securitygroups_rpc as sg_cfg
|
||||||
|
from neutron.cmd.sanity import checks
|
||||||
from neutron.common import constants
|
from neutron.common import constants
|
||||||
from neutron.tests.common import conn_testers
|
from neutron.tests.common import conn_testers
|
||||||
from neutron.tests.functional import base
|
from neutron.tests.functional import base
|
||||||
|
@ -47,6 +50,15 @@ reverse_transport_protocol = {
|
||||||
DEVICE_OWNER_COMPUTE = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'fake'
|
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):
|
def _add_rule(sg_rules, base, port_range_min=None, port_range_max=None):
|
||||||
rule = copy.copy(base)
|
rule = copy.copy(base)
|
||||||
if port_range_min:
|
if port_range_min:
|
||||||
|
@ -60,15 +72,34 @@ class FirewallTestCase(base.BaseSudoTestCase):
|
||||||
FAKE_SECURITY_GROUP_ID = 'fake_sg_id'
|
FAKE_SECURITY_GROUP_ID = 'fake_sg_id'
|
||||||
MAC_SPOOFED = "fa:16:3e:9a:2f:48"
|
MAC_SPOOFED = "fa:16:3e:9a:2f:48"
|
||||||
scenarios = [('IptablesFirewallDriver without ipset',
|
scenarios = [('IptablesFirewallDriver without ipset',
|
||||||
{'enable_ipset': False}),
|
{'enable_ipset': False,
|
||||||
|
'initialize': 'initialize_iptables'}),
|
||||||
('IptablesFirewallDriver with ipset',
|
('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,
|
cfg.CONF.set_override('enable_ipset', self.enable_ipset,
|
||||||
'SECURITYGROUP')
|
'SECURITYGROUP')
|
||||||
return iptables_firewall.IptablesFirewallDriver(
|
tester = self.useFixture(conn_testers.LinuxBridgeConnectionTester())
|
||||||
namespace=self.tester.bridge_namespace)
|
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
|
@staticmethod
|
||||||
def _create_port_description(port_id, ip_addresses, mac_address, sg_ids):
|
def _create_port_description(port_id, ip_addresses, mac_address, sg_ids):
|
||||||
|
@ -84,21 +115,28 @@ class FirewallTestCase(base.BaseSudoTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP')
|
cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP')
|
||||||
super(FirewallTestCase, self).setUp()
|
super(FirewallTestCase, self).setUp()
|
||||||
self.tester = self.useFixture(
|
self.tester, self.firewall = getattr(self, self.initialize)()
|
||||||
conn_testers.LinuxBridgeConnectionTester())
|
|
||||||
self.addOnException(self.tester.collect_debug_info)
|
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(
|
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])
|
[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)
|
self.firewall.prepare_port_filter(self.src_port_desc)
|
||||||
|
|
||||||
def _apply_security_group_rules(self, sg_id, sg_rules):
|
def _apply_security_group_rules(self, sg_id, sg_rules):
|
||||||
with self.firewall.defer_apply():
|
with self.firewall.defer_apply():
|
||||||
self.firewall.update_security_group_rules(sg_id, sg_rules)
|
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):
|
def test_rule_application_converges(self):
|
||||||
sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress'},
|
sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress'},
|
||||||
{'ethertype': 'IPv6', 'direction': 'egress'},
|
{'ethertype': 'IPv6', 'direction': 'egress'},
|
||||||
|
@ -163,6 +201,7 @@ class FirewallTestCase(base.BaseSudoTestCase):
|
||||||
# and the new one was inserted in the correct position
|
# and the new one was inserted in the correct position
|
||||||
self.assertEqual([], self.firewall.iptables._apply())
|
self.assertEqual([], self.firewall.iptables._apply())
|
||||||
|
|
||||||
|
@skip_if_not_iptables
|
||||||
def test_rule_ordering_correct(self):
|
def test_rule_ordering_correct(self):
|
||||||
sg_rules = [
|
sg_rules = [
|
||||||
{'ethertype': 'IPv4', 'direction': 'egress', 'protocol': 'tcp',
|
{'ethertype': 'IPv4', 'direction': 'egress', 'protocol': 'tcp',
|
||||||
|
@ -235,6 +274,7 @@ class FirewallTestCase(base.BaseSudoTestCase):
|
||||||
self.tester.assert_no_connection(protocol=self.tester.ICMP,
|
self.tester.assert_no_connection(protocol=self.tester.ICMP,
|
||||||
direction=self.tester.EGRESS)
|
direction=self.tester.EGRESS)
|
||||||
|
|
||||||
|
@skip_if_not_iptables
|
||||||
def test_mac_spoofing_works_without_port_security_enabled(self):
|
def test_mac_spoofing_works_without_port_security_enabled(self):
|
||||||
self.src_port_desc['port_security_enabled'] = False
|
self.src_port_desc['port_security_enabled'] = False
|
||||||
self.firewall.update_port_filter(self.src_port_desc)
|
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,
|
self.tester.assert_no_connection(protocol=self.tester.ICMP,
|
||||||
direction=self.tester.EGRESS)
|
direction=self.tester.EGRESS)
|
||||||
|
|
||||||
|
@skip_if_not_iptables
|
||||||
def test_ip_spoofing_works_without_port_security_enabled(self):
|
def test_ip_spoofing_works_without_port_security_enabled(self):
|
||||||
self.src_port_desc['port_security_enabled'] = False
|
self.src_port_desc['port_security_enabled'] = False
|
||||||
self.firewall.update_port_filter(self.src_port_desc)
|
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_sent = self.tester.get_sent_icmp_packets(direction)
|
||||||
packets_received = self.tester.get_received_icmp_packets(direction)
|
packets_received = self.tester.get_received_icmp_packets(direction)
|
||||||
self.assertGreater(packets_sent, 0)
|
self.assertGreater(packets_sent, 0)
|
||||||
self.assertEqual(0, packets_received)
|
self.assertEqual(packets_received, 0)
|
||||||
|
|
||||||
def test_remote_security_groups(self):
|
def test_remote_security_groups(self):
|
||||||
remote_sg_id = 'remote_sg_id'
|
remote_sg_id = 'remote_sg_id'
|
||||||
|
@ -446,22 +487,20 @@ class FirewallTestCase(base.BaseSudoTestCase):
|
||||||
[self.tester.peer_ip_address],
|
[self.tester.peer_ip_address],
|
||||||
self.tester.peer_mac_address,
|
self.tester.peer_mac_address,
|
||||||
[remote_sg_id])
|
[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',
|
peer_sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress',
|
||||||
'protocol': 'icmp'}]
|
'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',
|
vm_sg_rules = [{'ethertype': 'IPv4', 'direction': 'ingress',
|
||||||
'protocol': 'icmp', 'remote_group_id': remote_sg_id}]
|
'protocol': 'icmp', 'remote_group_id': remote_sg_id}]
|
||||||
self._apply_security_group_rules(self.FAKE_SECURITY_GROUP_ID,
|
self._apply_security_group_rules(self.FAKE_SECURITY_GROUP_ID,
|
||||||
vm_sg_rules)
|
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,
|
self.tester.assert_connection(protocol=self.tester.ICMP,
|
||||||
direction=self.tester.INGRESS)
|
direction=self.tester.INGRESS)
|
||||||
self.tester.assert_no_connection(protocol=self.tester.TCP,
|
self.tester.assert_no_connection(protocol=self.tester.TCP,
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -53,6 +53,8 @@ class OpenvswitchMechanismBaseTestCase(base.AgentMechanismBaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(OpenvswitchMechanismBaseTestCase, self).setUp()
|
super(OpenvswitchMechanismBaseTestCase, self).setUp()
|
||||||
|
cfg.CONF.set_override('firewall_driver', 'iptables_hybrid',
|
||||||
|
'SECURITYGROUP')
|
||||||
self.driver = mech_openvswitch.OpenvswitchMechanismDriver()
|
self.driver = mech_openvswitch.OpenvswitchMechanismDriver()
|
||||||
self.driver.initialize()
|
self.driver.initialize()
|
||||||
|
|
||||||
|
|
|
@ -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 <https://github.com/openvswitch/ovs/blob/master/FAQ.md>`_.
|
|
@ -147,6 +147,11 @@ neutron.interface_drivers =
|
||||||
linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver
|
linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver
|
||||||
null = neutron.agent.linux.interface:NullDriver
|
null = neutron.agent.linux.interface:NullDriver
|
||||||
openvswitch = neutron.agent.linux.interface:OVSInterfaceDriver
|
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]
|
[build_sphinx]
|
||||||
all_files = 1
|
all_files = 1
|
||||||
|
|
Loading…
Reference in New Issue