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:
Jakub Libosvar 2015-09-01 15:50:48 +00:00
parent 3dec972fcd
commit ef29f7eb9a
25 changed files with 1820 additions and 48 deletions

View File

@ -75,6 +75,7 @@ Neutron Internals
i18n
instrumentation
address_scopes
openvswitch_firewall
Testing
-------

View File

@ -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.

View File

@ -18,10 +18,24 @@ import contextlib
import six
from neutron.common import utils
from neutron.extensions import portsecurity as psec
INGRESS_DIRECTION = 'ingress'
EGRESS_DIRECTION = 'egress'
DIRECTION_IP_PREFIX = {INGRESS_DIRECTION: 'source_ip_prefix',
EGRESS_DIRECTION: 'dest_ip_prefix'}
def port_sec_enabled(port):
return port.get(psec.PORTSECURITY, True)
def load_firewall_driver_class(driver):
return utils.load_class_by_alias_or_classname(
'neutron.agent.firewall_drivers', driver)
@six.add_metaclass(abc.ABCMeta)
class FirewallDriver(object):
@ -57,6 +71,10 @@ class FirewallDriver(object):
remote_group_id will also remaining membership update management
"""
# OVS agent installs arp spoofing openflow rules. If firewall is capable
# of handling that, ovs agent doesn't need to install the protection.
provides_arp_spoofing_protection = False
@abc.abstractmethod
def prepare_port_filter(self, port):
"""Prepare filters for the port.

View File

@ -32,7 +32,6 @@ from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron.common import ipv6_utils
from neutron.common import utils as c_utils
from neutron.extensions import portsecurity as psec
LOG = logging.getLogger(__name__)
@ -41,8 +40,6 @@ SPOOF_FILTER = 'spoof-filter'
CHAIN_NAME_PREFIX = {firewall.INGRESS_DIRECTION: 'i',
firewall.EGRESS_DIRECTION: 'o',
SPOOF_FILTER: 's'}
DIRECTION_IP_PREFIX = {firewall.INGRESS_DIRECTION: 'source_ip_prefix',
firewall.EGRESS_DIRECTION: 'dest_ip_prefix'}
IPSET_DIRECTION = {firewall.INGRESS_DIRECTION: 'src',
firewall.EGRESS_DIRECTION: 'dst'}
# length of all device prefixes (e.g. qvo, tap, qvb)
@ -146,11 +143,8 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
LOG.debug("Update members of security group (%s)", sg_id)
self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
def _ps_enabled(self, port):
return port.get(psec.PORTSECURITY, True)
def _set_ports(self, port):
if not self._ps_enabled(port):
if not firewall.port_sec_enabled(port):
self.unfiltered_ports[port['device']] = port
self.filtered_ports.pop(port['device'], None)
else:
@ -466,7 +460,8 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
for ip in self.sg_members[remote_group_id][ethertype]:
if ip not in port_ips:
ip_rule = rule.copy()
direction_ip_prefix = DIRECTION_IP_PREFIX[direction]
direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[
direction]
ip_prefix = str(netaddr.IPNetwork(ip).cidr)
ip_rule[direction_ip_prefix] = ip_prefix
yield ip_rule

View File

@ -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

View File

@ -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,
}

View File

@ -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)

View File

@ -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

View File

