# Copyright (c) 2016 Yi Jing Zhu, IBM. # Copyright (c) 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.agent.common import utils as agent_common_utils from neutron.agent.linux import interface from neutron.agent.linux import ip_lib from neutron_lib import constants as lib_constants from neutron_lib import context as nctx from oslo_concurrency import lockutils from oslo_log import log as logging from neutron_vpnaas.services.vpn.common import topics from neutron_vpnaas.services.vpn.device_drivers import ipsec from neutron_vpnaas.services.vpn.device_drivers import libreswan_ipsec from neutron_vpnaas.services.vpn.device_drivers import strongswan_ipsec PORT_PREFIX_INTERNAL = 'vr' PORT_PREFIX_EXTERNAL = 'vg' PORT_PREFIXES: ty.Dict[str, str] = { 'internal': PORT_PREFIX_INTERNAL, 'external': PORT_PREFIX_EXTERNAL, } LOG = logging.getLogger(__name__) class DeviceManager: """Device Manager for ports in qvpn-xx namespace. It is a veth pair, one side in qvpn and the other side is attached to ovs. """ OVN_NS_PREFIX = "qvpn-" def __init__(self, conf, host, plugin, context): self.conf = conf self.host = host self.plugin = plugin self.context = context self.driver: interface.LinuxInterfaceDriver = \ agent_common_utils.load_interface_driver(conf) def get_interface_name(self, port: ty.Dict[str, str], ptype: str) -> str: suffix = port['id'] return (PORT_PREFIXES[ptype] + suffix)[:self.driver.DEV_NAME_LEN] def get_namespace_name(self, process_id: str): return self.OVN_NS_PREFIX + process_id def get_existing_process_ids(self) -> ty.List: """Return the process IDs derived from the existing VPN namespaces.""" return [ns[len(self.OVN_NS_PREFIX):] for ns in ip_lib.list_network_namespaces() if ns.startswith(self.OVN_NS_PREFIX)] def set_default_route(self, namespace, subnet, device_name): device = ip_lib.IPDevice(device_name, namespace=namespace) gateway = device.route.get_gateway(ip_version=subnet['ip_version']) if gateway: gateway = gateway.get('gateway') new_gateway = subnet['gateway_ip'] if gateway == new_gateway: return device.route.add_gateway(subnet['gateway_ip']) def add_routes(self, namespace, cidrs, via): device = ip_lib.IPDevice(None, namespace=namespace) for cidr in cidrs: device.route.add_route(cidr, via=via, metric=100, proto='static') def delete_routes(self, namespace, cidrs, via): device = ip_lib.IPDevice(None, namespace=namespace) for cidr in cidrs: device.route.delete_route(cidr, via=via, metric=100, proto='static') def list_routes(self, namespace, via=None) -> ty.List[ty.Dict[str, ty.Any]]: device = ip_lib.IPDevice(None, namespace=namespace) return device.route.list_routes( lib_constants.IP_VERSION_4, proto='static', via=via) def del_static_routes(self, namespace): device = ip_lib.IPDevice(None, namespace=namespace) routes = device.route.list_routes( lib_constants.IP_VERSION_4, proto='static') for r in routes: device.route.delete_route(r['cidr'], via=r['via']) def _del_port(self, process_id: str, ptype: str): namespace = self.get_namespace_name(process_id) prefix = PORT_PREFIXES[ptype] device = ip_lib.IPDevice(None, namespace=namespace) ports: ty.List[ty.Dict[str, ty.Union[str, ty.Any]]] = \ device.addr.list() for p in ports: if not p['name'].startswith(prefix): continue interface_name = p['name'] self.driver.unplug(interface_name, namespace=namespace) def del_internal_port(self, process_id: str): self._del_port(process_id, 'internal') def del_external_port(self, process_id: str): self._del_port(process_id, 'external') def setup_external(self, process_id: str, network_details) -> ty.Optional[str]: network = network_details["external_network"] vpn_port = network_details['gw_port'] ns_name = self.get_namespace_name(process_id) interface_name = self.get_interface_name(vpn_port, 'external') if not ip_lib.ensure_device_is_ready(interface_name, namespace=ns_name): try: self.driver.plug(network['id'], vpn_port['id'], interface_name, vpn_port['mac_address'], namespace=ns_name, mtu=network.get('mtu'), prefix=PORT_PREFIX_EXTERNAL) except Exception: LOG.exception('plug external port %s failed', vpn_port) return None ip_cidrs = [] subnets = [] for fixed_ip in vpn_port['fixed_ips']: subnet_id = fixed_ip['subnet_id'] subnet = self.plugin.get_subnet_info(subnet_id) net = netaddr.IPNetwork(subnet['cidr']) ip_cidr = f'{fixed_ip["ip_address"]}/{net.prefixlen}' ip_cidrs.append(ip_cidr) subnets.append(subnet) self.driver.init_l3(interface_name, ip_cidrs, namespace=ns_name) for subnet in subnets: self.set_default_route(ns_name, subnet, interface_name) return interface_name def setup_internal(self, process_id, network_details) -> ty.Optional[str]: vpn_port = network_details["transit_port"] ns_name = self.get_namespace_name(process_id) interface_name = self.get_interface_name(vpn_port, 'internal') if not ip_lib.ensure_device_is_ready(interface_name, namespace=ns_name): try: self.driver.plug('', vpn_port['id'], interface_name, vpn_port['mac_address'], namespace=ns_name, prefix=PORT_PREFIX_INTERNAL) except Exception: LOG.exception('plug internal port %s failed', vpn_port['id']) return None ip_cidrs = [] for fixed_ip in vpn_port['fixed_ips']: ip_cidr = f'{fixed_ip["ip_address"]}/28' ip_cidrs.append(ip_cidr) self.driver.init_l3(interface_name, ip_cidrs, namespace=ns_name) return interface_name class NamespaceManager: def __init__(self, use_ipv6=False): self.ip_wrapper_root = ip_lib.IPWrapper() self.use_ipv6 = use_ipv6 def exists(self, name) -> bool: return ip_lib.network_namespace_exists(name) def create(self, name): ip_wrapper = self.ip_wrapper_root.ensure_namespace(name) cmd = ['sysctl', '-w', 'net.ipv4.ip_forward=1'] ip_wrapper.netns.execute(cmd) if self.use_ipv6: cmd = ['sysctl', '-w', 'net.ipv6.conf.all.forwarding=1'] ip_wrapper.netns.execute(cmd) def delete(self, name): try: self.ip_wrapper_root.netns.delete(name) except RuntimeError: msg = 'Failed trying to delete namespace: %s' LOG.exception(msg, name) class OvnOpenSwanProcess(ipsec.OpenSwanProcess): pass class OvnStrongSwanProcess(strongswan_ipsec.StrongSwanProcess): pass class OvnLibreSwanProcess(libreswan_ipsec.LibreSwanProcess): pass class IPsecOvnDriverApi(ipsec.IPsecVpnDriverApi): def __init__(self, topic): super().__init__(topic) self.admin_ctx = nctx.get_admin_context_without_session() def get_vpn_transit_network_details(self, router_id): cctxt = self.client.prepare() return cctxt.call(self.admin_ctx, 'get_vpn_transit_network_details', router_id=router_id) def get_subnet_info(self, subnet_id): cctxt = self.client.prepare() return cctxt.call(self.admin_ctx, 'get_subnet_info', subnet_id=subnet_id) class OvnIPsecDriver(ipsec.IPsecDriver, metaclass=abc.ABCMeta): def __init__(self, vpn_service, host: str): self.nsmgr = NamespaceManager() super().__init__(vpn_service, host) self.agent_rpc = IPsecOvnDriverApi(topics.IPSEC_DRIVER_TOPIC) self.devmgr = DeviceManager(self.conf, self.host, self.agent_rpc, self.context) get_router_based_iptables_manager = None def get_namespace(self, router_id) -> str: """Get namespace for VPN services of router. :router_id: router_id :returns: namespace string. """ return self.devmgr.get_namespace_name(router_id) def _cleanup_namespace(self, router_id: str): ns_name = self.devmgr.get_namespace_name(router_id) if not self.nsmgr.exists(ns_name): return self.devmgr.del_internal_port(router_id) self.devmgr.del_external_port(router_id) self.nsmgr.delete(ns_name) def _ensure_namespace(self, router_id: str, network_details) -> str: ns_name = self.get_namespace(router_id) if not self.nsmgr.exists(ns_name): self.nsmgr.create(ns_name) # set up vpn external port on provider net self.devmgr.setup_external(router_id, network_details) # set up vpn internal port on transit net self.devmgr.setup_internal(router_id, network_details) return ns_name @abc.abstractmethod def create_process(self, process_id: str, vpnservice, namespace) -> ipsec.BaseSwanProcess: pass def destroy_process(self, process_id: str): LOG.info('process %s is destroyed', process_id) namespace = self.devmgr.get_namespace_name(process_id) # If the namespace exists but the process_id is not in the table # there may be an active swan process from a previous run of the agent # which does not have a process object in memory. # To be able to clean it up we need to create a dummy process object # here (without a vpnservice), so that destroy_process will stop # the swan. if self.nsmgr.exists(namespace) and process_id not in self.processes: self.ensure_process(process_id) super().destroy_process(process_id) self._cleanup_namespace(process_id) def create_router(self, router): pass def destroy_router(self, process_id): pass def _update_nat(self, vpnservice, func): pass def _update_route(self, vpnservice, network_details): router_id = vpnservice['router_id'] gateway_ip = network_details['transit_gateway_ip'] namespace = self.devmgr.get_namespace_name(router_id) old_local_cidrs = set() for route in self.devmgr.list_routes(namespace, via=gateway_ip): old_local_cidrs.add(route['cidr']) new_local_cidrs = set() for ipsec_site_conn in vpnservice['ipsec_site_connections']: new_local_cidrs.update(ipsec_site_conn['local_cidrs']) self.devmgr.delete_routes(namespace, old_local_cidrs - new_local_cidrs, gateway_ip) self.devmgr.add_routes(namespace, new_local_cidrs - old_local_cidrs, gateway_ip) def _sync_vpn_processes(self, vpnservices, sync_router_ids: ty.List[str]): # Ensure the ipsec process is enabled only for # - the vpn services which are not yet in self.processes # - vpn services whose router id is in 'sync_router_ids' for vpnservice in vpnservices: router_id = vpnservice['router_id'] if router_id not in self.processes or router_id in sync_router_ids: net_details = self.agent_rpc.get_vpn_transit_network_details( router_id) self._ensure_namespace(router_id, net_details) self._update_route(vpnservice, net_details) process = self.ensure_process(router_id, vpnservice=vpnservice) process.update() def _cleanup_stale_vpn_processes(self, vpn_router_ids: ty.List[str]): super()._cleanup_stale_vpn_processes(vpn_router_ids) # Look for additional namespaces on this node that we don't know # and that should be deleted for router_id in self.devmgr.get_existing_process_ids(): if router_id not in vpn_router_ids: self.destroy_process(router_id) @lockutils.synchronized('vpn-agent', 'neutron-') def vpnservice_removed_from_agent(self, context: nctx.Context, router_id: str): # must run under the same lock as sync() self.destroy_process(router_id) def vpnservice_added_to_agent(self, context: nctx.Context, router_ids: ty.List[str]): routers = [{'id': router_id} for router_id in router_ids] self.sync(context, routers) class OvnStrongSwanDriver(OvnIPsecDriver): def create_process(self, process_id: str, vpnservice, namespace: str) -> ipsec.BaseSwanProcess: return OvnStrongSwanProcess( self.conf, process_id, vpnservice, namespace) class OvnOpenSwanDriver(OvnIPsecDriver): def create_process(self, process_id: str, vpnservice, namespace: str) -> ipsec.BaseSwanProcess: return OvnOpenSwanProcess( self.conf, process_id, vpnservice, namespace) class OvnLibreSwanDriver(OvnIPsecDriver): def create_process(self, process_id: str, vpnservice, namespace: str) -> ipsec.BaseSwanProcess: return OvnLibreSwanProcess( self.conf, process_id, vpnservice, namespace)