
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
375 lines
18 KiB
Python
375 lines
18 KiB
Python
# 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)
|