@ -19,7 +19,6 @@ import functools
from oslo_config import cfg
from oslo_log import log as logging
import oslo_messaging
from oslo_utils import importutils
from neutron._i18n import _, _LI, _LW
from neutron.agent import firewall
@ -87,21 +86,30 @@ class SecurityGroupAgentRpc(object):
"""Enables SecurityGroup agent support in agent implementations."""
def __init__(self, context, plugin_rpc, local_vlan_map=None,
defer_refresh_firewall=False,):
defer_refresh_firewall=False, integration_bridge=None):
self.context = context
self.plugin_rpc = plugin_rpc
self.init_firewall(defer_refresh_firewall)
self.init_firewall(defer_refresh_firewall, integration_bridge)
self.local_vlan_map = local_vlan_map
def init_firewall(self, defer_refresh_firewall=False):
firewall_driver = cfg.CONF.SECURITYGROUP.firewall_driver
def init_firewall(self, defer_refresh_firewall=False,
integration_bridge=None):
firewall_driver = cfg.CONF.SECURITYGROUP.firewall_driver or 'noop'
LOG.debug("Init firewall settings (driver=%s)", firewall_driver)
if not _is_valid_driver_combination():
LOG.warn(_LW("Driver configuration doesn't match "
"with enable_security_group"))
if not firewall_driver:
firewall_driver = 'neutron.agent.firewall.NoopFirewallDriver'
self.firewall = importutils.import_object(firewall_driver)
firewall_class = firewall.load_firewall_driver_class(firewall_driver)
try:
self.firewall = firewall_class(
integration_bridge=integration_bridge)
except TypeError as e:
LOG.warning(_LW("Firewall driver {fw_driver} doesn't accept "
"integration_bridge parameter in __init__(): "
"{err}"),
fw_driver=firewall_driver,
err=e)
self.firewall = firewall_class()
# The following flag will be set to true if port filter must not be
# applied as soon as a rule or membership notification is received
self.defer_refresh_firewall = defer_refresh_firewall

View File

@ -332,6 +332,20 @@ def ovsdb_native_supported():
return False
def ovs_conntrack_supported():
random_str = utils.get_random_string(6)
br_name = "ovs-test-" + random_str
with ovs_lib.OVSBridge(br_name) as br:
try:
br.set_protocols(
"OpenFlow10,OpenFlow11,OpenFlow12,OpenFlow13,OpenFlow14")
except RuntimeError as e:
LOG.debug("Exception while checking ovs conntrack support: %s", e)
return False
return ofctl_arg_supported(cmd='add-flow', ct_state='+trk', actions='drop')
def ebtables_supported():
try:
cmd = ['ebtables', '--version']

View File

@ -193,6 +193,18 @@ def check_ovsdb_native():
return result
def check_ovs_conntrack():
result = checks.ovs_conntrack_supported()
if not result:
LOG.error(_LE('Check for Open vSwitch support of conntrack support '
'failed. OVS/CT firewall will not work. A newer '
'version of OVS (2.5+) and linux kernel (4.3+) are '
'required. See '
'https://github.com/openvswitch/ovs/blob/master/FAQ.md'
'for more information.'))
return result
def check_ebtables():
result = checks.ebtables_supported()
if not result:
@ -242,6 +254,8 @@ OPTS = [
help=_('Check minimal dnsmasq version')),
BoolOptCallback('ovsdb_native', check_ovsdb_native,
help=_('Check ovsdb native interface support')),
BoolOptCallback('ovs_conntrack', check_ovs_conntrack,
help=_('Check ovs conntrack support')),
BoolOptCallback('ebtables_installed', check_ebtables,
help=_('Check ebtables installation')),
BoolOptCallback('keepalived_ipv6_support', check_keepalived_ipv6_support,

View File

@ -48,6 +48,8 @@ SORT_DIRECTION_ASC = 'asc'
SORT_DIRECTION_DESC = 'desc'
ETHERTYPE_NAME_ARP = 'arp'
ETHERTYPE_ARP = 0x0806
ETHERTYPE_IP = 0x0800
ETHERTYPE_IPV6 = 0x86DD
# Protocol names and numbers for Security Groups/Firewalls
@ -122,10 +124,11 @@ IP_PROTOCOL_MAP = {PROTO_NAME_AH: PROTO_NUM_AH,
# Multicast Listener Report (131),
# Multicast Listener Done (132),
# Neighbor Solicitation (135),
ICMPV6_TYPE_NC = 135
# Neighbor Advertisement (136)
ICMPV6_TYPE_NA = 136
ICMPV6_ALLOWED_TYPES = [130, 131, 132, 135, 136]
ICMPV6_TYPE_RA = 134
ICMPV6_TYPE_NA = 136
DHCPV6_STATEFUL = 'dhcpv6-stateful'
DHCPV6_STATELESS = 'dhcpv6-stateless'

View File

@ -50,6 +50,21 @@ CANARY_TABLE = 23
# Table for ARP poison/spoofing prevention rules
ARP_SPOOF_TABLE = 24
# Tables used for ovs firewall
BASE_EGRESS_TABLE = 71
RULES_EGRESS_TABLE = 72
ACCEPT_OR_INGRESS_TABLE = 73
BASE_INGRESS_TABLE = 81
RULES_INGRESS_TABLE = 82
OVS_FIREWALL_TABLES = (
BASE_EGRESS_TABLE,
RULES_EGRESS_TABLE,
ACCEPT_OR_INGRESS_TABLE,
BASE_INGRESS_TABLE,
RULES_INGRESS_TABLE,
)
## Tunnel bridge (tun_br)
# Various tables for tunneling flows
@ -114,3 +129,10 @@ OVS_DPDK_VHOST_USER = 'dpdkvhostuser'
VHOST_USER_SOCKET_DIR = '/var/run/openvswitch'
MAX_DEVICE_RETRIES = 5
# OpenFlow version constants
OPENFLOW10 = "OpenFlow10"
OPENFLOW11 = "OpenFlow11"
OPENFLOW12 = "OpenFlow12"
OPENFLOW13 = "OpenFlow13"
OPENFLOW14 = "OpenFlow14"

View File

@ -19,6 +19,8 @@ from oslo_utils import excutils
from neutron._i18n import _LI
from neutron.agent.common import ovs_lib
from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \
as ovs_consts
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \
import ofswitch
@ -72,7 +74,7 @@ class OVSAgentBridge(ofswitch.OpenFlowSwitchMixin, ovs_lib.OVSBridge):
"port": conf.OVS.of_listen_port,
}
]
self.set_protocols("OpenFlow13")
self.set_protocols(ovs_consts.OPENFLOW13)
self.set_controller(controllers)
def drop_port(self, in_port):

