Add portsecurity extension support
Add portsecurity extension driver into ML2 plugin and implement it in iptables_firewall. The scope of this change is: - Abstract a common class PortSecurityDbCommon from the old PortSecurityDbMixin - Add a new extension driver port-security, implement process_xxx and extend_xxx_dict method and provide a db migration from the existing networks and ports - Update the new added 'unfiltered_ports' in iptables firewall of l2 agent to reflect the update of port-security Co-Authored-By: Shweta P <shpadubi@cisco.com> Change-Id: I2da53168e2529db7a8094ce90ef3a8a93fe55727 Partially Implements: blueprint ml2-ovs-portsecurity
This commit is contained in:
parent
5daf88fdb3
commit
554d266f56
@ -33,3 +33,4 @@ INVALID_DROP = ("Drop packets that appear related to an existing connection "
|
||||
ALLOW_ASSOC = ('Direct packets associated with a known session to the RETURN '
|
||||
'chain.')
|
||||
IPV6_RA_ALLOW = 'Allow IPv6 ICMP traffic to allow RA packets.'
|
||||
PORT_SEC_ACCEPT = 'Accept all packets when port security is disabled.'
|
||||
|
@ -24,6 +24,7 @@ from neutron.agent.linux import iptables_comments as ic
|
||||
from neutron.agent.linux import iptables_manager
|
||||
from neutron.common import constants
|
||||
from neutron.common import ipv6_utils
|
||||
from neutron.extensions import portsecurity as psec
|
||||
from neutron.i18n import _LI
|
||||
|
||||
|
||||
@ -57,9 +58,11 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||
self.ipset = ipset_manager.IpsetManager(namespace=namespace)
|
||||
# list of port which has security group
|
||||
self.filtered_ports = {}
|
||||
self.unfiltered_ports = {}
|
||||
self._add_fallback_chain_v4v6()
|
||||
self._defer_apply = False
|
||||
self._pre_defer_filtered_ports = None
|
||||
self._pre_defer_unfiltered_ports = None
|
||||
# List of security group rules for ports residing on this host
|
||||
self.sg_rules = {}
|
||||
self.pre_sg_rules = None
|
||||
@ -71,7 +74,7 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||
|
||||
@property
|
||||
def ports(self):
|
||||
return self.filtered_ports
|
||||
return dict(self.filtered_ports, **self.unfiltered_ports)
|
||||
|
||||
def update_security_group_rules(self, sg_id, sg_rules):
|
||||
LOG.debug("Update rules of security group (%s)", sg_id)
|
||||
@ -81,42 +84,72 @@ 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):
|
||||
self.unfiltered_ports[port['device']] = port
|
||||
self.filtered_ports.pop(port['device'], None)
|
||||
else:
|
||||
self.filtered_ports[port['device']] = port
|
||||
self.unfiltered_ports.pop(port['device'], None)
|
||||
|
||||
def _unset_ports(self, port):
|
||||
self.unfiltered_ports.pop(port['device'], None)
|
||||
self.filtered_ports.pop(port['device'], None)
|
||||
|
||||
def prepare_port_filter(self, port):
|
||||
LOG.debug("Preparing device (%s) filter", port['device'])
|
||||
self._remove_chains()
|
||||
self.filtered_ports[port['device']] = port
|
||||
self._set_ports(port)
|
||||
|
||||
# each security group has it own chains
|
||||
self._setup_chains()
|
||||
self.iptables.apply()
|
||||
|
||||
def update_port_filter(self, port):
|
||||
LOG.debug("Updating device (%s) filter", port['device'])
|
||||
if port['device'] not in self.filtered_ports:
|
||||
if port['device'] not in self.ports:
|
||||
LOG.info(_LI('Attempted to update port filter which is not '
|
||||
'filtered %s'), port['device'])
|
||||
return
|
||||
self._remove_chains()
|
||||
self.filtered_ports[port['device']] = port
|
||||
self._set_ports(port)
|
||||
self._setup_chains()
|
||||
self.iptables.apply()
|
||||
|
||||
def remove_port_filter(self, port):
|
||||
LOG.debug("Removing device (%s) filter", port['device'])
|
||||
if not self.filtered_ports.get(port['device']):
|
||||
if port['device'] not in self.ports:
|
||||
LOG.info(_LI('Attempted to remove port filter which is not '
|
||||
'filtered %r'), port)
|
||||
return
|
||||
self._remove_chains()
|
||||
self.filtered_ports.pop(port['device'], None)
|
||||
self._unset_ports(port)
|
||||
self._setup_chains()
|
||||
self.iptables.apply()
|
||||
|
||||
def _add_accept_rule_port_sec(self, port, direction):
|
||||
self._update_port_sec_rules(port, direction, add=True)
|
||||
|
||||
def _remove_rule_port_sec(self, port, direction):
|
||||
self._update_port_sec_rules(port, direction, add=False)
|
||||
|
||||
def _remove_rule_from_chain_v4v6(self, chain_name, ipv4_rules, ipv6_rules):
|
||||
for rule in ipv4_rules:
|
||||
self.iptables.ipv4['filter'].remove_rule(chain_name, rule)
|
||||
|
||||
for rule in ipv6_rules:
|
||||
self.iptables.ipv6['filter'].remove_rule(chain_name, rule)
|
||||
|
||||
def _setup_chains(self):
|
||||
"""Setup ingress and egress chain for a port."""
|
||||
if not self._defer_apply:
|
||||
self._setup_chains_apply(self.filtered_ports)
|
||||
self._setup_chains_apply(self.filtered_ports,
|
||||
self.unfiltered_ports)
|
||||
|
||||
def _setup_chains_apply(self, ports):
|
||||
def _setup_chains_apply(self, ports, unfiltered_ports):
|
||||
self._add_chain_by_name_v4v6(SG_CHAIN)
|
||||
for port in ports.values():
|
||||
self._setup_chain(port, INGRESS_DIRECTION)
|
||||
@ -124,16 +157,24 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||
self.iptables.ipv4['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
|
||||
self.iptables.ipv6['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
|
||||
|
||||
for port in unfiltered_ports.values():
|
||||
self._add_accept_rule_port_sec(port, INGRESS_DIRECTION)
|
||||
self._add_accept_rule_port_sec(port, EGRESS_DIRECTION)
|
||||
|
||||
def _remove_chains(self):
|
||||
"""Remove ingress and egress chain for a port."""
|
||||
if not self._defer_apply:
|
||||
self._remove_chains_apply(self.filtered_ports)
|
||||
self._remove_chains_apply(self.filtered_ports,
|
||||
self.unfiltered_ports)
|
||||
|
||||
def _remove_chains_apply(self, ports):
|
||||
def _remove_chains_apply(self, ports, unfiltered_ports):
|
||||
for port in ports.values():
|
||||
self._remove_chain(port, INGRESS_DIRECTION)
|
||||
self._remove_chain(port, EGRESS_DIRECTION)
|
||||
self._remove_chain(port, SPOOF_FILTER)
|
||||
for port in unfiltered_ports.values():
|
||||
self._remove_rule_port_sec(port, INGRESS_DIRECTION)
|
||||
self._remove_rule_port_sec(port, EGRESS_DIRECTION)
|
||||
self._remove_chain_by_name_v4v6(SG_CHAIN)
|
||||
|
||||
def _setup_chain(self, port, DIRECTION):
|
||||
@ -173,6 +214,30 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||
def _get_device_name(self, port):
|
||||
return port['device']
|
||||
|
||||
def _update_port_sec_rules(self, port, direction, add=False):
|
||||
# add/remove rules in FORWARD and INPUT chain
|
||||
device = self._get_device_name(port)
|
||||
|
||||
jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
|
||||
'-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
|
||||
device)]
|
||||
if add:
|
||||
self._add_rules_to_chain_v4v6(
|
||||
'FORWARD', jump_rule, jump_rule, comment=ic.PORT_SEC_ACCEPT)
|
||||
else:
|
||||
self._remove_rule_from_chain_v4v6('FORWARD', jump_rule, jump_rule)
|
||||
|
||||
if direction == EGRESS_DIRECTION:
|
||||
jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
|
||||
'-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
|
||||
device)]
|
||||
if add:
|
||||
self._add_rules_to_chain_v4v6('INPUT', jump_rule, jump_rule,
|
||||
comment=ic.PORT_SEC_ACCEPT)
|
||||
else:
|
||||
self._remove_rule_from_chain_v4v6(
|
||||
'INPUT', jump_rule, jump_rule)
|
||||
|
||||
def _add_chain(self, port, direction):
|
||||
chain_name = self._port_chain_name(port, direction)
|
||||
self._add_chain_by_name_v4v6(chain_name)
|
||||
@ -496,6 +561,7 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||
if not self._defer_apply:
|
||||
self.iptables.defer_apply_on()
|
||||
self._pre_defer_filtered_ports = dict(self.filtered_ports)
|
||||
self._pre_defer_unfiltered_ports = dict(self.unfiltered_ports)
|
||||
self.pre_sg_members = dict(self.sg_members)
|
||||
self.pre_sg_rules = dict(self.sg_rules)
|
||||
self._defer_apply = True
|
||||
@ -587,11 +653,14 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
|
||||
def filter_defer_apply_off(self):
|
||||
if self._defer_apply:
|
||||
self._defer_apply = False
|
||||
self._remove_chains_apply(self._pre_defer_filtered_ports)
|
||||
self._setup_chains_apply(self.filtered_ports)
|
||||
self._remove_chains_apply(self._pre_defer_filtered_ports,
|
||||
self._pre_defer_unfiltered_ports)
|
||||
self._setup_chains_apply(self.filtered_ports,
|
||||
self.unfiltered_ports)
|
||||
self.iptables.defer_apply_off()
|
||||
self._remove_unused_security_group_info()
|
||||
self._pre_defer_filtered_ports = None
|
||||
self._pre_defer_unfiltered_ports = None
|
||||
|
||||
|
||||
class OVSHybridIptablesFirewallDriver(IptablesFirewallDriver):
|
||||
|
@ -0,0 +1,44 @@
|
||||
# Copyright 2015 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""add port-security in ml2
|
||||
|
||||
Revision ID: 35a0f3365720
|
||||
Revises: 341ee8a4ccb5
|
||||
Create Date: 2014-09-30 09:41:14.146519
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '35a0f3365720'
|
||||
down_revision = '341ee8a4ccb5'
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute('INSERT INTO networksecuritybindings (network_id, '
|
||||
'port_security_enabled) SELECT id, True FROM networks '
|
||||
'WHERE id NOT IN (SELECT network_id FROM '
|
||||
'networksecuritybindings);')
|
||||
|
||||
op.execute('INSERT INTO portsecuritybindings (port_id, '
|
||||
'port_security_enabled) SELECT id, True FROM ports '
|
||||
'WHERE id NOT IN (SELECT port_id FROM '
|
||||
'portsecuritybindings);')
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -1 +1 @@
|
||||
341ee8a4ccb5
|
||||
35a0f3365720
|
||||
|
@ -12,71 +12,18 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
from neutron.api.v2 import attributes as attrs
|
||||
from neutron.db import db_base_plugin_v2
|
||||
from neutron.db import model_base
|
||||
from neutron.db import models_v2
|
||||
from neutron.db import portsecurity_db_common
|
||||
from neutron.extensions import portsecurity as psec
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortSecurityBinding(model_base.BASEV2):
|
||||
port_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('ports.id', ondelete="CASCADE"),
|
||||
primary_key=True)
|
||||
port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
|
||||
|
||||
# Add a relationship to the Port model in order to be to able to
|
||||
# instruct SQLAlchemy to eagerly load port security binding
|
||||
port = orm.relationship(
|
||||
models_v2.Port,
|
||||
backref=orm.backref("port_security", uselist=False,
|
||||
cascade='delete', lazy='joined'))
|
||||
|
||||
|
||||
class NetworkSecurityBinding(model_base.BASEV2):
|
||||
network_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('networks.id', ondelete="CASCADE"),
|
||||
primary_key=True)
|
||||
port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
|
||||
|
||||
# Add a relationship to the Port model in order to be able to instruct
|
||||
# SQLAlchemy to eagerly load default port security setting for ports
|
||||
# on this network
|
||||
network = orm.relationship(
|
||||
models_v2.Network,
|
||||
backref=orm.backref("port_security", uselist=False,
|
||||
cascade='delete', lazy='joined'))
|
||||
|
||||
|
||||
class PortSecurityDbMixin(object):
|
||||
"""Mixin class to add port security."""
|
||||
|
||||
def _process_network_port_security_create(
|
||||
self, context, network_req, network_res):
|
||||
with context.session.begin(subtransactions=True):
|
||||
db = NetworkSecurityBinding(
|
||||
network_id=network_res['id'],
|
||||
port_security_enabled=network_req[psec.PORTSECURITY])
|
||||
context.session.add(db)
|
||||
network_res[psec.PORTSECURITY] = network_req[psec.PORTSECURITY]
|
||||
return self._make_network_port_security_dict(db)
|
||||
|
||||
def _process_port_port_security_create(
|
||||
self, context, port_req, port_res):
|
||||
with context.session.begin(subtransactions=True):
|
||||
db = PortSecurityBinding(
|
||||
port_id=port_res['id'],
|
||||
port_security_enabled=port_req[psec.PORTSECURITY])
|
||||
context.session.add(db)
|
||||
port_res[psec.PORTSECURITY] = port_req[psec.PORTSECURITY]
|
||||
return self._make_port_security_dict(db)
|
||||
class PortSecurityDbMixin(portsecurity_db_common.PortSecurityDbCommon):
|
||||
# Register dict extend functions for ports and networks
|
||||
db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
|
||||
attrs.NETWORKS, ['_extend_port_security_dict'])
|
||||
db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
|
||||
attrs.PORTS, ['_extend_port_security_dict'])
|
||||
|
||||
def _extend_port_security_dict(self, response_data, db_data):
|
||||
if ('port-security' in
|
||||
@ -84,63 +31,6 @@ class PortSecurityDbMixin(object):
|
||||
psec_value = db_data['port_security'][psec.PORTSECURITY]
|
||||
response_data[psec.PORTSECURITY] = psec_value
|
||||
|
||||
def _get_network_security_binding(self, context, network_id):
|
||||
try:
|
||||
query = self._model_query(context, NetworkSecurityBinding)
|
||||
binding = query.filter(
|
||||
NetworkSecurityBinding.network_id == network_id).one()
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
return binding[psec.PORTSECURITY]
|
||||
|
||||
def _get_port_security_binding(self, context, port_id):
|
||||
try:
|
||||
query = self._model_query(context, PortSecurityBinding)
|
||||
binding = query.filter(
|
||||
PortSecurityBinding.port_id == port_id).one()
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
return binding[psec.PORTSECURITY]
|
||||
|
||||
def _process_port_port_security_update(
|
||||
self, context, port_req, port_res):
|
||||
if psec.PORTSECURITY in port_req:
|
||||
port_security_enabled = port_req[psec.PORTSECURITY]
|
||||
else:
|
||||
return
|
||||
try:
|
||||
query = self._model_query(context, PortSecurityBinding)
|
||||
port_id = port_res['id']
|
||||
binding = query.filter(
|
||||
PortSecurityBinding.port_id == port_id).one()
|
||||
|
||||
binding.port_security_enabled = port_security_enabled
|
||||
port_res[psec.PORTSECURITY] = port_security_enabled
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
|
||||
def _process_network_port_security_update(
|
||||
self, context, network_req, network_res):
|
||||
if psec.PORTSECURITY in network_req:
|
||||
port_security_enabled = network_req[psec.PORTSECURITY]
|
||||
else:
|
||||
return
|
||||
try:
|
||||
query = self._model_query(context, NetworkSecurityBinding)
|
||||
network_id = network_res['id']
|
||||
binding = query.filter(
|
||||
NetworkSecurityBinding.network_id == network_id).one()
|
||||
|
||||
binding.port_security_enabled = port_security_enabled
|
||||
network_res[psec.PORTSECURITY] = port_security_enabled
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
|
||||
def _make_network_port_security_dict(self, port_security, fields=None):
|
||||
res = {'network_id': port_security['network_id'],
|
||||
psec.PORTSECURITY: port_security[psec.PORTSECURITY]}
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _determine_port_security_and_has_ip(self, context, port):
|
||||
"""Returns a tuple of booleans (port_security_enabled, has_ip).
|
||||
|
||||
@ -170,16 +60,5 @@ class PortSecurityDbMixin(object):
|
||||
|
||||
return (port_security_enabled, has_ip)
|
||||
|
||||
def _make_port_security_dict(self, port, fields=None):
|
||||
res = {'port_id': port['port_id'],
|
||||
psec.PORTSECURITY: port[psec.PORTSECURITY]}
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _ip_on_port(self, port):
|
||||
return bool(port.get('fixed_ips'))
|
||||
|
||||
# Register dict extend functions for ports and networks
|
||||
db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
|
||||
attrs.NETWORKS, ['_extend_port_security_dict'])
|
||||
db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
|
||||
attrs.PORTS, ['_extend_port_security_dict'])
|
||||
|
139
neutron/db/portsecurity_db_common.py
Normal file
139
neutron/db/portsecurity_db_common.py
Normal file
@ -0,0 +1,139 @@
|
||||
# Copyright 2013 VMware, Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
from neutron.db import model_base
|
||||
from neutron.db import models_v2
|
||||
from neutron.extensions import portsecurity as psec
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortSecurityBinding(model_base.BASEV2):
|
||||
port_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('ports.id', ondelete="CASCADE"),
|
||||
primary_key=True)
|
||||
port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
|
||||
|
||||
# Add a relationship to the Port model in order to be to able to
|
||||
# instruct SQLAlchemy to eagerly load port security binding
|
||||
port = orm.relationship(
|
||||
models_v2.Port,
|
||||
backref=orm.backref("port_security", uselist=False,
|
||||
cascade='delete', lazy='joined'))
|
||||
|
||||
|
||||
class NetworkSecurityBinding(model_base.BASEV2):
|
||||
network_id = sa.Column(sa.String(36),
|
||||
sa.ForeignKey('networks.id', ondelete="CASCADE"),
|
||||
primary_key=True)
|
||||
port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
|
||||
|
||||
# Add a relationship to the Port model in order to be able to instruct
|
||||
# SQLAlchemy to eagerly load default port security setting for ports
|
||||
# on this network
|
||||
network = orm.relationship(
|
||||
models_v2.Network,
|
||||
backref=orm.backref("port_security", uselist=False,
|
||||
cascade='delete', lazy='joined'))
|
||||
|
||||
|
||||
class PortSecurityDbCommon(object):
|
||||
"""Mixin class to add port security."""
|
||||
|
||||
def _process_network_port_security_create(
|
||||
self, context, network_req, network_res):
|
||||
with context.session.begin(subtransactions=True):
|
||||
db = NetworkSecurityBinding(
|
||||
network_id=network_res['id'],
|
||||
port_security_enabled=network_req[psec.PORTSECURITY])
|
||||
context.session.add(db)
|
||||
network_res[psec.PORTSECURITY] = network_req[psec.PORTSECURITY]
|
||||
return self._make_network_port_security_dict(db)
|
||||
|
||||
def _process_port_port_security_create(
|
||||
self, context, port_req, port_res):
|
||||
with context.session.begin(subtransactions=True):
|
||||
db = PortSecurityBinding(
|
||||
port_id=port_res['id'],
|
||||
port_security_enabled=port_req[psec.PORTSECURITY])
|
||||
context.session.add(db)
|
||||
port_res[psec.PORTSECURITY] = port_req[psec.PORTSECURITY]
|
||||
return self._make_port_security_dict(db)
|
||||
|
||||
def _get_network_security_binding(self, context, network_id):
|
||||
try:
|
||||
query = self._model_query(context, NetworkSecurityBinding)
|
||||
binding = query.filter(
|
||||
NetworkSecurityBinding.network_id == network_id).one()
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
return binding[psec.PORTSECURITY]
|
||||
|
||||
def _get_port_security_binding(self, context, port_id):
|
||||
try:
|
||||
query = self._model_query(context, PortSecurityBinding)
|
||||
binding = query.filter(
|
||||
PortSecurityBinding.port_id == port_id).one()
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
return binding[psec.PORTSECURITY]
|
||||
|
||||
def _process_port_port_security_update(
|
||||
self, context, port_req, port_res):
|
||||
if psec.PORTSECURITY in port_req:
|
||||
port_security_enabled = port_req[psec.PORTSECURITY]
|
||||
else:
|
||||
return
|
||||
try:
|
||||
query = self._model_query(context, PortSecurityBinding)
|
||||
port_id = port_res['id']
|
||||
binding = query.filter(
|
||||
PortSecurityBinding.port_id == port_id).one()
|
||||
|
||||
binding.port_security_enabled = port_security_enabled
|
||||
port_res[psec.PORTSECURITY] = port_security_enabled
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
|
||||
def _process_network_port_security_update(
|
||||
self, context, network_req, network_res):
|
||||
if psec.PORTSECURITY in network_req:
|
||||
port_security_enabled = network_req[psec.PORTSECURITY]
|
||||
else:
|
||||
return
|
||||
try:
|
||||
query = self._model_query(context, NetworkSecurityBinding)
|
||||
network_id = network_res['id']
|
||||
binding = query.filter(
|
||||
NetworkSecurityBinding.network_id == network_id).one()
|
||||
|
||||
binding.port_security_enabled = port_security_enabled
|
||||
network_res[psec.PORTSECURITY] = port_security_enabled
|
||||
except exc.NoResultFound:
|
||||
raise psec.PortSecurityBindingNotFound()
|
||||
|
||||
def _make_network_port_security_dict(self, port_security, fields=None):
|
||||
res = {'network_id': port_security['network_id'],
|
||||
psec.PORTSECURITY: port_security[psec.PORTSECURITY]}
|
||||
return self._fields(res, fields)
|
||||
|
||||
def _make_port_security_dict(self, port, fields=None):
|
||||
res = {'port_id': port['port_id'],
|
||||
psec.PORTSECURITY: port[psec.PORTSECURITY]}
|
||||
return self._fields(res, fields)
|
0
neutron/plugins/ml2/extensions/__init__.py
Normal file
0
neutron/plugins/ml2/extensions/__init__.py
Normal file
86
neutron/plugins/ml2/extensions/port_security.py
Normal file
86
neutron/plugins/ml2/extensions/port_security.py
Normal file
@ -0,0 +1,86 @@
|
||||
# Copyright 2015 Intel Corporation.
|
||||
# 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.api.v2 import attributes as attrs
|
||||
from neutron.db import common_db_mixin
|
||||
from neutron.db import portsecurity_db_common as ps_db_common
|
||||
from neutron.extensions import portsecurity as psec
|
||||
from neutron.i18n import _LI
|
||||
from neutron.plugins.ml2 import driver_api as api
|
||||
from oslo_log import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortSecurityExtensionDriver(api.ExtensionDriver,
|
||||
ps_db_common.PortSecurityDbCommon,
|
||||
common_db_mixin.CommonDbMixin):
|
||||
_supported_extension_alias = 'port-security'
|
||||
|
||||
def initialize(self):
|
||||
LOG.info(_LI("PortSecurityExtensionDriver initialization complete"))
|
||||
|
||||
@property
|
||||
def extension_alias(self):
|
||||
return self._supported_extension_alias
|
||||
|
||||
def process_create_network(self, context, data, result):
|
||||
# Create the network extension attributes.
|
||||
if psec.PORTSECURITY in data:
|
||||
self._process_network_port_security_create(context, data, result)
|
||||
|
||||
def process_update_network(self, context, data, result):
|
||||
# Update the network extension attributes.
|
||||
if psec.PORTSECURITY in data:
|
||||
self._process_network_port_security_update(context, data, result)
|
||||
|
||||
def process_create_port(self, context, data, result):
|
||||
# Create the port extension attributes.
|
||||
data[psec.PORTSECURITY] = self._determine_port_security(context, data)
|
||||
self._process_port_port_security_create(context, data, result)
|
||||
|
||||
def process_update_port(self, context, data, result):
|
||||
if psec.PORTSECURITY in data:
|
||||
self._process_port_port_security_update(
|
||||
context, data, result)
|
||||
|
||||
def extend_network_dict(self, session, db_data, result):
|
||||
self._extend_port_security_dict(result, db_data)
|
||||
|
||||
def extend_port_dict(self, session, db_data, result):
|
||||
self._extend_port_security_dict(result, db_data)
|
||||
|
||||
def _extend_port_security_dict(self, response_data, db_data):
|
||||
response_data[psec.PORTSECURITY] = (
|
||||
db_data['port_security'][psec.PORTSECURITY])
|
||||
|
||||
def _determine_port_security(self, context, port):
|
||||
"""Returns a boolean (port_security_enabled).
|
||||
|
||||
Port_security is the value associated with the port if one is present
|
||||
otherwise the value associated with the network is returned.
|
||||
"""
|
||||
# we don't apply security groups for dhcp, router
|
||||
if (port.get('device_owner') and
|
||||
port['device_owner'].startswith('network:')):
|
||||
return False
|
||||
|
||||
if attrs.is_attr_set(port.get(psec.PORTSECURITY)):
|
||||
port_security_enabled = port[psec.PORTSECURITY]
|
||||
else:
|
||||
port_security_enabled = self._get_network_security_binding(
|
||||
context, port['network_id'])
|
||||
|
||||
return port_security_enabled
|
@ -57,7 +57,9 @@ from neutron.db import securitygroups_rpc_base as sg_db_rpc
|
||||
from neutron.extensions import allowedaddresspairs as addr_pair
|
||||
from neutron.extensions import extra_dhcp_opt as edo_ext
|
||||
from neutron.extensions import portbindings
|
||||
from neutron.extensions import portsecurity as psec
|
||||
from neutron.extensions import providernet as provider
|
||||
from neutron.extensions import securitygroup as ext_sg
|
||||
from neutron.i18n import _LE, _LI, _LW
|
||||
from neutron import manager
|
||||
from neutron.openstack.common import uuidutils
|
||||
@ -900,17 +902,39 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
# the fact that an error occurred.
|
||||
LOG.error(_LE("mechanism_manager.delete_subnet_postcommit failed"))
|
||||
|
||||
# TODO(yalei) - will be simplified after security group and address pair be
|
||||
# converted to ext driver too.
|
||||
def _portsec_ext_port_create_processing(self, context, port_data, port):
|
||||
attrs = port[attributes.PORT]
|
||||
port_security = ((port_data.get(psec.PORTSECURITY) is None) or
|
||||
port_data[psec.PORTSECURITY])
|
||||
|
||||
# allowed address pair checks
|
||||
if attributes.is_attr_set(attrs.get(addr_pair.ADDRESS_PAIRS)):
|
||||
if not port_security:
|
||||
raise addr_pair.AddressPairAndPortSecurityRequired()
|
||||
else:
|
||||
# remove ATTR_NOT_SPECIFIED
|
||||
attrs[addr_pair.ADDRESS_PAIRS] = []
|
||||
|
||||
if port_security:
|
||||
self._ensure_default_security_group_on_port(context, port)
|
||||
elif attributes.is_attr_set(attrs.get(ext_sg.SECURITYGROUPS)):
|
||||
raise psec.PortSecurityAndIPRequiredForSecurityGroups()
|
||||
|
||||
def _create_port_db(self, context, port):
|
||||
attrs = port[attributes.PORT]
|
||||
attrs['status'] = const.PORT_STATUS_DOWN
|
||||
|
||||
session = context.session
|
||||
with session.begin(subtransactions=True):
|
||||
self._ensure_default_security_group_on_port(context, port)
|
||||
sgids = self._get_security_groups_on_port(context, port)
|
||||
dhcp_opts = attrs.get(edo_ext.EXTRADHCPOPTS, [])
|
||||
result = super(Ml2Plugin, self).create_port(context, port)
|
||||
self.extension_manager.process_create_port(context, attrs, result)
|
||||
self._portsec_ext_port_create_processing(context, result, port)
|
||||
|
||||
# sgids must be got after portsec checked with security group
|
||||
sgids = self._get_security_groups_on_port(context, port)
|
||||
self._process_port_create_security_group(context, result, sgids)
|
||||
network = self.get_network(context, result['network_id'])
|
||||
binding = db.add_port_binding(session, result['id'])
|
||||
@ -987,6 +1011,47 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
resource_ids)
|
||||
self._delete_objects(context, attributes.PORT, objects)
|
||||
|
||||
# TODO(yalei) - will be simplified after security group and address pair be
|
||||
# converted to ext driver too.
|
||||
def _portsec_ext_port_update_processing(self, updated_port, context, port,
|
||||
id):
|
||||
port_security = ((updated_port.get(psec.PORTSECURITY) is None) or
|
||||
updated_port[psec.PORTSECURITY])
|
||||
|
||||
if port_security:
|
||||
return
|
||||
|
||||
# check the address-pairs
|
||||
if self._check_update_has_allowed_address_pairs(port):
|
||||
# has address pairs in request
|
||||
raise addr_pair.AddressPairAndPortSecurityRequired()
|
||||
elif (not
|
||||
self._check_update_deletes_allowed_address_pairs(port)):
|
||||
# not a request for deleting the address-pairs
|
||||
updated_port[addr_pair.ADDRESS_PAIRS] = (
|
||||
self.get_allowed_address_pairs(context, id))
|
||||
|
||||
# check if address pairs has been in db, if address pairs could
|
||||
# be put in extension driver, we can refine here.
|
||||
if updated_port[addr_pair.ADDRESS_PAIRS]:
|
||||
raise addr_pair.AddressPairAndPortSecurityRequired()
|
||||
|
||||
# checks if security groups were updated adding/modifying
|
||||
# security groups, port security is set
|
||||
if self._check_update_has_security_groups(port):
|
||||
raise psec.PortSecurityAndIPRequiredForSecurityGroups()
|
||||
elif (not
|
||||
self._check_update_deletes_security_groups(port)):
|
||||
# Update did not have security groups passed in. Check
|
||||
# that port does not have any security groups already on it.
|
||||
filters = {'port_id': [id]}
|
||||
security_groups = (
|
||||
super(Ml2Plugin, self)._get_port_security_group_bindings(
|
||||
context, filters)
|
||||
)
|
||||
if security_groups:
|
||||
raise psec.PortSecurityPortHasSecurityGroup()
|
||||
|
||||
def update_port(self, context, id, port):
|
||||
attrs = port[attributes.PORT]
|
||||
need_port_update_notify = False
|
||||
@ -1009,6 +1074,14 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
port)
|
||||
self.extension_manager.process_update_port(context, attrs,
|
||||
updated_port)
|
||||
self._portsec_ext_port_update_processing(updated_port, context,
|
||||
port, id)
|
||||
|
||||
if (psec.PORTSECURITY in attrs) and (
|
||||
original_port[psec.PORTSECURITY] !=
|
||||
updated_port[psec.PORTSECURITY]):
|
||||
need_port_update_notify = True
|
||||
|
||||
if addr_pair.ADDRESS_PAIRS in attrs:
|
||||
need_port_update_notify |= (
|
||||
self.update_address_pairs_on_port(context, id, port,
|
||||
|
@ -16,12 +16,26 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from neutron.agent.linux import ip_lib
|
||||
from neutron.agent.linux import iptables_firewall
|
||||
from neutron.agent import securitygroups_rpc as sg_cfg
|
||||
from neutron.tests.functional.agent.linux import base
|
||||
from neutron.tests.functional.agent.linux import helpers
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
class IptablesFirewallTestCase(base.BaseBridgeTestCase):
|
||||
MAC_REAL = "fa:16:3e:9a:2f:49"
|
||||
MAC_SPOOFED = "fa:16:3e:9a:2f:48"
|
||||
FAKE_SECURITY_GROUP_ID = "fake_sg_id"
|
||||
|
||||
def _set_src_mac(self, mac):
|
||||
self.src_veth.link.set_down()
|
||||
self.src_veth.link.set_address(mac)
|
||||
self.src_veth.link.set_up()
|
||||
|
||||
def setUp(self):
|
||||
cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP')
|
||||
super(IptablesFirewallTestCase, self).setUp()
|
||||
self.bridge = self.create_bridge()
|
||||
|
||||
@ -40,8 +54,39 @@ class IptablesFirewallTestCase(base.BaseBridgeTestCase):
|
||||
self.firewall = iptables_firewall.IptablesFirewallDriver(
|
||||
namespace=self.bridge.namespace)
|
||||
|
||||
# TODO(yamahata): add tests...
|
||||
self._set_src_mac(self.MAC_REAL)
|
||||
|
||||
self.src_port = {'admin_state_up': True,
|
||||
'device': self.src_br_veth.name,
|
||||
'device_owner': 'compute:None',
|
||||
'fixed_ips': [self.SRC_ADDRESS],
|
||||
'mac_address': self.MAC_REAL,
|
||||
'port_security_enabled': True,
|
||||
'security_groups': [self.FAKE_SECURITY_GROUP_ID],
|
||||
'status': 'ACTIVE'}
|
||||
|
||||
# setup firewall on bridge and send packet from src_veth and observe
|
||||
# if sent packet can be observed on dst_veth
|
||||
def test_firewall(self):
|
||||
pass
|
||||
def test_port_sec_within_firewall(self):
|
||||
pinger = helpers.Pinger(ip_lib.IPWrapper(self.src_veth.namespace))
|
||||
|
||||
# update the sg_group to make ping pass
|
||||
sg_rules = [{'ethertype': 'IPv4', 'direction': 'ingress',
|
||||
'source_ip_prefix': '0.0.0.0/0', 'protocol': 'icmp'},
|
||||
{'ethertype': 'IPv4', 'direction': 'egress'}]
|
||||
|
||||
with self.firewall.defer_apply():
|
||||
self.firewall.update_security_group_rules(
|
||||
self.FAKE_SECURITY_GROUP_ID,
|
||||
sg_rules)
|
||||
self.firewall.prepare_port_filter(self.src_port)
|
||||
pinger.assert_ping(self.DST_ADDRESS)
|
||||
|
||||
# modify the src_veth's MAC and test again
|
||||
self._set_src_mac(self.MAC_SPOOFED)
|
||||
pinger.assert_no_ping(self.DST_ADDRESS)
|
||||
|
||||
# update the port's port_security_enabled value and test again
|
||||
self.src_port['port_security_enabled'] = False
|
||||
self.firewall.update_port_filter(self.src_port)
|
||||
pinger.assert_ping(self.DST_ADDRESS)
|
||||
|
29
neutron/tests/unit/ml2/test_ext_portsecurity.py
Normal file
29
neutron/tests/unit/ml2/test_ext_portsecurity.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2015 OpenStack Foundation.
|
||||
# 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.plugins.ml2 import config
|
||||
from neutron.tests.unit.ml2 import test_ml2_plugin
|
||||
from neutron.tests.unit import test_extension_portsecurity as test_psec
|
||||
|
||||
|
||||
class PSExtDriverTestCase(test_ml2_plugin.Ml2PluginV2TestCase,
|
||||
test_psec.TestPortSecurity):
|
||||
_extension_drivers = ['port_security']
|
||||
|
||||
def setUp(self):
|
||||
config.cfg.CONF.set_override('extension_drivers',
|
||||
self._extension_drivers,
|
||||
group='ml2')
|
||||
super(PSExtDriverTestCase, self).setUp()
|
@ -166,7 +166,7 @@ class PortSecurityTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
|
||||
|
||||
class PortSecurityDBTestCase(PortSecurityTestCase):
|
||||
def setUp(self, plugin=None):
|
||||
def setUp(self, plugin=None, service_plugins=None):
|
||||
plugin = plugin or DB_PLUGIN_KLASS
|
||||
super(PortSecurityDBTestCase, self).setUp(plugin)
|
||||
|
||||
@ -279,7 +279,9 @@ class TestPortSecurity(PortSecurityDBTestCase):
|
||||
'json', self._create_security_group(self.fmt, 'asdf', 'asdf'))
|
||||
security_group_id = security_group['security_group']['id']
|
||||
res = self._create_port('json', net['network']['id'],
|
||||
arg_list=('security_groups',),
|
||||
arg_list=('security_groups',
|
||||
'port_security_enabled'),
|
||||
port_security_enabled=True,
|
||||
security_groups=[security_group_id])
|
||||
port = self.deserialize('json', res)
|
||||
self.assertEqual(port['port'][psec.PORTSECURITY], True)
|
||||
|
@ -1213,12 +1213,12 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
|
||||
self.firewall.prepare_port_filter(port_prepare)
|
||||
self.firewall.update_port_filter(port_update)
|
||||
self.firewall.remove_port_filter(port_update)
|
||||
chain_applies.assert_has_calls([mock.call.remove({}),
|
||||
mock.call.setup({'d1': port_prepare}),
|
||||
mock.call.remove({'d1': port_prepare}),
|
||||
mock.call.setup({'d1': port_update}),
|
||||
mock.call.remove({'d1': port_update}),
|
||||
mock.call.setup({})])
|
||||
chain_applies.assert_has_calls([mock.call.remove({}, {}),
|
||||
mock.call.setup({'d1': port_prepare}, {}),
|
||||
mock.call.remove({'d1': port_prepare}, {}),
|
||||
mock.call.setup({'d1': port_update}, {}),
|
||||
mock.call.remove({'d1': port_update}, {}),
|
||||
mock.call.setup({}, {})])
|
||||
|
||||
def test_defer_chain_apply_need_pre_defer_copy(self):
|
||||
chain_applies = self._mock_chain_applies()
|
||||
@ -1227,10 +1227,10 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
|
||||
self.firewall.prepare_port_filter(port)
|
||||
with self.firewall.defer_apply():
|
||||
self.firewall.remove_port_filter(port)
|
||||
chain_applies.assert_has_calls([mock.call.remove({}),
|
||||
mock.call.setup(device2port),
|
||||
mock.call.remove(device2port),
|
||||
mock.call.setup({})])
|
||||
chain_applies.assert_has_calls([mock.call.remove({}, {}),
|
||||
mock.call.setup(device2port, {}),
|
||||
mock.call.remove(device2port, {}),
|
||||
mock.call.setup({}, {})])
|
||||
|
||||
def test_defer_chain_apply_coalesce_simple(self):
|
||||
chain_applies = self._mock_chain_applies()
|
||||
@ -1239,8 +1239,8 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
|
||||
self.firewall.prepare_port_filter(port)
|
||||
self.firewall.update_port_filter(port)
|
||||
self.firewall.remove_port_filter(port)
|
||||
chain_applies.assert_has_calls([mock.call.remove({}),
|
||||
mock.call.setup({})])
|
||||
chain_applies.assert_has_calls([mock.call.remove({}, {}),
|
||||
mock.call.setup({}, {})])
|
||||
|
||||
def test_defer_chain_apply_coalesce_multiple_ports(self):
|
||||
chain_applies = self._mock_chain_applies()
|
||||
@ -1250,8 +1250,8 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
|
||||
with self.firewall.defer_apply():
|
||||
self.firewall.prepare_port_filter(port1)
|
||||
self.firewall.prepare_port_filter(port2)
|
||||
chain_applies.assert_has_calls([mock.call.remove({}),
|
||||
mock.call.setup(device2port)])
|
||||
chain_applies.assert_has_calls([mock.call.remove({}, {}),
|
||||
mock.call.setup(device2port, {})])
|
||||
|
||||
def test_ip_spoofing_filter_with_multiple_ips(self):
|
||||
port = {'device': 'tapfake_dev',
|
||||
@ -1642,6 +1642,7 @@ class IptablesFirewallEnhancedIpsetTestCase(BaseIptablesFirewallTestCase):
|
||||
port = self._fake_port()
|
||||
self.firewall.filtered_ports['tapfake_dev'] = port
|
||||
self.firewall._pre_defer_filtered_ports = {}
|
||||
self.firewall._pre_defer_unfiltered_ports = {}
|
||||
self.firewall.filter_defer_apply_off()
|
||||
calls = [mock.call.destroy('fake_sgid', 'IPv4')]
|
||||
|
||||
|
@ -183,6 +183,7 @@ neutron.ml2.mechanism_drivers =
|
||||
neutron.ml2.extension_drivers =
|
||||
test = neutron.tests.unit.ml2.drivers.ext_test:TestExtensionDriver
|
||||
testdb = neutron.tests.unit.ml2.drivers.ext_test:TestDBExtensionDriver
|
||||
port_security = neutron.plugins.ml2.extensions.port_security:PortSecurityExtensionDriver
|
||||
neutron.openstack.common.cache.backends =
|
||||
memory = neutron.openstack.common.cache._backends.memory:MemoryBackend
|
||||
# These are for backwards compat with Icehouse notification_driver configuration values
|
||||
|
Loading…
x
Reference in New Issue
Block a user