From b993b7f4c0277e7f4951e78bb74e7c1606901466 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Wed, 8 Nov 2017 10:16:17 +0200 Subject: [PATCH] NSX|V3: VPNaaS support New support for VPNaaS on NSX-V3 2.2 Creating a vpn service per neutron service, and ike/ipsec/dpd policies + endpoints + connection per neutron connection Change-Id: Iad3778c1d826ae67f1b602625f5be0fe2f4c8fe3 --- doc/source/devstack.rst | 10 + .../notes/nsxv3-vpnaas-0b02762ff4b83904.yaml | 6 + vmware_nsx/db/db.py | 28 + .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../expand/0dbeda408e41_nsxv3_vpn_mapping.py | 43 ++ vmware_nsx/db/nsx_models.py | 11 + vmware_nsx/plugins/nsx_v3/plugin.py | 23 +- vmware_nsx/services/vpnaas/nsxv3/__init__.py | 0 .../services/vpnaas/nsxv3/ipsec_driver.py | 645 ++++++++++++++++++ .../services/vpnaas/nsxv3/ipsec_utils.py | 59 ++ .../services/vpnaas/nsxv3/ipsec_validator.py | 374 ++++++++++ vmware_nsx/tests/unit/nsx_v3/test_plugin.py | 3 + .../unit/services/vpnaas/test_nsxv3_vpnaas.py | 390 +++++++++++ 13 files changed, 1587 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/nsxv3-vpnaas-0b02762ff4b83904.yaml create mode 100644 vmware_nsx/db/migration/alembic_migrations/versions/queens/expand/0dbeda408e41_nsxv3_vpn_mapping.py create mode 100644 vmware_nsx/services/vpnaas/nsxv3/__init__.py create mode 100644 vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py create mode 100644 vmware_nsx/services/vpnaas/nsxv3/ipsec_utils.py create mode 100644 vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py create mode 100644 vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py diff --git a/doc/source/devstack.rst b/doc/source/devstack.rst index 8e677423c7..16feaedd58 100644 --- a/doc/source/devstack.rst +++ b/doc/source/devstack.rst @@ -211,6 +211,16 @@ Configure the service provider:: [service_providers] service_provider = LOADBALANCERV2:VMWareEdge:neutron_lbaas.drivers.vmware.edge_driver_v2.EdgeLoadBalancerDriverV2:default +Neutron VPNaaS +~~~~~~~~~~~~~~ + +Add neutron-vpnaas repo as an external repository and configure following flags in ``local.conf``:: + + [[local|localrc]] + enable_plugin neutron-vpnaas https://git.openstack.org/openstack/neutron-vpnaas + NEUTRON_VPNAAS_SERVICE_PROVIDER=VPN:vmware:vmware_nsx.services.vpnaas.nsxv3.ipsec_driver.NSXv3IPsecVpnDriver:default + + NSX-TVD ------- diff --git a/releasenotes/notes/nsxv3-vpnaas-0b02762ff4b83904.yaml b/releasenotes/notes/nsxv3-vpnaas-0b02762ff4b83904.yaml new file mode 100644 index 0000000000..d77d92700f --- /dev/null +++ b/releasenotes/notes/nsxv3-vpnaas-0b02762ff4b83904.yaml @@ -0,0 +1,6 @@ +--- +prelude: > + Support VPN-as-a-Service for VPN IPSEC in NSXv3 plugin. +features: + - | + NSXv3 plugin now supports VPN SEC through VPNaaS plugin. diff --git a/vmware_nsx/db/db.py b/vmware_nsx/db/db.py index bc2f2d540b..4a3c8be47e 100644 --- a/vmware_nsx/db/db.py +++ b/vmware_nsx/db/db.py @@ -692,3 +692,31 @@ def get_project_plugin_mapping(session, project): def get_project_plugin_mappings(session): return session.query(nsx_models.NsxProjectPluginMapping).all() + + +def add_nsx_vpn_connection_mapping(session, neutron_id, session_id, + dpd_profile_id, ike_profile_id, + ipsec_profile_id, peer_ep_id): + with session.begin(subtransactions=True): + mapping = nsx_models.NsxVpnConnectionMapping( + neutron_id=neutron_id, + session_id=session_id, + dpd_profile_id=dpd_profile_id, + ike_profile_id=ike_profile_id, + ipsec_profile_id=ipsec_profile_id, + peer_ep_id=peer_ep_id) + session.add(mapping) + return mapping + + +def get_nsx_vpn_connection_mapping(session, neutron_id): + try: + return (session.query(nsx_models.NsxVpnConnectionMapping). + filter_by(neutron_id=neutron_id).one()) + except exc.NoResultFound: + return + + +def delete_nsx_vpn_connection_mapping(session, neutron_id): + return (session.query(nsx_models.NsxVpnConnectionMapping). + filter_by(neutron_id=neutron_id).delete()) diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index b7146d2bc4..76862a8293 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -9799427fc0e1 +0dbeda408e41 diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/queens/expand/0dbeda408e41_nsxv3_vpn_mapping.py b/vmware_nsx/db/migration/alembic_migrations/versions/queens/expand/0dbeda408e41_nsxv3_vpn_mapping.py new file mode 100644 index 0000000000..91d72043e6 --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/queens/expand/0dbeda408e41_nsxv3_vpn_mapping.py @@ -0,0 +1,43 @@ +# Copyright 2017 VMware, 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. + +"""nsxv3_vpn_mapping + +Revision ID: 0dbeda408e41 +Revises: 9799427fc0e1 +Create Date: 2017-11-26 12:27:40.846088 + +""" + +# revision identifiers, used by Alembic. +revision = '0dbeda408e41' +down_revision = '9799427fc0e1' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + + op.create_table( + 'neutron_nsx_vpn_connection_mappings', + sa.Column('neutron_id', sa.String(36), nullable=False), + sa.Column('session_id', sa.String(36), nullable=False), + sa.Column('dpd_profile_id', sa.String(36), nullable=False), + sa.Column('ike_profile_id', sa.String(36), nullable=False), + sa.Column('ipsec_profile_id', sa.String(36), nullable=False), + sa.Column('peer_ep_id', sa.String(36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('neutron_id')) diff --git a/vmware_nsx/db/nsx_models.py b/vmware_nsx/db/nsx_models.py index b7c2a9acf0..82e7aacb66 100644 --- a/vmware_nsx/db/nsx_models.py +++ b/vmware_nsx/db/nsx_models.py @@ -488,3 +488,14 @@ class NsxProjectPluginMapping(model_base.BASEV2, models.TimestampMixin): __tablename__ = 'nsx_project_plugin_mappings' project = sa.Column(sa.String(36), primary_key=True) plugin = sa.Column(sa.Enum('dvs', 'nsx-v', 'nsx-t'), nullable=False) + + +class NsxVpnConnectionMapping(model_base.BASEV2, models.TimestampMixin): + """Stores the mapping between VPNaaS connections and NSX objects""" + __tablename__ = 'neutron_nsx_vpn_connection_mappings' + neutron_id = sa.Column(sa.String(36), primary_key=True) + session_id = sa.Column(sa.String(36), nullable=False) + dpd_profile_id = sa.Column(sa.String(36), nullable=False) + ike_profile_id = sa.Column(sa.String(36), nullable=False) + ipsec_profile_id = sa.Column(sa.String(36), nullable=False) + peer_ep_id = sa.Column(sa.String(36), nullable=False) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 1357056622..6dc1ff6495 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -24,6 +24,8 @@ from neutron_lib.api.validators import availability_zone as az_validator from neutron_lib.exceptions import allowedaddresspairs as addr_exc from neutron_lib.exceptions import l3 as l3_exc from neutron_lib.exceptions import port_security as psec_exc +from neutron_lib.plugins import constants as plugin_const +from neutron_lib.plugins import directory from neutron_lib.services.qos import constants as qos_consts from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api @@ -3125,7 +3127,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return (ipaddress, netmask, nexthop) - def _get_tier0_uuid_by_net(self, context, network_id): + def _get_tier0_uuid_by_router(self, context, router): + network_id = router.gw_port_id and router.gw_port.network_id if not network_id: return network = self.get_network(context, network_id) @@ -3136,10 +3139,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, def _update_router_gw_info(self, context, router_id, info): router = self._get_router(context, router_id) - org_ext_net_id = router.gw_port_id and router.gw_port.network_id - org_tier0_uuid = self._get_tier0_uuid_by_net(context, org_ext_net_id) + org_tier0_uuid = self._get_tier0_uuid_by_router(context, router) org_enable_snat = router.enable_snat - new_ext_net_id = info and info.get('network_id') orgaddr, orgmask, _orgnexthop = ( self._get_external_attachment_info( context, router)) @@ -3159,8 +3160,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, super(NsxV3Plugin, self)._update_router_gw_info( context, router_id, info, router=router) - new_ext_net_id = router.gw_port_id and router.gw_port.network_id - new_tier0_uuid = self._get_tier0_uuid_by_net(context, new_ext_net_id) + new_tier0_uuid = self._get_tier0_uuid_by_router(context, router) new_enable_snat = router.enable_snat newaddr, newmask, _newnexthop = ( self._get_external_attachment_info( @@ -3600,6 +3600,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, (but not both) and include the source/dest nsx logical port. """ extra_rules = [] + # DHCP relay rules: # get the list of relevant relay servers elv_ctx = context.elevated() @@ -3651,6 +3652,16 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, 'services': dhcp_services, 'direction': 'OUT'}) + # VPN rules: + vpn_plugin = directory.get_plugin(plugin_const.VPN) + if vpn_plugin: + vpn_driver = vpn_plugin.drivers[vpn_plugin.default_provider] + vpn_rules = ( + vpn_driver._generate_ipsecvpn_firewall_rules( + context, router_id)) + if vpn_rules: + extra_rules.extend(vpn_rules) + return extra_rules def _get_ports_and_address_groups(self, context, router_id, network_id, diff --git a/vmware_nsx/services/vpnaas/nsxv3/__init__.py b/vmware_nsx/services/vpnaas/nsxv3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py b/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py new file mode 100644 index 0000000000..61aa98d7d6 --- /dev/null +++ b/vmware_nsx/services/vpnaas/nsxv3/ipsec_driver.py @@ -0,0 +1,645 @@ +# Copyright 2017 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. + +import netaddr +from oslo_log import log as logging +from oslo_utils import excutils + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants +from neutron_lib.plugins import directory +from neutron_vpnaas.services.vpn import service_drivers + +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.db import db +from vmware_nsx.services.vpnaas.nsxv3 import ipsec_utils +from vmware_nsx.services.vpnaas.nsxv3 import ipsec_validator +from vmware_nsxlib.v3 import exceptions as nsx_lib_exc +from vmware_nsxlib.v3 import nsx_constants as consts +from vmware_nsxlib.v3 import vpn_ipsec + +LOG = logging.getLogger(__name__) +IPSEC = 'ipsec' + + +class NSXv3IPsecVpnDriver(service_drivers.VpnDriver): + + def __init__(self, service_plugin): + self.vpn_plugin = service_plugin + self._core_plugin = directory.get_plugin() + self._nsxlib = self._core_plugin.nsxlib + self._nsx_vpn = self._nsxlib.vpn_ipsec + validator = ipsec_validator.IPsecV3Validator(service_plugin) + super(NSXv3IPsecVpnDriver, self).__init__(service_plugin, validator) + + registry.subscribe( + self._delete_local_endpoint, resources.ROUTER_GATEWAY, + events.AFTER_DELETE) + + @property + def l3_plugin(self): + return self._core_plugin + + @property + def service_type(self): + return IPSEC + + def _translate_cidr(self, cidr): + return self._nsxlib.firewall_section.get_ip_cidr_reference( + cidr, + consts.IPV6 if netaddr.valid_ipv6(cidr) else consts.IPV4) + + def _translate_addresses_to_target(self, cidrs): + return [self._translate_cidr(ip) for ip in cidrs] + + def _generate_ipsecvpn_firewall_rules(self, context, router_id): + """Return the firewall rules needed to allow vpn traffic""" + fw_rules = [] + # get all the active services of this router + filters = {'router_id': [router_id], + 'status': [constants.ACTIVE]} + services = self.vpn_plugin.get_vpnservices( + context.elevated(), filters=filters) + if not services: + return fw_rules + for srv in services: + subnet = self.l3_plugin.get_subnet( + context.elevated(), srv['subnet_id']) + local_cidrs = [subnet['cidr']] + # get all the active connections of this service + filters = {'vpnservice_id': [srv['id']], + 'status': [constants.ACTIVE]} + connections = self.vpn_plugin.get_ipsec_site_connections( + context.elevated(), filters=filters) + for conn in connections: + peer_cidrs = conn['peer_cidrs'] + fw_rules.append({ + 'display_name': 'VPN connection ' + conn['id'], + 'action': consts.FW_ACTION_ALLOW, + 'sources': self._translate_addresses_to_target( + peer_cidrs), + 'destinations': self._translate_addresses_to_target( + local_cidrs)}) + + return fw_rules + + def _update_firewall_rules(self, context, vpnservice): + LOG.debug("Updating vpn firewall rules for router %s", + vpnservice['router_id']) + self._core_plugin.update_router_firewall( + context, vpnservice['router_id']) + + def _update_router_advertisement(self, context, vpnservice): + LOG.debug("Updating router advertisement rules for router %s", + vpnservice['router_id']) + + router_id = vpnservice['router_id'] + # skip no-snat router as it is already advertised, + # and router with no gw + rtr = self.l3_plugin.get_router(context, router_id) + if (not rtr.get('external_gateway_info') or + not rtr['external_gateway_info'].get('enable_snat', True)): + return + + rules = [] + + # get all the active services of this router + filters = {'router_id': [router_id], 'status': [constants.ACTIVE]} + services = self.vpn_plugin.get_vpnservices( + context.elevated(), filters=filters) + for srv in services: + # use only services with active connections + filters = {'vpnservice_id': [srv['id']], + 'status': [constants.ACTIVE]} + connections = self.vpn_plugin.get_ipsec_site_connections( + context.elevated(), filters=filters) + if not connections: + continue + subnet = self.l3_plugin.get_subnet( + context.elevated(), srv['subnet_id']) + rules.append({ + 'display_name': 'VPN advertisement service ' + srv['id'], + 'action': consts.FW_ACTION_ALLOW, + 'networks': [subnet['cidr']]}) + + logical_router_id = db.get_nsx_router_id(context.session, router_id) + self._nsxlib.logical_router.update_advertisement_rules( + logical_router_id, rules) + + def _update_status(self, context, vpn_service_id, ipsec_site_conn_id, + status, updated_pending_status=True): + ipsec_site_conn = {'status': status, + 'updated_pending_status': updated_pending_status} + vpn_status = {'id': vpn_service_id, + 'updated_pending_status': updated_pending_status, + 'status': status, + 'ipsec_site_connections': {ipsec_site_conn_id: + ipsec_site_conn}} + status_list = [vpn_status] + self.service_plugin.update_status_by_agent(context, status_list) + + def _nsx_tags(self, context, connection): + return self._nsxlib.build_v3_tags_payload( + connection, resource_type='os-vpn-connection-id', + project_name=context.tenant_name) + + def _nsx_tags_for_reused(self): + # Service & Local endpoint can be reused cross tenants, + # so we do not add the tenant/object id. + return self._nsxlib.build_v3_api_version_tag() + + def _create_ike_profile(self, context, connection): + """Create an ike profile for a connection""" + # Note(asarfaty) the NSX profile can be reused, so we can consider + # creating it only once in the future, and keeping a use-count for it. + # There is no driver callback for profiles creation so it has to be + # done on connection creation. + ike_policy_id = connection['ikepolicy_id'] + ikepolicy = self.vpn_plugin.get_ikepolicy(context, ike_policy_id) + try: + profile = self._nsx_vpn.ike_profile.create( + ikepolicy['name'], + description=ikepolicy['description'], + encryption_algorithm=ipsec_utils.ENCRYPTION_ALGORITHM_MAP[ + ikepolicy['encryption_algorithm']], + digest_algorithm=ipsec_utils.AUTH_ALGORITHM_MAP[ + ikepolicy['auth_algorithm']], + ike_version=ipsec_utils.IKE_VERSION_MAP[ + ikepolicy['ike_version']], + dh_group=ipsec_utils.PFS_MAP[ikepolicy['pfs']], + pfs=True, + sa_life_time=ikepolicy['lifetime']['value'], + tags=self._nsx_tags(context, connection)) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create an ike profile: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + return profile['id'] + + def _delete_ike_profile(self, ikeprofile_id): + self._nsx_vpn.ike_profile.delete(ikeprofile_id) + + def _create_ipsec_profile(self, context, connection): + """Create an ipsec profile for a connection""" + # Note(asarfaty) the NSX profile can be reused, so we can consider + # creating it only once in the future, and keeping a use-count for it. + # There is no driver callback for profiles creation so it has to be + # done on connection creation. + ipsec_policy_id = connection['ipsecpolicy_id'] + ipsecpolicy = self.vpn_plugin.get_ipsecpolicy( + context, ipsec_policy_id) + + try: + profile = self._nsx_vpn.tunnel_profile.create( + ipsecpolicy['name'], + description=ipsecpolicy['description'], + encryption_algorithm=ipsec_utils.ENCRYPTION_ALGORITHM_MAP[ + ipsecpolicy['encryption_algorithm']], + digest_algorithm=ipsec_utils.AUTH_ALGORITHM_MAP[ + ipsecpolicy['auth_algorithm']], + dh_group=ipsec_utils.PFS_MAP[ipsecpolicy['pfs']], + pfs=True, + sa_life_time=ipsecpolicy['lifetime']['value'], + tags=self._nsx_tags(context, connection)) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create a tunnel profile: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + return profile['id'] + + def _delete_ipsec_profile(self, ipsecprofile_id): + self._nsx_vpn.tunnel_profile.delete(ipsecprofile_id) + + def _create_dpd_profile(self, context, connection): + dpd_info = connection['dpd'] + try: + profile = self._nsx_vpn.dpd_profile.create( + connection['name'][:240] + '-dpd-profile', + description='neutron dpd profile', + timeout=dpd_info.get('timeout'), + enabled=True if dpd_info.get('action') == 'hold' else False, + tags=self._nsx_tags(context, connection)) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create a DPD profile: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + + return profile['id'] + + def _delete_dpd_profile(self, dpdprofile_id): + self._nsx_vpn.dpd_profile.delete(dpdprofile_id) + + def _update_dpd_profile(self, connection, dpdprofile_id): + dpd_info = connection['dpd'] + self._nsx_vpn.dpd_profile.update(dpdprofile_id, + timeout=dpd_info.get('timeout'), + enabled=True if dpd_info.get('action') == 'hold' else False) + + def _create_peer_endpoint(self, context, connection, ikeprofile_id, + ipsecprofile_id, dpdprofile_id): + default_auth = vpn_ipsec.AuthenticationModeTypes.AUTH_MODE_PSK + try: + peer_endpoint = self._nsx_vpn.peer_endpoint.create( + connection['name'], + connection['peer_address'], + connection['peer_id'], + description=connection['description'], + authentication_mode=default_auth, + dpd_profile_id=dpdprofile_id, + ike_profile_id=ikeprofile_id, + ipsec_tunnel_profile_id=ipsecprofile_id, + connection_initiation_mode=ipsec_utils.INITIATION_MODE_MAP[ + connection['initiator']], + psk=connection['psk'], + tags=self._nsx_tags(context, connection)) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create a peer endpoint: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + + return peer_endpoint['id'] + + def _update_peer_endpoint(self, peer_ep_id, connection): + self._nsx_vpn.peer_endpoint.update( + peer_ep_id, + name=connection['name'], + peer_address=connection['peer_address'], + peer_id=connection['peer_id'], + description=connection['description'], + connection_initiation_mode=ipsec_utils.INITIATION_MODE_MAP[ + connection['initiator']], + psk=connection['psk']) + + def _delete_peer_endpoint(self, peer_ep_id): + self._nsx_vpn.peer_endpoint.delete(peer_ep_id) + + def _get_profiles_from_peer_endpoint(self, peer_ep_id): + peer_ep = self._nsx_vpn.peer_endpoint.get(peer_ep_id) + return ( + peer_ep['ike_profile_id'], + peer_ep['ipsec_tunnel_profile_id'], + peer_ep['dpd_profile_id']) + + def _create_local_endpoint(self, context, local_addr, nsx_service_id, + router_id): + """Creating an NSX local endpoint for a logical router + + This endpoint can be reused by other connections, and will be deleted + when the router is deleted or gateway is removed + """ + # Add the neutron router-id to the tags to help search later + tags = self._nsxlib.build_v3_tags_payload( + {'id': router_id}, resource_type='os-neutron-router-id', + project_name=context.tenant_name) + + try: + local_endpoint = self._nsx_vpn.local_endpoint.create( + 'Local endpoint for OS VPNaaS', + local_addr, + nsx_service_id, + tags=tags) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create a local endpoint: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + + return local_endpoint['id'] + + def _search_local_endpint(self, router_id): + tags = [{'scope': 'os-neutron-router-id', 'tag': router_id}] + ep_list = self._nsxlib.search_by_tags( + tags=tags, + resource_type=self._nsx_vpn.local_endpoint.resource_type) + if ep_list['results']: + return ep_list['results'][0]['id'] + + def _get_local_endpoint(self, context, connection, vpnservice): + """Get the id of the local endpoint for a service + + The NSX allows only one local endpoint per local address + This method will create it if there is not matching endpoint + """ + # use the router GW as the local ip + router_id = vpnservice['router']['id'] + + # check if we already have this endpoint on the NSX + local_ep_id = self._search_local_endpint(router_id) + if local_ep_id: + return local_ep_id + + # create a new one + local_addr = self._get_router_ext_gw(context, router_id) + nsx_service_id = self._get_nsx_vpn_service(context, vpnservice) + local_ep_id = self._create_local_endpoint( + context, local_addr, nsx_service_id, router_id) + return local_ep_id + + def _delete_local_endpoint(self, resource, event, trigger, **kwargs): + """Upon router deletion / gw removal delete the matching endpoint""" + router_id = kwargs.get('router_id') + local_ep_id = self._search_local_endpint(router_id) + if local_ep_id: + self._nsx_vpn.local_endpoint.delete(local_ep_id) + + def _get_session_rules(self, context, connection, vpnservice): + # TODO(asarfaty): support vpn-endpoint-groups too + peer_cidrs = connection['peer_cidrs'] + local_cidrs = [vpnservice['subnet']['cidr']] + rule = self._nsx_vpn.session.get_rule_obj(local_cidrs, peer_cidrs) + return [rule] + + def _create_session(self, context, connection, local_ep_id, + peer_ep_id, rules): + try: + session = self._nsx_vpn.session.create( + connection['name'], local_ep_id, peer_ep_id, rules, + description=connection['description'], + tags=self._nsx_tags(context, connection)) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create a session: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + + return session['id'] + + def _update_session(self, session_id, connection, rules): + self._nsx_vpn.session.update( + session_id, + name=connection['name'], + description=connection['description'], + policy_rules=rules) + + def _delete_session(self, session_id): + self._nsx_vpn.session.delete(session_id) + + def create_ipsec_site_connection(self, context, ipsec_site_conn): + LOG.debug('Creating ipsec site connection %(conn_info)s.', + {"conn_info": ipsec_site_conn}) + # Note(asarfaty) the plugin already calls the validator + # which also validated the policies and service + + ikeprofile_id = None + ipsecprofile_id = None + dpdprofile_id = None + peer_ep_id = None + session_id = None + vpnservice_id = ipsec_site_conn['vpnservice_id'] + vpnservice = self.service_plugin._get_vpnservice( + context, vpnservice_id) + ipsec_id = ipsec_site_conn["id"] + + try: + # create the ike profile + ikeprofile_id = self._create_ike_profile( + context, ipsec_site_conn) + LOG.debug("Created NSX ike profile %s", ikeprofile_id) + + # create the ipsec profile + ipsecprofile_id = self._create_ipsec_profile( + context, ipsec_site_conn) + LOG.debug("Created NSX ipsec profile %s", ipsecprofile_id) + + # create the dpd profile + dpdprofile_id = self._create_dpd_profile( + context, ipsec_site_conn) + LOG.debug("Created NSX dpd profile %s", dpdprofile_id) + + # create the peer endpoint and add to the DB + peer_ep_id = self._create_peer_endpoint( + context, ipsec_site_conn, + ikeprofile_id, ipsecprofile_id, dpdprofile_id) + LOG.debug("Created NSX peer endpoint %s", peer_ep_id) + + # create or reuse a local endpoint using the vpn service + local_ep_id = self._get_local_endpoint( + context, ipsec_site_conn, vpnservice) + + # Finally: create the session with policy rules + rules = self._get_session_rules( + context, ipsec_site_conn, vpnservice) + session_id = self._create_session( + context, ipsec_site_conn, local_ep_id, peer_ep_id, rules) + + # update the DB with the session id + db.add_nsx_vpn_connection_mapping( + context.session, ipsec_site_conn['id'], session_id, + dpdprofile_id, ikeprofile_id, ipsecprofile_id, peer_ep_id) + + self._update_status(context, vpnservice_id, ipsec_id, + constants.ACTIVE) + + except nsx_exc.NsxPluginException: + with excutils.save_and_reraise_exception(): + self._update_status(context, vpnservice_id, ipsec_id, + constants.ERROR) + + # delete the NSX objects that were already created + # Do not delete reused objects: service, local endpoint + if session_id: + self._delete_session(session_id) + if peer_ep_id: + self._delete_peer_endpoint(peer_ep_id) + if dpdprofile_id: + self._delete_dpd_profile(dpdprofile_id) + if ipsecprofile_id: + self._delete_ipsec_profile(ipsecprofile_id) + if ikeprofile_id: + self._delete_ike_profile(ikeprofile_id) + + # update router firewall rules + self._update_firewall_rules(context, vpnservice) + + # update router advertisement rules + self._update_router_advertisement(context, vpnservice) + + def delete_ipsec_site_connection(self, context, ipsec_site_conn): + LOG.debug('Deleting ipsec site connection %(site)s.', + {"site": ipsec_site_conn}) + + vpnservice_id = ipsec_site_conn['vpnservice_id'] + vpnservice = self.service_plugin._get_vpnservice( + context, vpnservice_id) + + # get all data from the nsx based on the connection id in the DB + mapping = db.get_nsx_vpn_connection_mapping( + context.session, ipsec_site_conn['id']) + if not mapping: + LOG.warning("Couldn't find nsx ids for VPN connection %s", + ipsec_site_conn['id']) + # Do not fail the deletion + return + + if mapping['session_id']: + self._delete_session(mapping['session_id']) + if mapping['peer_ep_id']: + self._delete_peer_endpoint(mapping['peer_ep_id']) + if mapping['dpd_profile_id']: + self._delete_dpd_profile(mapping['dpd_profile_id']) + if mapping['ipsec_profile_id']: + self._delete_ipsec_profile(mapping['ipsec_profile_id']) + if mapping['ike_profile_id']: + self._delete_ike_profile(mapping['ike_profile_id']) + + # Do not delete the local endpoint and service as they are reused + db.delete_nsx_vpn_connection_mapping(context.session, + ipsec_site_conn['id']) + # update router firewall rules + self._update_firewall_rules(context, vpnservice) + + # update router advertisement rules + self._update_router_advertisement(context, vpnservice) + + def update_ipsec_site_connection(self, context, old_ipsec_conn, + ipsec_site_conn): + LOG.debug('Updating ipsec site connection new %(site)s.', + {"site": ipsec_site_conn}) + LOG.debug('Updating ipsec site connection old %(site)s.', + {"site": old_ipsec_conn}) + + # Note(asarfaty) the plugin already calls the validator + # which also validated the policies and service + + ipsec_id = old_ipsec_conn['id'] + vpnservice_id = old_ipsec_conn['vpnservice_id'] + vpnservice = self.service_plugin._get_vpnservice( + context, vpnservice_id) + mapping = db.get_nsx_vpn_connection_mapping( + context.session, ipsec_site_conn['id']) + if not mapping: + LOG.error("Couldn't find nsx ids for VPN connection %s", + ipsec_site_conn['id']) + self._update_status(context, vpnservice_id, ipsec_id, "ERROR") + raise nsx_exc.NsxIPsecVpnMappingNotFound(conn=ipsec_id) + + update_all = (old_ipsec_conn['name'] != ipsec_site_conn['name'] or + old_ipsec_conn['description'] != + ipsec_site_conn['description']) + # check if the dpd configuration changed + old_dpd = old_ipsec_conn['dpd'] + new_dpd = ipsec_site_conn['dpd'] + if (old_dpd['action'] != new_dpd['action'] or + old_dpd['timeout'] != new_dpd['timeout'] or + update_all): + self._update_dpd_profile(ipsec_site_conn, + mapping['dpd_profile_id']) + + # update peer endpoint with all the parameters that could be modified + # Note(asarfaty): local endpoints are reusable and will not be updated + self._update_peer_endpoint(mapping['peer_ep_id'], ipsec_site_conn) + rules = self._get_session_rules( + context, ipsec_site_conn, vpnservice) + self._update_session(mapping['session_id'], ipsec_site_conn, rules) + + if 'peer_cidrs' in ipsec_site_conn: + # Update firewall + self._update_firewall_rules(context, vpnservice) + + # No service updates. No need to update router advertisement rules + + def _get_gateway_ips(self, router): + """Obtain the IPv4 and/or IPv6 GW IP for the router. + + If there are multiples, (arbitrarily) use the first one. + """ + v4_ip = v6_ip = None + for fixed_ip in router.gw_port['fixed_ips']: + addr = fixed_ip['ip_address'] + vers = netaddr.IPAddress(addr).version + if vers == 4: + if v4_ip is None: + v4_ip = addr + elif v6_ip is None: + v6_ip = addr + return v4_ip, v6_ip + + def _create_vpn_service(self, tier0_uuid): + try: + service = self._nsx_vpn.service.create( + 'Neutron VPN service for router ' + tier0_uuid, + tier0_uuid, + enabled=True, + ike_log_level=ipsec_utils.DEFAULT_LOG_LEVEL, + tags=self._nsx_tags_for_reused()) + except nsx_lib_exc.ManagerError as e: + msg = _("Failed to create vpn service: %s") % e + raise nsx_exc.NsxPluginException(err_msg=msg) + + return service['id'] + + def _get_tier0_uuid(self, context, router_id): + router_db = self._core_plugin._get_router(context, router_id) + return self._core_plugin._get_tier0_uuid_by_router(context, router_db) + + def _get_router_ext_gw(self, context, router_id): + router_db = self._core_plugin.get_router(context, router_id) + gw = router_db['external_gateway_info'] + return gw['external_fixed_ips'][0]["ip_address"] + + def _find_vpn_service(self, tier0_uuid): + # find the service for the tier0 router in the NSX. + # Note(asarfaty) we expect only a small number of services + services = self._nsx_vpn.service.list()['results'] + for srv in services: + if srv['logical_router_id']['target_id'] == tier0_uuid: + # if it exists but disabled: issue an error + if not srv.get('enabled', True): + msg = _("NSX vpn service %s must be enabled") % srv['id'] + raise nsx_exc.NsxPluginException(err_msg=msg) + return srv['id'] + + def _create_vpn_service_if_needed(self, context, vpnservice): + # The service is created on the TIER0 router attached to the router GW + # The NSX can keep only one service per tier0 router so we reuse it + router_id = vpnservice['router_id'] + tier0_uuid = self._get_tier0_uuid(context, router_id) + if self._find_vpn_service(tier0_uuid): + return + + # create a new one + self._create_vpn_service(tier0_uuid) + + def _get_nsx_vpn_service(self, context, vpnservice): + router_id = vpnservice['router_id'] + tier0_uuid = self._get_tier0_uuid(context, router_id) + return self._find_vpn_service(tier0_uuid) + + def create_vpnservice(self, context, vpnservice): + #TODO(asarfaty) support vpn-endpoint-group-create for local & peer + # cidrs too + LOG.debug('Creating VPN service %(vpn)s', {'vpn': vpnservice}) + vpnservice_id = vpnservice['id'] + + try: + self.validator.validate_vpnservice(context, vpnservice) + except Exception: + with excutils.save_and_reraise_exception(): + # Rolling back change on the neutron + self.service_plugin.delete_vpnservice(context, vpnservice_id) + + vpnservice = self.service_plugin._get_vpnservice(context, + vpnservice_id) + v4_ip, v6_ip = self._get_gateway_ips(vpnservice.router) + if v4_ip: + vpnservice['external_v4_ip'] = v4_ip + if v6_ip: + vpnservice['external_v6_ip'] = v6_ip + self.service_plugin.set_external_tunnel_ips(context, + vpnservice_id, + v4_ip=v4_ip, v6_ip=v6_ip) + self._create_vpn_service_if_needed(context, vpnservice) + + def update_vpnservice(self, context, old_vpnservice, vpnservice): + # No meaningful field can change here + pass + + def delete_vpnservice(self, context, vpnservice): + # Do not delete the NSX service or DB entry as those will be reused. + pass diff --git a/vmware_nsx/services/vpnaas/nsxv3/ipsec_utils.py b/vmware_nsx/services/vpnaas/nsxv3/ipsec_utils.py new file mode 100644 index 0000000000..7d89c994b8 --- /dev/null +++ b/vmware_nsx/services/vpnaas/nsxv3/ipsec_utils.py @@ -0,0 +1,59 @@ +# Copyright 2017 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 vmware_nsxlib.v3 import vpn_ipsec + +ENCRYPTION_ALGORITHM_MAP = { + 'aes-128': vpn_ipsec.EncryptionAlgorithmTypes.ENCRYPTION_ALGORITHM_128, + 'aes-256': vpn_ipsec.EncryptionAlgorithmTypes.ENCRYPTION_ALGORITHM_256, +} + +AUTH_ALGORITHM_MAP = { + 'sha1': vpn_ipsec.DigestAlgorithmTypes.DIGEST_ALGORITHM_SHA1, + 'sha256': vpn_ipsec.DigestAlgorithmTypes.DIGEST_ALGORITHM_SHA256, +} + +PFS_MAP = { + 'group2': vpn_ipsec.DHGroupTypes.DH_GROUP_2, + 'group5': vpn_ipsec.DHGroupTypes.DH_GROUP_5, + 'group14': vpn_ipsec.DHGroupTypes.DH_GROUP_14 +} + +IKE_VERSION_MAP = { + 'v1': vpn_ipsec.IkeVersionTypes.IKE_VERSION_V1, + 'v2': vpn_ipsec.IkeVersionTypes.IKE_VERSION_V2, +} + +ENCAPSULATION_MODE_MAP = { + 'tunnel': vpn_ipsec.EncapsulationModeTypes.ENCAPSULATION_MODE_TUNNEL +} + +TRANSFORM_PROTOCOL_MAP = { + 'esp': vpn_ipsec.TransformProtocolTypes.TRANSFORM_PROTOCOL_ESP +} + +DPD_ACTION_MAP = { + 'hold': vpn_ipsec.DpdProfileActionTypes.DPD_PROFILE_ACTION_HOLD, + 'disabled': None +} + +INITIATION_MODE_MAP = { + 'bi-directional': (vpn_ipsec.ConnectionInitiationModeTypes. + INITIATION_MODE_INITIATOR), + 'response-only': (vpn_ipsec.ConnectionInitiationModeTypes. + INITIATION_MODE_RESPOND_ONLY) +} + +DEFAULT_LOG_LEVEL = vpn_ipsec.IkeLogLevelTypes.LOG_LEVEL_ERROR diff --git a/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py b/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py new file mode 100644 index 0000000000..f318af4314 --- /dev/null +++ b/vmware_nsx/services/vpnaas/nsxv3/ipsec_validator.py @@ -0,0 +1,374 @@ +# Copyright 2017 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. + +import netaddr +from oslo_log import log as logging + +from neutron_lib import constants +from neutron_vpnaas.db.vpn import vpn_validator + +from vmware_nsx._i18n import _ +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.services.vpnaas.nsxv3 import ipsec_utils +from vmware_nsxlib.v3 import nsx_constants as consts +from vmware_nsxlib.v3 import vpn_ipsec + +LOG = logging.getLogger(__name__) + + +class IPsecV3Validator(vpn_validator.VpnReferenceValidator): + + """Validator methods for Vmware NSX-V3 VPN support""" + def __init__(self, service_plugin): + super(IPsecV3Validator, self).__init__() + self.vpn_plugin = service_plugin + self.nsxlib = self.core_plugin.nsxlib + self.check_backend_version() + + def check_backend_version(self): + if not self.nsxlib.feature_supported(consts.FEATURE_IPSEC_VPN): + # ipsec vpn is not supported + LOG.warning("VPNaaS is not supported by the NSX backend (version " + "%s)", + self.nsxlib.get_version()) + self.backend_support = False + else: + self.backend_support = True + + def _validate_backend_version(self): + if not self.backend_support: + msg = (_("VPNaaS is not supported by the NSX backend " + "(version %s)") % self.nsxlib.get_version()) + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _validate_policy_lifetime(self, policy_info, policy_type): + """NSX supports only units=seconds""" + lifetime = policy_info.get('lifetime') + if not lifetime: + return + if lifetime.get('units') != 'seconds': + msg = _("Unsupported policy lifetime %(val)s in %(pol)s policy. " + "Only seconds lifetime is supported.") % { + 'val': lifetime, 'pol': policy_type} + raise nsx_exc.NsxVpnValidationError(details=msg) + value = lifetime.get('value') + if (value and (value < vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MIN or + value > vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MAX)): + msg = _("Unsupported policy lifetime %(value)s in %(pol)s policy. " + "Value range is [%(min)s-%(max)s].") % { + 'value': value, + 'pol': policy_type, + 'min': vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MIN, + 'max': vpn_ipsec.SALifetimeLimits.SA_LIFETIME_MAX} + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _validate_policy_auth_algorithm(self, policy_info, policy_type): + """NSX supports only SHA1 and SHA256""" + auth = policy_info.get('auth_algorithm') + if auth and auth not in ipsec_utils.AUTH_ALGORITHM_MAP: + msg = _("Unsupported auth_algorithm: %(algo)s in %(pol)s policy. " + "Please select one of the following supported algorithms: " + "%(supported_algos)s") % { + 'pol': policy_type, + 'algo': auth, + 'supported_algos': + ipsec_utils.AUTH_ALGORITHM_MAP.keys()} + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _validate_policy_encryption_algorithm(self, policy_info, policy_type): + encryption = policy_info.get('encryption_algorithm') + if (encryption and + encryption not in ipsec_utils.ENCRYPTION_ALGORITHM_MAP): + msg = _("Unsupported encryption_algorithm: %(algo)s in %(pol)s " + "policy. Please select one of the following supported " + "algorithms: %(supported_algos)s") % { + 'algo': encryption, + 'pol': policy_type, + 'supported_algos': + ipsec_utils.ENCRYPTION_ALGORITHM_MAP.keys()} + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _validate_policy_pfs(self, policy_info, policy_type): + pfs = policy_info.get('pfs') + if pfs and pfs not in ipsec_utils.PFS_MAP: + msg = _("Unsupported pfs: %(pfs)s in %(pol)s policy. Please " + "select one of the following pfs: " + "%(supported_pfs)s") % { + 'pfs': pfs, + 'pol': policy_type, + 'supported_pfs': + ipsec_utils.PFS_MAP.keys()} + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _validate_dpd(self, connection): + dpd_info = connection.get('dpd') + if not dpd_info: + return + action = dpd_info.get('action') + if action not in ipsec_utils.DPD_ACTION_MAP.keys(): + msg = _("Unsupported DPD action: %(action)s! Currently only " + "%(supported)s is supported.") % { + 'action': action, + 'supported': ipsec_utils.DPD_ACTION_MAP.keys()} + raise nsx_exc.NsxVpnValidationError(details=msg) + timeout = dpd_info.get('timeout') + if (timeout < vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MIN or + timeout > vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MAX): + msg = _("Unsupported DPD timeout: %(timeout)s. Value range is " + "[%(min)s-%(max)s].") % { + 'timeout': timeout, + 'min': vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MIN, + 'max': vpn_ipsec.DpdProfileTimeoutLimits.DPD_TIMEOUT_MAX} + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _validate_psk(self, connection): + if 'psk' in connection and not connection['psk']: + msg = _("'psk' cannot be empty or null when authentication " + "mode is psk") + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _check_policy_rules_overlap(self, context, ipsec_site_conn): + """validate no overlapping policy rules + + The nsx does not support overlapping policy rules cross + all tenants, and tier0 routers + """ + connections = self.vpn_plugin.get_ipsec_site_connections( + context.elevated()) + if not connections: + return + vpnservice_id = ipsec_site_conn.get('vpnservice_id') + vpnservice = self.vpn_plugin._get_vpnservice(context, vpnservice_id) + local_cidrs = [vpnservice['subnet']['cidr']] + peer_cidrs = ipsec_site_conn['peer_cidrs'] + for conn in connections: + # skip this connection and connections in non active state + if (conn['id'] == ipsec_site_conn.get('id') or + conn['status'] != constants.ACTIVE): + continue + # TODO(asarfaty): support peer groups too + # check if it overlaps with the peer cidrs + conn_peer_cidrs = conn['peer_cidrs'] + if netaddr.IPSet(conn_peer_cidrs) & netaddr.IPSet(peer_cidrs): + # check if the local cidr also overlaps + con_service_id = conn.get('vpnservice_id') + con_service = self.vpn_plugin._get_vpnservice( + context.elevated(), con_service_id) + conn_local_cidr = [con_service['subnet']['cidr']] + if netaddr.IPSet(conn_local_cidr) & netaddr.IPSet(local_cidrs): + msg = (_("Cannot create a connection with overlapping " + "local and peer cidrs (%(local)s and %(peer)s) " + "as connection %(id)s") % {'local': local_cidrs, + 'peer': peer_cidrs, + 'id': conn['id']}) + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _check_unique_addresses(self, context, ipsec_site_conn): + """Validate no repeating local & peer addresses (of all tenants) + + The nsx does not support it cross all tenants, and tier0 routers + """ + vpnservice_id = ipsec_site_conn.get('vpnservice_id') + local_addr = self._get_service_local_address(context, vpnservice_id) + peer_address = ipsec_site_conn.get('peer_address') + filters = {'peer_address': [peer_address]} + connections = self.vpn_plugin.get_ipsec_site_connections( + context.elevated(), filters=filters) + for conn in connections: + # skip this connection and connections in non active state + if (conn['id'] == ipsec_site_conn.get('id') or + conn['status'] != constants.ACTIVE): + continue + # this connection has the same peer addr as ours. + # check the service local address + srv_id = conn.get('vpnservice_id') + srv_local = self._get_service_local_address( + context.elevated(), srv_id) + if srv_local == local_addr: + msg = (_("Cannot create another connection with the same " + "local address %(local)s and peer address %(peer)s " + "as connection %(id)s") % {'local': local_addr, + 'peer': peer_address, + 'id': conn['id']}) + raise nsx_exc.NsxVpnValidationError(details=msg) + + def _check_advertisment_overlap(self, context, ipsec_site_conn): + """Validate there is no overlapping advertisement of networks + + The plugin advertise all no-snat routers networks + vpn local + networks. + The NSX does not allow different Tier1 router to advertise the + same subnets + """ + admin_con = context.elevated() + srv_id = ipsec_site_conn.get('vpnservice_id') + srv = self.vpn_plugin._get_vpnservice(admin_con, srv_id) + this_router = srv['router_id'] + this_cidr = srv['subnet']['cidr'] + + # get all subnets of no-snat routers + all_routers = self.core_plugin.get_routers(admin_con) + nosnat_routers = [rtr for rtr in all_routers + if (rtr['id'] != this_router and + rtr.get('external_gateway_info') and + not rtr['external_gateway_info'].get( + 'enable_snat', True))] + for rtr in nosnat_routers: + if rtr['id'] == this_router: + continue + # go over the subnets of this router + subnets = self.core_plugin._find_router_subnets_cidrs( + admin_con, rtr['id']) + if subnets and netaddr.IPSet(subnets) & netaddr.IPSet([this_cidr]): + msg = (_("Cannot create connection with overlapping local " + "cidrs %(local)s which was already advertised by " + "no-snat router %(rtr)s") % {'local': subnets, + 'rtr': rtr['id']}) + raise nsx_exc.NsxVpnValidationError(details=msg) + + # add all vpn local subnets + connections = self.vpn_plugin.get_ipsec_site_connections(admin_con) + for conn in connections: + # skip this connection and connections in non active state + if (conn['id'] == ipsec_site_conn.get('id') or + conn['status'] != constants.ACTIVE): + continue + # check the service local address + conn_srv_id = conn.get('vpnservice_id') + conn_srv = self.vpn_plugin._get_vpnservice(admin_con, conn_srv_id) + if conn_srv['router_id'] == this_router: + continue + conn_cidr = conn_srv['subnet']['cidr'] + if netaddr.IPSet([conn_cidr]) & netaddr.IPSet([this_cidr]): + msg = (_("Cannot create connection with overlapping local " + "cidr %(local)s which was already advertised by " + "router %(rtr)s and connection %(conn)s") % { + 'local': conn_cidr, + 'rtr': conn_srv['router_id'], + 'conn': conn['id']}) + raise nsx_exc.NsxVpnValidationError(details=msg) + + # TODO(asarfaty): also add this validation when adding an interface + # or no-snat to a router through the nsx-v3 plugin + + def validate_ipsec_site_connection(self, context, ipsec_site_conn): + """Called upon create/update of a connection""" + + self._validate_backend_version() + + self._validate_dpd(ipsec_site_conn) + self._validate_psk(ipsec_site_conn) + + ike_policy_id = ipsec_site_conn.get('ikepolicy_id') + if ike_policy_id: + ikepolicy = self.vpn_plugin.get_ikepolicy(context, + ike_policy_id) + self.validate_ike_policy(context, ikepolicy) + + ipsec_policy_id = ipsec_site_conn.get('ipsecpolicy_id') + if ipsec_policy_id: + ipsecpolicy = self.vpn_plugin.get_ipsecpolicy(context, + ipsec_policy_id) + self.validate_ipsec_policy(context, ipsecpolicy) + + self._check_advertisment_overlap(context, ipsec_site_conn) + self._check_unique_addresses(context, ipsec_site_conn) + self._check_policy_rules_overlap(context, ipsec_site_conn) + + #TODO(asarfaty): IPv6 is not yet supported. add validation + + def _get_service_local_address(self, context, vpnservice_id): + vpnservice = self.vpn_plugin._get_vpnservice(context, + vpnservice_id) + router_id = vpnservice['router_id'] + router_db = self.core_plugin.get_router(context, router_id) + gw = router_db['external_gateway_info'] + return gw['external_fixed_ips'][0]['ip_address'] + + def _validate_router(self, context, router_id): + # Verify that the router gw network is connected to an active-standby + # Tier0 router + router_db = self.core_plugin._get_router(context, router_id) + tier0_uuid = self.core_plugin._get_tier0_uuid_by_router(context, + router_db) + # TODO(asarfaty): cache this result + tier0_router = self.nsxlib.logical_router.get(tier0_uuid) + if (not tier0_router or + tier0_router.get('high_availability_mode') != 'ACTIVE_STANDBY'): + msg = _("The router GW should be connected to a TIER-0 router " + "with ACTIVE_STANDBY HA mode") + raise nsx_exc.NsxVpnValidationError(details=msg) + + def validate_vpnservice(self, context, vpnservice): + """Called upon create/update of a service""" + + self._validate_backend_version() + + # Call general validations + super(IPsecV3Validator, self).validate_vpnservice( + context, vpnservice) + + # Call specific NSX validations + self._validate_router(context, vpnservice['router_id']) + + if not vpnservice['subnet_id']: + # we currently do not support multiple subnets so a subnet must + # be defined + msg = _("Subnet must be defined in a service") + raise nsx_exc.NsxVpnValidationError(details=msg) + + #TODO(asarfaty): IPv6 is not yet supported. add validation + + def validate_ipsec_policy(self, context, ipsec_policy): + # Call general validations + super(IPsecV3Validator, self).validate_ipsec_policy( + context, ipsec_policy) + + # Call specific NSX validations + self._validate_policy_lifetime(ipsec_policy, "IPSec") + self._validate_policy_auth_algorithm(ipsec_policy, "IPSec") + self._validate_policy_encryption_algorithm(ipsec_policy, "IPSec") + self._validate_policy_pfs(ipsec_policy, "IPSec") + + # Ensure IPSec policy encap mode is tunnel + mode = ipsec_policy.get('encapsulation_mode') + if mode and mode not in ipsec_utils.ENCAPSULATION_MODE_MAP.keys(): + msg = _("Unsupported encapsulation mode: %s. Only 'tunnel' mode " + "is supported.") % mode + raise nsx_exc.NsxVpnValidationError(details=msg) + + # Ensure IPSec policy transform protocol is esp + prot = ipsec_policy.get('transform_protocol') + if prot and prot not in ipsec_utils.TRANSFORM_PROTOCOL_MAP.keys(): + msg = _("Unsupported transform protocol: %s. Only 'esp' protocol " + "is supported.") % prot + raise nsx_exc.NsxVpnValidationError(details=msg) + + def validate_ike_policy(self, context, ike_policy): + # Call general validations + super(IPsecV3Validator, self).validate_ike_policy( + context, ike_policy) + + # Call specific NSX validations + self._validate_policy_lifetime(ike_policy, "IKE") + self._validate_policy_auth_algorithm(ike_policy, "IKE") + self._validate_policy_encryption_algorithm(ike_policy, "IKE") + self._validate_policy_pfs(ike_policy, "IKE") + + # 'aggressive' phase1-negotiation-mode is not supported + if ike_policy.get('phase1-negotiation-mode', 'main') != 'main': + msg = _("Unsupported phase1-negotiation-mode: %s! Only 'main' is " + "supported.") % ike_policy['phase1-negotiation-mode'] + raise nsx_exc.NsxVpnValidationError(details=msg) diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index 62ee1208fb..8e747c6eaf 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -197,6 +197,9 @@ class NsxV3PluginTestCaseMixin(test_plugin.NeutronDbPluginV2TestCase, _mock_nsx_backend_calls() self.setup_conf_overrides() self.mock_plugin_methods() + # ignoring the given plugin and use the nsx-v3 one + if not plugin.endswith('NsxTVDPlugin'): + plugin = PLUGIN_NAME super(NsxV3PluginTestCaseMixin, self).setUp(plugin=plugin, ext_mgr=ext_mgr) diff --git a/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py b/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py new file mode 100644 index 0000000000..6bb9aa55bf --- /dev/null +++ b/vmware_nsx/tests/unit/services/vpnaas/test_nsxv3_vpnaas.py @@ -0,0 +1,390 @@ +# Copyright 2013, Nachi Ueno, NTT I3, 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 mock + +from neutron_lib import context as n_ctx +from neutron_vpnaas.tests import base + +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.services.vpnaas.nsxv3 import ipsec_validator + + +class TestDriverValidation(base.BaseTestCase): + + def setUp(self): + super(TestDriverValidation, self).setUp() + self.context = n_ctx.Context('some_user', 'some_tenant') + self.service_plugin = mock.Mock() + driver = mock.Mock() + driver.service_plugin = self.service_plugin + with mock.patch("neutron_lib.plugins.directory.get_plugin"): + self.validator = ipsec_validator.IPsecV3Validator(driver) + self.validator._l3_plugin = mock.Mock() + self.validator._core_plugin = mock.Mock() + + self.vpn_service = {'router_id': 'dummy_router', + 'subnet_id': 'dummy_subnet'} + self.peer_address = '10.10.10.10' + self.peer_cidr = '10.10.11.0/20' + + def _test_lifetime_not_in_seconds(self, validation_func): + policy_info = {'lifetime': {'units': 'kilobytes', 'value': 1000}} + self.assertRaises(nsx_exc.NsxVpnValidationError, + validation_func, + self.context, policy_info) + + def test_ike_lifetime_not_in_seconds(self): + self._test_lifetime_not_in_seconds( + self.validator.validate_ike_policy) + + def test_ipsec_lifetime_not_in_seconds(self): + self._test_lifetime_not_in_seconds( + self.validator.validate_ipsec_policy) + + def _test_lifetime_seconds_values_at_limits(self, validation_func): + policy_info = {'lifetime': {'units': 'seconds', 'value': 90}} + validation_func(self.context, policy_info) + policy_info = {'lifetime': {'units': 'seconds', 'value': 86400}} + validation_func(self.context, policy_info) + + policy_info = {'lifetime': {'units': 'seconds', 'value': 10}} + self.assertRaises(nsx_exc.NsxVpnValidationError, + validation_func, + self.context, policy_info) + + def test_ike_lifetime_seconds_values_at_limits(self): + self._test_lifetime_seconds_values_at_limits( + self.validator.validate_ike_policy) + + def test_ipsec_lifetime_seconds_values_at_limits(self): + self._test_lifetime_seconds_values_at_limits( + self.validator.validate_ipsec_policy) + + def _test_auth_algorithm(self, validation_func): + auth_algorithm = {'auth_algorithm': 'sha384'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + validation_func, + self.context, auth_algorithm) + + auth_algorithm = {'auth_algorithm': 'sha512'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + validation_func, + self.context, auth_algorithm) + + auth_algorithm = {'auth_algorithm': 'sha1'} + validation_func(self.context, auth_algorithm) + + auth_algorithm = {'auth_algorithm': 'sha256'} + validation_func(self.context, auth_algorithm) + + def test_ipsec_auth_algorithm(self): + self._test_auth_algorithm(self.validator.validate_ipsec_policy) + + def test_ike_auth_algorithm(self): + self._test_auth_algorithm(self.validator.validate_ike_policy) + + def _test_encryption_algorithm(self, validation_func): + auth_algorithm = {'encryption_algorithm': 'aes-192'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + validation_func, + self.context, auth_algorithm) + + auth_algorithm = {'encryption_algorithm': 'aes-128'} + validation_func(self.context, auth_algorithm) + + auth_algorithm = {'encryption_algorithm': 'aes-256'} + validation_func(self.context, auth_algorithm) + + def test_ipsec_encryption_algorithm(self): + self._test_encryption_algorithm(self.validator.validate_ipsec_policy) + + def test_ike_encryption_algorithm(self): + self._test_encryption_algorithm(self.validator.validate_ike_policy) + + def test_ike_negotiation_mode(self): + policy_info = {'phase1-negotiation-mode': 'aggressive'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + self.validator.validate_ike_policy, + self.context, policy_info) + + policy_info = {'phase1-negotiation-mode': 'main'} + self.validator.validate_ike_policy(self.context, policy_info) + + def _test_pfs(self, validation_func): + policy_info = {'pfs': 'group15'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + validation_func, + self.context, policy_info) + + policy_info = {'pfs': 'group5'} + validation_func(self.context, policy_info) + + def test_ipsec_pfs(self): + self._test_pfs(self.validator.validate_ipsec_policy) + + def test_ike_pfs(self): + self._test_pfs(self.validator.validate_ike_policy) + + def test_ipsec_encap_mode(self): + policy_info = {'encapsulation_mode': 'transport'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + self.validator.validate_ipsec_policy, + self.context, policy_info) + + policy_info = {'encapsulation_mode': 'tunnel'} + self.validator.validate_ipsec_policy(self.context, policy_info) + + def test_ipsec_transform_protocol(self): + policy_info = {'transform_protocol': 'ah'} + self.assertRaises(nsx_exc.NsxVpnValidationError, + self.validator.validate_ipsec_policy, + self.context, policy_info) + + policy_info = {'transform_protocol': 'esp'} + self.validator.validate_ipsec_policy(self.context, policy_info) + + def test_vpn_service_validation_router(self): + router = {'high_availability_mode': 'ACITVE_ACTIVE'} + with mock.patch.object(self.validator.nsxlib.logical_router, 'get', + return_value=router): + self.assertRaises(nsx_exc.NsxVpnValidationError, + self.validator.validate_vpnservice, + self.context, self.vpn_service) + + router = {'high_availability_mode': 'ACTIVE_STANDBY'} + with mock.patch.object(self.validator.nsxlib.logical_router, 'get', + return_value=router): + self.validator.validate_vpnservice(self.context, self.vpn_service) + + def _test_conn_validation(self, conn_params=None, success=True, + connections=None, service_subnets=None, + router_subnets=None): + if connections is None: + connections = [] + if router_subnets is None: + router_subnets = [] + + def mock_get_router(context, router_id): + return {'id': router_id, + 'external_gateway_info': { + 'external_fixed_ips': [{ + 'ip_address': '1.1.1.%s' % router_id}]}} + + def mock_get_routers(context, filters=None, fields=None): + return [{'id': 'no-snat', + 'external_gateway_info': {'enable_snat': False}}] + + def mock_get_service(context, service_id): + if service_subnets: + # option to give the test a different subnet per service + subnet_cidr = service_subnets[int(service_id) - 1] + else: + subnet_cidr = '5.5.5.0/2%s' % service_id + return {'id': service_id, + 'router_id': service_id, + 'subnet_id': 'dummy_subnet', + 'subnet': {'id': 'dummy_subnet', + 'cidr': subnet_cidr}} + + def mock_get_connections(context, filters=None, fields=None): + if filters and 'peer_address' in filters: + return [conn for conn in connections + if conn['peer_address'] == filters['peer_address'][0]] + else: + return connections + + with mock.patch.object(self.validator.vpn_plugin, '_get_vpnservice', + side_effect=mock_get_service),\ + mock.patch.object(self.validator._core_plugin, 'get_router', + side_effect=mock_get_router),\ + mock.patch.object(self.validator._core_plugin, 'get_routers', + side_effect=mock_get_routers),\ + mock.patch.object(self.validator._core_plugin, + '_find_router_subnets_cidrs', + return_value=router_subnets),\ + mock.patch.object(self.validator.vpn_plugin, + 'get_ipsec_site_connections', + side_effect=mock_get_connections): + ipsec_sitecon = {'id': '1', + 'vpnservice_id': '1', + 'mtu': 1500, + 'peer_address': self.peer_address, + 'peer_cidrs': [self.peer_cidr]} + if conn_params: + ipsec_sitecon.update(conn_params) + if success: + self.validator.validate_ipsec_site_connection( + self.context, ipsec_sitecon) + else: + self.assertRaises( + nsx_exc.NsxVpnValidationError, + self.validator.validate_ipsec_site_connection, + self.context, ipsec_sitecon) + + def test_dpd_validation(self): + params = {'dpd': {'action': 'hold', + 'timeout': 120}} + self._test_conn_validation(conn_params=params, success=True) + + params = {'dpd': {'action': 'clear', + 'timeout': 120}} + self._test_conn_validation(conn_params=params, success=False) + + params = {'dpd': {'action': 'hold', + 'timeout': 5}} + self._test_conn_validation(conn_params=params, success=False) + + def test_check_unique_addresses(self): + # this test runs with non-overlapping local subnets on + # different routers + subnets = ['5.5.5.0/20', '6.6.6.0/20'] + + # same service/router gw & peer address - should fail + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '1', + 'peer_address': self.peer_address, + 'peer_cidrs': [self.peer_cidr]}] + self._test_conn_validation(success=False, + connections=connections, + service_subnets=subnets) + + # different service/router gw - ok + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': self.peer_address, + 'peer_cidrs': ['6.6.6.6']}] + self._test_conn_validation(success=True, + connections=connections, + service_subnets=subnets) + + # different peer address - ok + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '1', + 'peer_address': '7.7.7.1', + 'peer_cidrs': ['7.7.7.7']}] + self._test_conn_validation(success=True, + connections=connections, + service_subnets=subnets) + + # ignoring non-active connections + connections = [{'id': '2', + 'status': 'ERROR', + 'vpnservice_id': '1', + 'peer_address': self.peer_address, + 'peer_cidrs': [self.peer_cidr]}] + self._test_conn_validation(success=True, + connections=connections, + service_subnets=subnets) + + def test_overlapping_rules(self): + # peer-cidr overlapping with new one, same subnet - should fail + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '1', + 'peer_address': '9.9.9.9', + 'peer_cidrs': ['10.10.11.1/19']}] + self._test_conn_validation(success=False, + connections=connections) + + # same peer-cidr, overlapping subnets - should fail + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': '9.9.9.9', + 'peer_cidrs': [self.peer_cidr]}] + self._test_conn_validation(success=False, + connections=connections) + + # non overlapping peer-cidr, same subnet - ok + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '1', + 'peer_address': '7.7.7.1', + 'peer_cidrs': ['7.7.7.7']}] + self._test_conn_validation(success=True, + connections=connections) + + # ignoring non-active connections + connections = [{'id': '2', + 'status': 'ERROR', + 'vpnservice_id': '1', + 'peer_address': '9.9.9.9', + 'peer_cidrs': ['10.10.11.1/19']}] + self._test_conn_validation(success=True, + connections=connections) + + def test_advertisment(self): + # different routers, same subnet - should fail + subnets = ['5.5.5.0/20', '5.5.5.0/20'] + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': self.peer_address, + 'peer_cidrs': ['6.6.6.6']}] + self._test_conn_validation(success=False, + connections=connections, + service_subnets=subnets) + + # different routers, overlapping subnet - should fail + subnets = ['5.5.5.0/20', '5.5.5.0/21'] + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': self.peer_address, + 'peer_cidrs': ['6.6.6.6']}] + self._test_conn_validation(success=False, + connections=connections, + service_subnets=subnets) + + # different routers, non overlapping subnet - ok + subnets = ['5.5.5.0/20', '50.5.5.0/21'] + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': self.peer_address, + 'peer_cidrs': ['6.6.6.6']}] + self._test_conn_validation(success=True, + connections=connections, + service_subnets=subnets) + + # no-snat router with overlapping subnet to the service subnet - fail + subnets = ['5.5.5.0/21', '1.1.1.0/20'] + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': self.peer_address, + 'peer_cidrs': ['6.6.6.6']}] + self._test_conn_validation(success=False, + connections=connections, + router_subnets=subnets) + + # no-snat router with non overlapping subnet to the service subnet - ok + service_subnets = ['5.5.5.0/20', '6.6.6.0/20'] + router_subnets = ['50.5.5.0/21', '1.1.1.0/20'] + connections = [{'id': '2', + 'status': 'ACTIVE', + 'vpnservice_id': '2', + 'peer_address': self.peer_address, + 'peer_cidrs': ['6.6.6.6']}] + self._test_conn_validation(success=True, + connections=connections, + service_subnets=service_subnets, + router_subnets=router_subnets) + + +# TODO(asarfaty): add tests for the driver