View File

@ -16,6 +16,8 @@
from neutron.agent.common import ovs_lib
from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \
as ovs_consts
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.ovs_ofctl \
import ofswitch
@ -24,7 +26,7 @@ class OVSAgentBridge(ofswitch.OpenFlowSwitchMixin, ovs_lib.OVSBridge):
"""Common code for bridges used by OVS agent"""
def setup_controllers(self, conf):
self.set_protocols("[OpenFlow10]")
self.set_protocols(ovs_consts.OPENFLOW10)
self.del_controller()
def drop_port(self, in_port):

View File

@ -163,7 +163,6 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
# ML2 l2 population mechanism driver.
self.enable_distributed_routing = agent_conf.enable_distributed_routing
self.arp_responder_enabled = agent_conf.arp_responder and self.l2_pop
self.prevent_arp_spoofing = agent_conf.prevent_arp_spoofing
host = self.conf.host
self.agent_id = 'ovs-agent-%s' % host
@ -277,7 +276,11 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
# Security group agent support
self.sg_agent = sg_rpc.SecurityGroupAgentRpc(self.context,
self.sg_plugin_rpc, self.local_vlan_map,
defer_refresh_firewall=True)
defer_refresh_firewall=True, integration_bridge=self.int_br)
self.prevent_arp_spoofing = (
agent_conf.prevent_arp_spoofing and
not self.sg_agent.firewall.provides_arp_spoofing_protection)
# Initialize iteration counter
self.iter_num = 0
@ -819,7 +822,8 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
LOG.debug("Port %s was deleted concurrently, skipping it",
port.port_name)
continue
if cur_tag != lvm.vlan:
# Uninitialized port has tag set to []
if cur_tag and cur_tag != lvm.vlan:
self.int_br.delete_flows(in_port=port.ofport)
if self.prevent_arp_spoofing:
self.setup_arp_spoofing_protection(self.int_br,

View File

@ -15,6 +15,8 @@
import os
from oslo_config import cfg
from neutron.agent import securitygroups_rpc
from neutron.common import constants
from neutron.extensions import portbindings
@ -25,6 +27,9 @@ from neutron.plugins.ml2.drivers.openvswitch.agent.common \
import constants as a_const
from neutron.services.qos import qos_consts
IPTABLES_FW_DRIVER_FULL = ("neutron.agent.linux.iptables_firewall."
"OVSHybridIptablesFirewallDriver")
class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
"""Attach to networks using openvswitch L2 agent.
@ -40,8 +45,10 @@ class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
def __init__(self):
sg_enabled = securitygroups_rpc.is_firewall_enabled()
hybrid_plug_required = (cfg.CONF.SECURITYGROUP.firewall_driver in (
IPTABLES_FW_DRIVER_FULL, 'iptables_hybrid')) and sg_enabled
vif_details = {portbindings.CAP_PORT_FILTER: sg_enabled,
portbindings.OVS_HYBRID_PLUG: sg_enabled}
portbindings.OVS_HYBRID_PLUG: hybrid_plug_required}
super(OpenvswitchMechanismDriver, self).__init__(
constants.AGENT_TYPE_OVS,
portbindings.VIF_TYPE_OVS,

View File

@ -16,6 +16,7 @@ import functools
import fixtures
from oslo_log import log as logging
from oslo_utils import uuidutils
from neutron.agent import firewall
from neutron.agent.linux import ip_lib
@ -64,8 +65,8 @@ class ConnectionTester(fixtures.Fixture):
self.TCP: self._test_transport_connectivity,
self.ICMP: self._test_icmp_connectivity,
self.ARP: self._test_arp_connectivity}
self._nc_testers = dict()
self._pingers = dict()
self._nc_testers = {}
self._pingers = {}
self.addCleanup(self.cleanup)
def cleanup(self):
@ -288,6 +289,53 @@ class ConnectionTester(fixtures.Fixture):
return pinger.received
class OVSConnectionTester(ConnectionTester):
"""Tester with OVS bridge in the middle
The endpoints are created as OVS ports attached to the OVS bridge.
NOTE: The OVS ports are connected from the namespace. This connection is
currently not supported in OVS and may lead to unpredicted behavior:
https://bugzilla.redhat.com/show_bug.cgi?id=1160340
"""
def setUp(self):
super(OVSConnectionTester, self).setUp()
self.bridge = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
self._peer, self._vm = self.useFixture(
machine_fixtures.PeerMachines(self.bridge)).machines
self._set_port_attrs(self._peer.port)
self._set_port_attrs(self._vm.port)
def _set_port_attrs(self, port):
port.id = uuidutils.generate_uuid()
attrs = [('type', 'internal'),
('external_ids', {
'iface-id': port.id,
'iface-status': 'active',
'attached-mac': port.link.address})]
for column, value in attrs:
self.bridge.set_db_attribute('Interface', port.name, column, value)
@property
def peer_port_id(self):
return self._peer.port.id
@property
def vm_port_id(self):
return self._vm.port.id
def set_tag(self, port_name, tag):
self.bridge.set_db_attribute('Port', port_name, 'tag', tag)
def set_vm_tag(self, tag):
self.set_tag(self._vm.port.name, tag)
def set_peer_tag(self, tag):
self.set_tag(self._peer.port.name, tag)
class LinuxBridgeConnectionTester(ConnectionTester):
"""Tester with linux bridge in the middle
@ -298,13 +346,13 @@ class LinuxBridgeConnectionTester(ConnectionTester):
def _setUp(self):
super(LinuxBridgeConnectionTester, self)._setUp()
self._bridge = self.useFixture(net_helpers.LinuxBridgeFixture()).bridge
self.bridge = self.useFixture(net_helpers.LinuxBridgeFixture()).bridge
self._peer, self._vm = self.useFixture(
machine_fixtures.PeerMachines(self._bridge)).machines
machine_fixtures.PeerMachines(self.bridge)).machines
@property
def bridge_namespace(self):
return self._bridge.namespace
return self.bridge.namespace
@property
def vm_port_id(self):
@ -315,7 +363,7 @@ class LinuxBridgeConnectionTester(ConnectionTester):
return net_helpers.VethFixture.get_peer_name(self._peer.port.name)
def flush_arp_tables(self):
self._bridge.neigh.flush(4, 'all')
self.bridge.neigh.flush(4, 'all')
super(LinuxBridgeConnectionTester, self).flush_arp_tables()
def collect_debug_info(self, exc_info):

