From: Yalei Wang Date: Mon, 9 Feb 2015 19:22:27 +0000 (+0800) Subject: Add portsecurity extension support X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=554d266f56862d4f15de104e9199e9149124efbe;p=openstack-build%2Fneutron-build.git 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 Change-Id: I2da53168e2529db7a8094ce90ef3a8a93fe55727 Partially Implements: blueprint ml2-ovs-portsecurity --- diff --git a/neutron/agent/linux/iptables_comments.py b/neutron/agent/linux/iptables_comments.py index 7d158a9d8..142f82b72 100644 --- a/neutron/agent/linux/iptables_comments.py +++ b/neutron/agent/linux/iptables_comments.py @@ -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.' diff --git a/neutron/agent/linux/iptables_firewall.py b/neutron/agent/linux/iptables_firewall.py index d44f5b90b..0fa77b8a6 100644 --- a/neutron/agent/linux/iptables_firewall.py +++ b/neutron/agent/linux/iptables_firewall.py @@ -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): diff --git a/neutron/db/migration/alembic_migrations/versions/35a0f3365720_add_port_security_in_ml2.py b/neutron/db/migration/alembic_migrations/versions/35a0f3365720_add_port_security_in_ml2.py new file mode 100644 index 000000000..30b271476 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/35a0f3365720_add_port_security_in_ml2.py @@ -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 diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 1f9bcd91d..c535b02c3 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -341ee8a4ccb5 +35a0f3365720 diff --git a/neutron/db/portsecurity_db.py b/neutron/db/portsecurity_db.py index 9890948b2..bff1555a0 100644 --- a/neutron/db/portsecurity_db.py +++ b/neutron/db/portsecurity_db.py @@ -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']) diff --git a/neutron/db/portsecurity_db_common.py b/neutron/db/portsecurity_db_common.py new file mode 100644 index 000000000..3fad11152 --- /dev/null +++ b/neutron/db/portsecurity_db_common.py @@ -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) diff --git a/neutron/plugins/ml2/extensions/__init__.py b/neutron/plugins/ml2/extensions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron/plugins/ml2/extensions/port_security.py b/neutron/plugins/ml2/extensions/port_security.py new file mode 100644 index 000000000..aceec24a2 --- /dev/null +++ b/neutron/plugins/ml2/extensions/port_security.py @@ -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 diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index aa5620e04..d566fb802 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -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, diff --git a/neutron/tests/functional/agent/linux/test_iptables_firewall.py b/neutron/tests/functional/agent/linux/test_iptables_firewall.py index b902604fe..d0cfa1828 100644 --- a/neutron/tests/functional/agent/linux/test_iptables_firewall.py +++ b/neutron/tests/functional/agent/linux/test_iptables_firewall.py @@ -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) diff --git a/neutron/tests/unit/ml2/test_ext_portsecurity.py b/neutron/tests/unit/ml2/test_ext_portsecurity.py new file mode 100644 index 000000000..28fa68dcb --- /dev/null +++ b/neutron/tests/unit/ml2/test_ext_portsecurity.py @@ -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() diff --git a/neutron/tests/unit/test_extension_portsecurity.py b/neutron/tests/unit/test_extension_portsecurity.py index 591a9fac2..a7664058e 100644 --- a/neutron/tests/unit/test_extension_portsecurity.py +++ b/neutron/tests/unit/test_extension_portsecurity.py @@ -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) diff --git a/neutron/tests/unit/test_iptables_firewall.py b/neutron/tests/unit/test_iptables_firewall.py index 9e8db6732..8a22f827d 100644 --- a/neutron/tests/unit/test_iptables_firewall.py +++ b/neutron/tests/unit/test_iptables_firewall.py @@ -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')] diff --git a/setup.cfg b/setup.cfg index 126fa549d..a49631b9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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