# Copyright 2016, Yi Jing Zhu, IBM. # Copyright 2023, SysEleven GmbH # 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 typing as ty import abc import netaddr from neutron.db import extraroute_db from neutron.plugins.ml2 import plugin from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants as lib_constants from neutron_lib import context as nctx from neutron_lib.db import api as db_api from neutron_lib import exceptions as n_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.plugins import utils as p_utils from neutron_lib import rpc as n_rpc from oslo_config import cfg from oslo_db import exception as o_exc from oslo_log import log as logging from neutron_vpnaas.db.vpn import vpn_agentschedulers_db as agent_db from neutron_vpnaas.db.vpn import vpn_ext_gw_db as ext_gw from neutron_vpnaas.db.vpn import vpn_models from neutron_vpnaas.extensions import vpnaas from neutron_vpnaas.services.vpn.common import constants as v_constants from neutron_vpnaas.services.vpn.common import topics from neutron_vpnaas.services.vpn import ovn_plugin from neutron_vpnaas.services.vpn.service_drivers import base_ipsec #pylint: disable=ungrouped-imports # Additional import for typechecking. Importing these without typechecking # would resolve in a cyclic dependency if ty.TYPE_CHECKING: from neutron.db import db_base_plugin_v2 as db_plugin #pylint: enable=ungrouped-imports LOG = logging.getLogger(__name__) IPSEC = 'ipsec' BASE_IPSEC_VERSION = '1.0' TRANSIT_NETWORK_PREFIX = 'vpn-transit-network-' TRANSIT_SUBNET_PREFIX = 'vpn-transit-subnet-' TRANSIT_PORT_PREFIX = 'vpn-ns-' VPN_GW_PORT_PREFIX = 'vpn-gw-' VPN_TRANSIT_LIP = '169.254.0.1' VPN_TRANSIT_RIP = '169.254.0.2' VPN_TRANSIT_CIDR = '169.254.0.0/28' HIDDEN_PROJECT_ID = '' class IPsecVpnOvnDriverCallBack(base_ipsec.IPsecVpnDriverCallBack): def __init__(self, driver): super().__init__(driver) self.admin_ctx = nctx.get_admin_context() @property def core_plugin(self) -> 'db_plugin.NeutronDbPluginV2': return self.driver.core_plugin @property def service_plugin(self) -> ext_gw.VPNExtGWPlugin_db: return self.driver.service_plugin def _get_vpn_gateway(self, context: nctx.ContextBase, router_id: str): return self.service_plugin.get_vpn_gw_by_router_id(context, router_id) def get_vpn_transit_network_details(self, context: nctx.ContextBase, router_id: str): vpn_gw = self._get_vpn_gateway(context, router_id) network_id = vpn_gw.gw_port['network_id'] external_network = self.core_plugin.get_network(context, network_id) details = { 'gw_port': vpn_gw.gw_port, 'transit_port': vpn_gw.transit_port, 'transit_gateway_ip': VPN_TRANSIT_LIP, 'external_network': external_network, } return details def get_subnet_info(self, context: nctx.ContextBase, subnet_id: ty.Optional[str] = None): try: return self.core_plugin.get_subnet(context, subnet_id) except n_exc.SubnetNotFound: return None def _get_agent_hosting_vpn_services(self, context: nctx.Context, host): agent = self.service_plugin.get_vpn_agent_on_host(context, host) if not agent: return [] # We're here because a VPN agent asked for the VPN services it's # hosting. This means, the agent is alive. This is a chance to # schedule VPN services of routers that are still unscheduled. if cfg.CONF.vpn_auto_schedule: self.service_plugin.auto_schedule_routers(context, agent) query = context.session.query(vpn_models.VPNService) query = query.join(vpn_models.IPsecSiteConnection) query = query.join(agent_db.RouterVPNAgentBinding, agent_db.RouterVPNAgentBinding.router_id == vpn_models.VPNService.router_id) query = query.filter( agent_db.RouterVPNAgentBinding.vpn_agent_id == agent['id']) return query @registry.has_registry_receivers class BaseOvnIPsecVPNDriver(base_ipsec.BaseIPsecVPNDriver, metaclass=abc.ABCMeta): def __init__(self, service_plugin: ovn_plugin.VPNOVNPlugin): self._l3_plugin: \ ty.Optional[extraroute_db.ExtraRoute_dbonly_mixin] = None self._core_plugin: ty.Optional[plugin.Ml2Plugin] = None self.service_plugin = service_plugin super().__init__(service_plugin) @property def l3_plugin(self) -> extraroute_db.ExtraRoute_dbonly_mixin: if self._l3_plugin is None: self._l3_plugin = directory.get_plugin(plugin_constants.L3) return self._l3_plugin @property def core_plugin(self) -> plugin.Ml2Plugin: if self._core_plugin is None: self._core_plugin = directory.get_plugin() return self._core_plugin @registry.receives(resources.ROUTER, [events.PRECOMMIT_UPDATE]) def _handle_router_precommit_update(self, resource, event, trigger, payload): """Check that a router update won't remove routes we need for VPN.""" LOG.debug("Router %s PRECOMMIT_UPDATE event: %s", payload.resource_id, payload.request_body) router_id = payload.resource_id context = payload.context router_data = payload.request_body routes_removed = router_data.get('routes_removed') if not routes_removed: return removed_cidrs = {r['destination'] for r in routes_removed} vpn_cidrs = set( self.service_plugin.get_peer_cidrs_for_router(context, router_id)) conflict_cidrs = removed_cidrs.intersection(vpn_cidrs) if conflict_cidrs: raise vpnaas.RouteInUseByVPN( destinations=", ".join(conflict_cidrs)) @abc.abstractmethod def create_rpc_conn(self) -> None: pass def get_vpn_gw_port_name(self, router_id: str) -> str: return VPN_GW_PORT_PREFIX + router_id def get_vpn_namespace_port_name(self, router_id: str) -> str: return TRANSIT_PORT_PREFIX + router_id def get_transit_network_name(self, router_id: str) -> str: return TRANSIT_NETWORK_PREFIX + router_id def get_transit_subnet_name(self, router_id: str) -> str: return TRANSIT_SUBNET_PREFIX + router_id def make_transit_network(self, router_id: str, tenant_id: str, agent_host: str, gateway_update: ty.Dict[str, ty.Any]): context = nctx.get_admin_context() network_data = { 'tenant_id': HIDDEN_PROJECT_ID, 'name': self.get_transit_network_name(router_id), 'admin_state_up': True, 'shared': False, } network = p_utils.create_network(self.core_plugin, context, {'network': network_data}) gateway_update['transit_network_id'] = network['id'] # The subnet tenant_id must be of the user, otherwise updating the # router by the user may fail (it needs access to all subnets) subnet_data = { 'tenant_id': tenant_id, 'name': self.get_transit_subnet_name(router_id), 'gateway_ip': VPN_TRANSIT_LIP, 'cidr': VPN_TRANSIT_CIDR, 'network_id': network['id'], 'ip_version': 4, 'enable_dhcp': False, } subnet = p_utils.create_subnet(self.core_plugin, context, {'subnet': subnet_data}) gateway_update['transit_subnet_id'] = subnet['id'] self.l3_plugin.add_router_interface(context, router_id, {'subnet_id': subnet['id']}) fixed_ip = {'subnet_id': subnet['id'], 'ip_address': VPN_TRANSIT_RIP} port_data = { 'tenant_id': HIDDEN_PROJECT_ID, 'network_id': network['id'], 'fixed_ips': [fixed_ip], 'device_id': subnet['id'], 'device_owner': v_constants.DEVICE_OWNER_TRANSIT_NETWORK, 'admin_state_up': True, portbindings.HOST_ID: agent_host, 'name': self.get_vpn_namespace_port_name(router_id) } port = p_utils.create_port(self.core_plugin, context, {"port": port_data}) gateway_update['transit_port_id'] = port['id'] def _del_port(self, context: nctx.ContextBase, port_id: str): try: self.core_plugin.delete_port(context, port_id, l3_port_check=False) except n_exc.PortNotFound: pass def _remove_router_interface(self, context: nctx.ContextBase, router_id: str, subnet_id: str): try: self.l3_plugin.remove_router_interface( context, router_id, {'subnet_id': subnet_id}) except (n_exc.l3.RouterInterfaceNotFoundForSubnet, n_exc.SubnetNotFound): pass def _del_subnet(self, context: nctx.ContextBase, subnet_id: str): try: self.core_plugin.delete_subnet(context, subnet_id) except n_exc.SubnetNotFound: pass def _del_network(self, context: nctx.ContextBase, network_id: str): try: self.core_plugin.delete_network(context, network_id) except n_exc.NetworkNotFound: pass def del_transit_network(self, gw: ext_gw.VPNExtGW): context = nctx.get_admin_context() router_id: str = gw['router_id'] port_id: ty.Optional[str] = gw.get('transit_port_id') if port_id: self._del_port(context, port_id) subnet_id: ty.Optional[str] = gw.get('transit_subnet_id') if subnet_id: self._remove_router_interface(context, router_id, subnet_id) self._del_subnet(context, subnet_id) network_id = gw.get('transit_network_id') if network_id: self._del_network(context, network_id) def make_gw_port(self, router_id: str, network_id: str, agent_host: str, gateway_update: ty.Dict[str, ty.Any]): context = nctx.get_admin_context() port_data = {'tenant_id': HIDDEN_PROJECT_ID, 'network_id': network_id, 'fixed_ips': lib_constants.ATTR_NOT_SPECIFIED, 'device_id': router_id, 'device_owner': v_constants.DEVICE_OWNER_VPN_ROUTER_GW, 'admin_state_up': True, portbindings.HOST_ID: agent_host, 'name': self.get_vpn_gw_port_name(router_id)} gw_port = p_utils.create_port(self.core_plugin, context.elevated(), {'port': port_data}) if not gw_port['fixed_ips']: LOG.debug('No IPs available for external network %s', network_id) gateway_update['gw_port_id'] = gw_port['id'] def del_gw_port(self, gateway: ext_gw.VPNExtGW): context = nctx.get_admin_context() port_id = gateway.get('gw_port_id') if port_id: self._del_port(context, port_id) def _get_peer_cidrs(self, vpnservice): cidrs = [] for ipsec_site_connection in vpnservice.ipsec_site_connections: if ipsec_site_connection.peer_cidrs: for peer_cidr in ipsec_site_connection.peer_cidrs: cidrs.append(peer_cidr.cidr) if ipsec_site_connection.peer_ep_group is not None: for ep in ipsec_site_connection.peer_ep_group.endpoints: cidrs.append(ep.endpoint) return cidrs def _routes_update(self, cidrs: ty.Set, nexthop): routes = [{'destination': cidr, 'nexthop': nexthop} for cidr in cidrs] return {'router': {'routes': routes}} def _update_static_routes(self, context: nctx.ContextBase, ipsec_site_connection): vpnservice = self.service_plugin.get_vpnservice( context, ipsec_site_connection['vpnservice_id']) router_id = vpnservice['router_id'] gw: ext_gw.VPNExtGW = self.service_plugin.get_vpn_gw_by_router_id( context, router_id) nexthop = gw.transit_port['fixed_ips'][0]['ip_address'] router = self.l3_plugin.get_router(context, router_id) old_routes = router.get('routes', []) old_cidrs = {r['destination'] for r in old_routes if r['nexthop'] == nexthop} new_cidrs = set( self.service_plugin.get_peer_cidrs_for_router(context, router_id)) to_remove = old_cidrs - new_cidrs if to_remove: self.l3_plugin.remove_extraroutes(context, router_id, self._routes_update(to_remove, nexthop)) to_add = new_cidrs - old_cidrs if to_add: self.l3_plugin.add_extraroutes(context, router_id, self._routes_update(to_add, nexthop)) 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. """ gateway = self.service_plugin.get_vpn_gw_dict_by_router_id( nctx.get_admin_context(), router['id']) if gateway is None or gateway['external_fixed_ips'] is None: raise ext_gw.RouterIsNotVPNExternal(router_id=router['id']) v4_ip = v6_ip = None for fixed_ip in gateway['external_fixed_ips']: addr = fixed_ip['ip_address'] vers = netaddr.IPAddress(addr).version if vers == lib_constants.IP_VERSION_4: if v4_ip is None: v4_ip = addr elif v6_ip is None: v6_ip = addr return v4_ip, v6_ip def _update_gateway(self, context: nctx.Context, gateway_id: str, **kwargs): gateway = {'gateway': kwargs} return self.service_plugin.update_gateway(context, gateway_id, gateway) @db_api.retry_if_session_inactive() def _ensure_gateway(self, context: nctx.Context, vpnservice) -> \ ty.Dict[str, ty.Any]: gw = self.service_plugin.get_vpn_gw_dict_by_router_id( context, vpnservice['router_id'], refresh=True) if not gw: gateway = {'gateway': { 'router_id': vpnservice['router_id'], 'tenant_id': vpnservice['tenant_id'], }} # create_gateway may raise oslo_db.exception.DBDuplicateEntry # if someone else created one in the meantime return self.service_plugin.create_gateway(context, gateway) if gw['status'] == lib_constants.ERROR: raise vpnaas.VPNGatewayInError() # Raise an exception if an existing gateway is in status # PENDING_CREATE or PENDING_DELETE. # One of the next retries should succeed. if gw['status'] != lib_constants.ACTIVE: raise o_exc.RetryRequest(vpnaas.VPNGatewayNotReady()) return gw @db_api.CONTEXT_WRITER def _setup(self, context: nctx.Context, vpnservice_dict: ty.Dict[str, ty.Any]): router_id = vpnservice_dict['router_id'] agent = self.service_plugin.schedule_router(context, router_id) if not agent: raise vpnaas.NoVPNAgentAvailable agent_host = agent['host'] gateway: ty.Optional[ty.Dict[str, ty.Any]] = self._ensure_gateway( context, vpnservice_dict) # If the gateway status is ACTIVE the ports have been created already if gateway and gateway['status'] == lib_constants.ACTIVE: return vpnservice = self.service_plugin._get_vpnservice( context, vpnservice_dict["id"]) network_id = vpnservice.router.gw_port.network_id # keeps track of already-created IDs gateway_update: ty.Dict[str, ty.Any] = {} try: self.make_gw_port(router_id, network_id, agent_host, gateway_update) self.make_transit_network(router_id, vpnservice_dict['tenant_id'], agent_host, gateway_update) except Exception: self._update_gateway(context, gateway['id'], # type: ignore status=lib_constants.ERROR, **gateway_update) raise self._update_gateway(context, gateway['id'], # type: ignore status=lib_constants.ACTIVE, **gateway_update) def _cleanup(self, context: nctx.Context, router_id: str): gw = self.service_plugin.get_vpn_gw_dict_by_router_id(context, router_id) if not gw: return self._update_gateway(context, gw['id'], status=lib_constants.PENDING_DELETE) try: self.del_gw_port(gw) self.del_transit_network(gw) self.service_plugin.delete_gateway(context, gw['id']) except Exception: LOG.exception("Cleanup of VPN gateway for router %s failed.", router_id) self._update_gateway(context, gw['id'], status=lib_constants.ERROR) raise def create_vpnservice(self, context: nctx.Context, vpnservice_dict): try: self._setup(context, vpnservice_dict) except Exception: LOG.exception("Setting up the VPN gateway for router %s failed.", vpnservice_dict['router_id']) self.service_plugin.set_vpnservice_status( context, vpnservice_dict['id'], lib_constants.ERROR, updated_pending_status=True) raise super().create_vpnservice(context, vpnservice_dict) def delete_vpnservice(self, context: nctx.Context, vpnservice): router_id = vpnservice['router_id'] super().delete_vpnservice(context, vpnservice) services = self.service_plugin.get_vpnservices(context) router_ids = [s['router_id'] for s in services] if router_id not in router_ids: self._cleanup(context, router_id) def create_ipsec_site_connection(self, context: nctx.Context, ipsec_site_connection): self._update_static_routes(context, ipsec_site_connection) super().create_ipsec_site_connection(context, ipsec_site_connection) def delete_ipsec_site_connection(self, context: nctx.Context, ipsec_site_connection): self._update_static_routes(context, ipsec_site_connection) super().delete_ipsec_site_connection(context, ipsec_site_connection) def update_ipsec_site_connection( self, context, old_ipsec_site_connection, ipsec_site_connection): self._update_static_routes(context, ipsec_site_connection) super().update_ipsec_site_connection( context, old_ipsec_site_connection, ipsec_site_connection) def _update_port_binding(self, context: nctx.Context, port_id, host): port_data = {'binding:host_id': host} self.core_plugin.update_port(context, port_id, {'port': port_data}) def update_port_bindings(self, context: nctx.Context, router_id, host): gw = self.service_plugin.get_vpn_gw_dict_by_router_id(context, router_id) if not gw: return port_id = gw.get('gw_port_id') if port_id: self._update_port_binding(context, port_id, host) port_id = gw.get('transit_port_id') if port_id: self._update_port_binding(context, port_id, host) class IPsecOvnVpnAgentApi(base_ipsec.IPsecVpnAgentApi): def _agent_notification(self, context: nctx.Context, method, router_id, version=None, **kwargs): """Notify update for the agent. This method will find where is the router, and dispatch notification for the agent. """ admin_context = context if context.is_admin else context.elevated() if not version: version = self.target.version vpn_agents = self.driver.service_plugin.get_vpn_agents_hosting_routers( admin_context, [router_id], active=True) for vpn_agent in vpn_agents: LOG.debug('Notify agent at %(topic)s.%(host)s the message ' '%(method)s %(args)s', {'topic': self.topic, 'host': vpn_agent['host'], 'method': method, 'args': kwargs}) cctxt = self.client.prepare(server=vpn_agent['host'], version=version) cctxt.cast(context, method, **kwargs) class IPsecOvnVPNDriver(BaseOvnIPsecVPNDriver): """VPN Service Driver class for IPsec.""" def create_rpc_conn(self): self.agent_rpc = IPsecOvnVpnAgentApi( topics.IPSEC_AGENT_TOPIC, BASE_IPSEC_VERSION, self) def start_rpc_listeners(self) -> ty.List: self.endpoints = [IPsecVpnOvnDriverCallBack(self)] self.conn = n_rpc.Connection() self.conn.create_consumer( topics.IPSEC_DRIVER_TOPIC, self.endpoints, fanout=False) return self.conn.consume_in_threads()