View File

@ -18,6 +18,7 @@
# under the License.
import copy
import functools
import netaddr
from oslo_config import cfg
@ -25,7 +26,9 @@ import testscenarios
from neutron.agent import firewall
from neutron.agent.linux import iptables_firewall
from neutron.agent.linux import openvswitch_firewall
from neutron.agent import securitygroups_rpc as sg_cfg
from neutron.cmd.sanity import checks
from neutron.common import constants
from neutron.tests.common import conn_testers
from neutron.tests.functional import base
@ -47,6 +50,15 @@ reverse_transport_protocol = {
DEVICE_OWNER_COMPUTE = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'fake'
def skip_if_not_iptables(f):
@functools.wraps(f)
def wrap(self, *args, **kwargs):
if not hasattr(self, 'enable_ipset'):
self.skipTest("This test doesn't use iptables")
return f(self, *args, **kwargs)
return wrap
def _add_rule(sg_rules, base, port_range_min=None, port_range_max=None):
rule = copy.copy(base)
if port_range_min:
@ -60,15 +72,34 @@ class FirewallTestCase(base.BaseSudoTestCase):
FAKE_SECURITY_GROUP_ID = 'fake_sg_id'
MAC_SPOOFED = "fa:16:3e:9a:2f:48"
scenarios = [('IptablesFirewallDriver without ipset',
{'enable_ipset': False}),
{'enable_ipset': False,
'initialize': 'initialize_iptables'}),
('IptablesFirewallDriver with ipset',
{'enable_ipset': True})]
{'enable_ipset': True,
'initialize': 'initialize_iptables'}),
('OVS Firewall Driver',
{'initialize': 'initialize_ovs'})]
def create_iptables_firewall(self):
def initialize_iptables(self):
cfg.CONF.set_override('enable_ipset', self.enable_ipset,
'SECURITYGROUP')
return iptables_firewall.IptablesFirewallDriver(
namespace=self.tester.bridge_namespace)
tester = self.useFixture(conn_testers.LinuxBridgeConnectionTester())
firewall_drv = iptables_firewall.IptablesFirewallDriver(
namespace=tester.bridge_namespace)
return tester, firewall_drv
def initialize_ovs(self):
# Tests for ovs requires kernel >= 4.3 and OVS >= 2.5
if not checks.ovs_conntrack_supported():
self.skipTest("Open vSwitch with conntrack is not installed "
"on this machine. To run tests for OVS/CT firewall,"
" please meet the requirements (kernel>=4.3, "
"OVS>=2.5. More info at"
"https://github.com/openvswitch/ovs/blob/master/"
"FAQ.md")
tester = self.useFixture(conn_testers.OVSConnectionTester())
firewall_drv = openvswitch_firewall.OVSFirewallDriver(tester.bridge)
return tester, firewall_drv
@staticmethod
def _create_port_description(port_id, ip_addresses, mac_address, sg_ids):
@ -84,21 +115,28 @@ class FirewallTestCase(base.BaseSudoTestCase):
def setUp(self):
cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP')
super(FirewallTestCase, self).setUp()
self.tester = self.useFixture(
conn_testers.LinuxBridgeConnectionTester())
self.tester, self.firewall = getattr(self, self.initialize)()
self.addOnException(self.tester.collect_debug_info)
self.firewall = self.create_iptables_firewall()
vm_mac = self.tester.vm_mac_address
vm_port_id = self.tester.vm_port_id
self.src_port_desc = self._create_port_description(
vm_port_id, [self.tester.vm_ip_address], vm_mac,
self.tester.vm_port_id,
[self.tester.vm_ip_address],
self.tester.vm_mac_address,
[self.FAKE_SECURITY_GROUP_ID])
# FIXME(jlibosva): We should consider to call prepare_port_filter with
# deferred bridge depending on its performance
self.firewall.prepare_port_filter(self.src_port_desc)
def _apply_security_group_rules(self, sg_id, sg_rules):
with self.firewall.defer_apply():
self.firewall.update_security_group_rules(sg_id, sg_rules)
self.firewall.update_port_filter(self.src_port_desc)
def _apply_security_group_members(self, sg_id, members):
with self.firewall.defer_apply():
self.firewall.update_security_group_members(sg_id, members)
self.firewall.update_port_filter(self.src_port_desc)
@skip_if_not_iptables
def test_rule_application_converges(self):
sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress'},
{'ethertype': 'IPv6', 'direction': 'egress'},
@ -163,6 +201,7 @@ class FirewallTestCase(base.BaseSudoTestCase):
# and the new one was inserted in the correct position
self.assertEqual([], self.firewall.iptables._apply())
@skip_if_not_iptables
def test_rule_ordering_correct(self):
sg_rules = [
{'ethertype': 'IPv4', 'direction': 'egress', 'protocol': 'tcp',
@ -235,6 +274,7 @@ class FirewallTestCase(base.BaseSudoTestCase):
self.tester.assert_no_connection(protocol=self.tester.ICMP,
direction=self.tester.EGRESS)
@skip_if_not_iptables
def test_mac_spoofing_works_without_port_security_enabled(self):
self.src_port_desc['port_security_enabled'] = False
self.firewall.update_port_filter(self.src_port_desc)
@ -286,6 +326,7 @@ class FirewallTestCase(base.BaseSudoTestCase):
self.tester.assert_no_connection(protocol=self.tester.ICMP,
direction=self.tester.EGRESS)
@skip_if_not_iptables
def test_ip_spoofing_works_without_port_security_enabled(self):
self.src_port_desc['port_security_enabled'] = False
self.firewall.update_port_filter(self.src_port_desc)
@ -437,7 +478,7 @@ class FirewallTestCase(base.BaseSudoTestCase):
packets_sent = self.tester.get_sent_icmp_packets(direction)
packets_received = self.tester.get_received_icmp_packets(direction)
self.assertGreater(packets_sent, 0)
self.assertEqual(0, packets_received)
self.assertEqual(packets_received, 0)
def test_remote_security_groups(self):
remote_sg_id = 'remote_sg_id'
@ -446,22 +487,20 @@ class FirewallTestCase(base.BaseSudoTestCase):
[self.tester.peer_ip_address],
self.tester.peer_mac_address,
[remote_sg_id])
self.firewall.prepare_port_filter(peer_port_desc)
vm_sg_members = {'IPv4': [self.tester.peer_ip_address]}
peer_sg_rules = [{'ethertype': 'IPv4', 'direction': 'egress',
'protocol': 'icmp'}]
self._apply_security_group_rules(remote_sg_id, peer_sg_rules)
self.firewall.update_security_group_rules(remote_sg_id, peer_sg_rules)
self.firewall.update_security_group_members(remote_sg_id,
vm_sg_members)
self.firewall.prepare_port_filter(peer_port_desc)
vm_sg_rules = [{'ethertype': 'IPv4', 'direction': 'ingress',
'protocol': 'icmp', 'remote_group_id': remote_sg_id}]
self._apply_security_group_rules(self.FAKE_SECURITY_GROUP_ID,
vm_sg_rules)
vm_sg_members = {'IPv4': [self.tester.peer_ip_address]}
with self.firewall.defer_apply():
self.firewall.update_security_group_members(
remote_sg_id, vm_sg_members)
self.tester.assert_connection(protocol=self.tester.ICMP,
direction=self.tester.INGRESS)
self.tester.assert_no_connection(protocol=self.tester.TCP,

View File

@ -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)

View File

@ -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)

View File

@ -53,6 +53,8 @@ class OpenvswitchMechanismBaseTestCase(base.AgentMechanismBaseTestCase):
def setUp(self):
super(OpenvswitchMechanismBaseTestCase, self).setUp()
cfg.CONF.set_override('firewall_driver', 'iptables_hybrid',
'SECURITYGROUP')
self.driver = mech_openvswitch.OpenvswitchMechanismDriver()
self.driver.initialize()

View File

@ -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>`_.

View File

@ -147,6 +147,11 @@ neutron.interface_drivers =
linuxbridge = neutron.agent.linux.interface:BridgeInterfaceDriver
null = neutron.agent.linux.interface:NullDriver
openvswitch = neutron.agent.linux.interface:OVSInterfaceDriver
neutron.agent.firewall_drivers =
noop = neutron.agent.firewall:NoopFirewallDriver
iptables = neutron.agent.linux.iptables_firewall:IptablesFirewallDriver
iptables_hybrid = neutron.agent.linux.iptables_firewall:OVSHybridIptablesFirewallDriver
openvswitch = neutron.agent.linux.openvswitch_firewall:OVSFirewallDriver
[build_sphinx]
all_files = 1