OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

neutron-vpnaas — REST API

REST API — neutron-vpnaas — branch stable/2026.2

Branch stable/2026.2 @ 6ce5da102cf1 — repository https://opendev.org/openstack/neutron-vpnaas — analysed on 2026-10-29. Cross-repository references: neutron-lib: = neutron-lib stable/2026.2 @ 1ee72052251a, neutron: = neutron stable/2026.2 @ eef1a0f7ee36.

neutron-vpnaas has no WSGI application of its own. It is a Neutron service plugin: the Neutron server loads it (service_plugins = vpnaas or ovn-vpnaas, pyproject.toml:57-60) and builds the routes at start-up from the extensions' attribute maps. The attribute maps live in neutron-lib (neutron-lib:neutron_lib/api/definitions/vpn.py:230, neutron-lib:neutron_lib/api/definitions/vpn_endpoint_groups.py:43); the extension descriptors in neutron_vpnaas/extensions/. Requests go through Neutron's legacy Routes ExtensionMiddleware and the generic controller (neutron:neutron/api/v2/base.py), which validates the body against the attribute map, checks the policy, then calls plugin.<action>_<resource>().

The route table below was extracted from the running server with _rewrite/tools/vpnaas_capture.py (files _rewrite/routes-legacy_stable-2026.2.json and _rewrite/routes-ovn_stable-2026.2.json), and every behaviour marked observed was replayed through the real WSGI pipeline (_rewrite/exchanges-*_stable-2026.2.json).

Two deployment profiles exist, and the API differs slightly between them:

ProfileService pluginService driver ([service_providers])Agent
legacy (ML2/OVS, L3 agent)vpnaas → VPNDriverPlugin (neutron_vpnaas/services/vpn/plugin.py:56)IPsecVPNDriver (neutron_vpnaas/services/vpn/service_drivers/ipsec.py:27)L3 agent extension vpnaas (neutron_vpnaas/services/vpn/agent.py:108)
OVN (OpenKeel target)ovn-vpnaas → VPNOVNDriverPlugin (neutron_vpnaas/services/vpn/ovn_plugin.py:77)IPsecOvnVPNDriver (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:503)neutron-ovn-vpn-agent (neutron_vpnaas/agent/ovn/vpn/agent.py:79)

Summary table

All paths are under /v2.0. The vpn prefix comes from path_prefix = "/vpn" (neutron_vpnaas/services/vpn/plugin.py:53, neutron_vpnaas/services/vpn/ovn_plugin.py:74). Policies default to ADMIN_OR_PROJECT_MEMBER (project scope); the deprecated rules (rule:admin_or_owner, rule:regular_user for create) apply until enforce_new_defaults is set (neutron_vpnaas/policies/vpnservice.py:22-93).

MethodPathHandlerPolicySuccessErrors (observed or traced)
GET/vpn/ikepoliciesneutron_vpnaas/db/vpn/vpn_db.py:380 get_ikepoliciesget_ikepolicy200401, 403
POST/vpn/ikepoliciesneutron_vpnaas/db/vpn/vpn_db.py:318 create_ikepolicycreate_ikepolicy201400, 401, 403
GET/vpn/ikepolicies/{id}neutron_vpnaas/db/vpn/vpn_db.py:374 get_ikepolicyget_ikepolicy200401, 404
PUT/vpn/ikepolicies/{id}neutron_vpnaas/db/vpn/vpn_db.py:344 update_ikepolicyupdate_ikepolicy200400, 401, 403, 404, 409
DELETE/vpn/ikepolicies/{id}neutron_vpnaas/db/vpn/vpn_db.py:364 delete_ikepolicydelete_ikepolicy204401, 403, 404, 409
GET/vpn/ipsecpoliciesneutron_vpnaas/db/vpn/vpn_db.py:466 get_ipsecpoliciesget_ipsecpolicy200401, 403
POST/vpn/ipsecpoliciesneutron_vpnaas/db/vpn/vpn_db.py:404 create_ipsecpolicycreate_ipsecpolicy201400, 401, 403
GET/vpn/ipsecpolicies/{id}neutron_vpnaas/db/vpn/vpn_db.py:460 get_ipsecpolicyget_ipsecpolicy200401, 404
PUT/vpn/ipsecpolicies/{id}neutron_vpnaas/db/vpn/vpn_db.py:428 update_ipsecpolicyupdate_ipsecpolicy200400, 401, 403, 404, 409
DELETE/vpn/ipsecpolicies/{id}neutron_vpnaas/db/vpn/vpn_db.py:449 delete_ipsecpolicydelete_ipsecpolicy204401, 403, 404, 409
GET/vpn/endpoint-groupsneutron_vpnaas/db/vpn/vpn_db.py:680 get_endpoint_groupsget_endpoint_group200401, 403
POST/vpn/endpoint-groupsneutron_vpnaas/db/vpn/vpn_db.py:635 create_endpoint_groupcreate_endpoint_group201400, 401, 403
GET/vpn/endpoint-groups/{id}neutron_vpnaas/db/vpn/vpn_db.py:674 get_endpoint_groupget_endpoint_group200401, 404
PUT/vpn/endpoint-groups/{id}neutron_vpnaas/db/vpn/vpn_db.py:655 update_endpoint_groupupdate_endpoint_group200400, 401, 403, 404
DELETE/vpn/endpoint-groups/{id}neutron_vpnaas/db/vpn/vpn_db.py:666 delete_endpoint_groupdelete_endpoint_group204400 (in use), 401, 403, 404
GET/vpn/vpnservicesneutron_vpnaas/db/vpn/vpn_db.py:560 get_vpnservicesget_vpnservice200401, 403
POST/vpn/vpnservicesneutron_vpnaas/services/vpn/plugin.py:204 create_vpnservicecreate_vpnservice201400, 401, 403, 404, 409 (OVN), 503
GET/vpn/vpnservices/{id}neutron_vpnaas/db/vpn/vpn_db.py:554 get_vpnserviceget_vpnservice200401, 404
PUT/vpn/vpnservices/{id}neutron_vpnaas/services/vpn/plugin.py:214 update_vpnserviceupdate_vpnservice200400, 401, 403, 404
DELETE/vpn/vpnservices/{id}neutron_vpnaas/services/vpn/plugin.py:222 delete_vpnservicedelete_vpnservice204401, 403, 404, 409
GET/vpn/ipsec-site-connectionsneutron_vpnaas/db/vpn/vpn_db.py:275 get_ipsec_site_connectionsget_ipsec_site_connection200401, 403
POST/vpn/ipsec-site-connectionsneutron_vpnaas/services/vpn/plugin.py:166 create_ipsec_site_connectioncreate_ipsec_site_connection201400, 401, 403, 404
GET/vpn/ipsec-site-connections/{id}neutron_vpnaas/db/vpn/vpn_db.py:267 get_ipsec_site_connectionget_ipsec_site_connection200401, 404
PUT/vpn/ipsec-site-connections/{id}neutron_vpnaas/services/vpn/plugin.py:186 update_ipsec_site_connectionupdate_ipsec_site_connection200400, 401, 403, 404
DELETE/vpn/ipsec-site-connections/{id}neutron_vpnaas/services/vpn/plugin.py:177 delete_ipsec_site_connectiondelete_ipsec_site_connection204401, 403, 404
GET/agents/{agent_id}/vpn-routersneutron_vpnaas/extensions/vpn_agentschedulers.py:50 indexget_vpn-routers (undefined → default)200500 for non-admins
POST/agents/{agent_id}/vpn-routersneutron_vpnaas/extensions/vpn_agentschedulers.py:58 createcreate_vpn-router (undefined → default)201404, 409
DELETE/agents/{agent_id}/vpn-routers/{id}neutron_vpnaas/extensions/vpn_agentschedulers.py:70 deletedelete_vpn-router (undefined → default)204404
GET/routers/{router_id}/vpn-agentsneutron_vpnaas/extensions/vpn_agentschedulers.py:91 indexget_vpn-agents (undefined → default)200404

The four vpn-routers/vpn-agents operations exist only in the OVN profile (extension alias vpn-agent-scheduler, neutron_vpnaas/services/vpn/ovn_plugin.py:70). Routes also generates GET/PUT …/vpn-routers/{id}, POST …/vpn-agents and GET/PUT/DELETE …/vpn-agents/{id}, but the controllers have no such methods: these six routes answer 500 (observed, sched-show-unimplemented, sched-post-vpn-agents-unimplemented).

Common Neutron behaviours apply to every collection: fields= selection, filters on every is_filter attribute, sorting on is_sort_key attributes, pagination (limit, marker), bulk create (emulated, native_bulk = false in the route table), and the response wraps the resource under its singular or plural name. Responses carry both project_id and the legacy tenant_id.

Effective attribute validation (observed)

Four extensions touch the IKE/IPsec policy attributes: vpnaas (base lists, neutron-lib:neutron_lib/api/definitions/vpn.py:71), vpn-aes-ccm-gcm (shim, no attributes, neutron-lib:neutron_lib/api/definitions/vpn_aes_ccm_gcm.py:27), vpn-aes-ctr (adds aes-128-ctr, aes-192-ctr, aes-256-ctr, neutron-lib:neutron_lib/api/definitions/vpn_aes_ctr.py:38) and vpn-no-sha1-3des (removes sha1 and 3des, defaults auth_algorithm to sha256, neutron-lib:neutron_lib/api/definitions/vpn_no_sha1_3des.py:33-89).

Neutron merges extended attributes by replacing the whole attribute definition, in extension processing order (neutron:neutron/api/extensions.py:353-364); extensions are loaded from their directory in sorted file-name order (neutron:neutron/api/extensions.py:447), so vpn_no_sha1_3des is applied after vpn_aes_ctr. Its VPN_SUPPORTED_ENCRYPTION_ALGORITHMS_V2 is computed from the base list, which has no CTR values (neutron-lib:neutron_lib/api/definitions/vpn_no_sha1_3des.py:38-41). Result, observed on both profiles: POST /vpn/ikepolicies {"encryption_algorithm": "aes-128-ctr"} → 400 Invalid input for encryption_algorithm. Reason: aes-128-ctr is not in valid_values. although vpn-aes-ctr is advertised in GET /v2.0/extensions and the database enum accepts CTR (neutron_vpnaas/db/vpn/vpn_models.py:32-41). See 00 § Points of attention.

The effective lists (from the live route table) are therefore:

AttributeAccepted valuesDefault
auth_algorithm (IKE, IPsec)sha256, sha384, sha512, aes-xcbc, aes-cmacsha256
encryption_algorithm (IKE, IPsec)aes-128, aes-192, aes-256, aes-{128,192,256}-ccm-{8,12,16}, aes-{128,192,256}-gcm-{8,12,16}aes-128
pfsgroup2, group5, group14–group31group5
ike_versionv1, v2v1
phase1_negotiation_modemain, aggressivemain
transform_protocolesp, ah, ah-espesp
encapsulation_modetunnel, transporttunnel
lifetime{"units": "seconds", "value": >= 60}{seconds, 3600} (applied in neutron_vpnaas/db/vpn/vpn_db.py:322-323)
initiatorbi-directional, response-onlybi-directional
dpd.actionclear, disabled, hold, restart, restart-by-peerhold (neutron_vpnaas/db/vpn/vpn_validator.py:229-231)
endpoint group typecidr, network, router, subnet, vlan— (required)

IKE policies — /vpn/ikepolicies

Functional description

  • Purpose: define the phase-1 (IKE) parameters a site-to-site connection negotiates: authentication hash, encryption, DH group (PFS), IKE version, phase-1 mode and SA lifetime.
  • Actor: project member (create_ikepolicy … get_ikepolicy, neutron_vpnaas/policies/ike_policy.py:22-90). A reader cannot see the policies (the get_* rules require member), and other projects get 404.
  • Preconditions: none; the policy is a pure database object.
  • Business rules:
    • values must be in the lists above (attribute map validation, 400);
    • lifetime.units defaults to seconds, lifetime.value to 3600 when absent (neutron_vpnaas/db/vpn/vpn_db.py:321-323);
    • an update or a delete is refused while any IPsec site connection uses the policy — even a rename (IKEPolicyInUse, 409, neutron_vpnaas/db/vpn/vpn_db.py:349-351, neutron_vpnaas/db/vpn/vpn_db.py:366-368);
    • the driver validators accept every IKE policy (neutron_vpnaas/db/vpn/vpn_validator.py:357-363).
  • Flow: synchronous only — validation, one INSERT/UPDATE/DELETE on ikepolicies, response. No agent is notified: a policy only reaches an agent through a connection.
  • Outcome: immediate; no status attribute.
  • Limitations: there is no way to change a policy in use; create a new one and update each connection… which the API does not allow either (ikepolicy_id is not updatable, neutron-lib:neutron_lib/api/definitions/vpn.py:467-473). In practice the connection must be recreated.

Technical sheet

  • Request (POST): {"ikepolicy": {"name", "description", "auth_algorithm", "encryption_algorithm", "phase1_negotiation_mode", "lifetime": {"units", "value"}, "ike_version", "pfs", "project_id"}}, all optional. Unknown attributes → 400 Unrecognized attribute(s) 'foo'.
  • Response: {"ikepolicy": {"id", "project_id", "tenant_id", "name", "description", "auth_algorithm", "encryption_algorithm", "phase1_negotiation_mode", "lifetime": {"units", "value"}, "ike_version", "pfs"}} (neutron_vpnaas/db/vpn/vpn_db.py:300-316).
  • Errors: 400 (validation), 403 (policy), 404 IKEPolicyNotFound, 409 IKEPolicyInUse — see errors.
  • Effects: table ikepolicies only.
  • Spec/code mismatches: the api-ref still shows sha1 and 3des in samples and lists (neutron-lib:api-ref/source/v2/vpnaas.inc:62); the code rejects them since vpn-no-sha1-3des. The api-ref documents no 403 for PUT/DELETE and no 409 for PUT; both happen.

IPsec policies — /vpn/ipsecpolicies

Same model as IKE policies (neutron_vpnaas/db/vpn/vpn_db.py:385-469), with transform_protocol and encapsulation_mode instead of phase1_negotiation_mode and ike_version.

  • Business rules specific to IPsec policies:
    • ah-esp is accepted at policy creation, because the reference validator accepts every IPsec policy (neutron_vpnaas/db/vpn/vpn_validator.py:297-303);
    • it is refused later, when a connection uses it, by the legacy driver only: IpsecVpnValidator raises IpsecValidationFailure (400) for ah-esp (neutron_vpnaas/services/vpn/service_drivers/ipsec_validator.py:30-50). The OVN driver uses the base VpnDriverValidator, which validates nothing (neutron_vpnaas/services/vpn/service_drivers/__init__.py:31-35, neutron_vpnaas/services/vpn/service_drivers/driver_validator.py:17-29): on OVN the connection is created (observed conn-create-ah-esp: 400 legacy, 201 OVN) and strongSwan receives a configuration it cannot express (the template only knows ah and ESP, neutron_vpnaas/services/vpn/device_drivers/template/strongswan/ipsec.conf.template:29-35) [inferred: tunnel never comes up];
    • update/delete refused while in use (IPsecPolicyInUse, 409, neutron_vpnaas/db/vpn/vpn_db.py:433-436, neutron_vpnaas/db/vpn/vpn_db.py:451-454).

Endpoint groups — /vpn/endpoint-groups

Functional description

  • Purpose: name a set of local subnets (type: subnet) or remote CIDRs (type: cidr) that a connection protects. Endpoint groups are the modern way to describe traffic selectors; the legacy way is vpnservice.subnet_id + connection.peer_cidrs.
  • Actor: project member (neutron_vpnaas/policies/endpoint_group.py:22-90).
  • Business rules (neutron_vpnaas/db/vpn/vpn_validator.py:328-355, only at creation):
    • at least one endpoint (MissingEndpointForEndpointGroup, 400; its message embeds the whole request body, observed);
    • no duplicates (InvalidEndpointInEndpointGroup … Endpoints have to be unique, 400);
    • cidr: each endpoint must be a valid CIDR (… Invalid CIDR, 400);
    • subnet: each endpoint must be a UUID (… Invalid UUID) of an existing subnet visible to the caller (NonExistingSubnetInEndpointGroup, 400);
    • vlan, network, router: no validation at all (observed: {"type": "vlan", "endpoints": ["not-a-vlan"]} → 201); no driver uses them;
    • type and endpoints are read-only after creation (allow_put: false, neutron-lib:neutron_lib/api/definitions/vpn_endpoint_groups.py:77-90) → 400 Cannot update read-only attribute endpoints;
    • delete refused while a connection references the group as local or peer group (EndpointGroupInUse, 400 and not 409, neutron_vpnaas/db/vpn/vpn_db.py:685-693).
  • Effects visible elsewhere: once a subnet is in a subnet group, Neutron refuses to delete that subnet (subnet_callback, neutron_vpnaas/db/vpn/vpn_db.py:793-799; observed 409 SubnetInUse … Callback neutron_vpnaas.db.vpn.vpn_db.subnet_callback-<n> failed with "Subnet … is used by endpoint group …").
  • Tables: vpn_endpoint_groups, vpn_endpoints.

VPN services — /vpn/vpnservices

POST /vpn/vpnservices

Functional description

  • Purpose: attach a VPN capability to one of the project's routers. The service is the container of the site connections; it fixes the local tunnel endpoint (the router's external IP).
  • Actor: project member (create_vpnservice).
  • Preconditions:
    • the router exists, is visible to the caller and has an external gateway (RouterIsNotExternal, 400, neutron_vpnaas/db/vpn/vpn_validator.py:275-278);
    • if the legacy subnet_id is given, that subnet must be attached to the router (SubnetIsNotConnectedToRouter, 400, neutron_vpnaas/db/vpn/vpn_validator.py:280-289);
    • if flavor_id is given: the flavors plugin is loaded, the flavor exists, has service_type VPN, is enabled, and its first provider is a configured driver (neutron_vpnaas/services/vpn/plugin.py:129-150; observed 404 FlavorNotFound for an unknown flavor);
    • OVN only: a live VPN agent exists to host the router (see the flow).
  • Business rules:
    • subnet_id is optional (uuid_or_none); when it is set, all connections of the service must use peer_cidrs; when it is not, they must use endpoint groups (enforced at connection time, see below);
    • several services may exist on the same router (no uniqueness check; observed: two services on the same router);
    • the provider (driver) is chosen from the flavor or the default provider, and recorded in Neutron's providerresourceassociations (neutron_vpnaas/services/vpn/plugin.py:205-209).
  • Flow — legacy profile:
    1. (sync) policy and attribute validation; validate_vpnservice (neutron_vpnaas/db/vpn/vpn_db.py:485-502); INSERT vpnservices with status PENDING_CREATE.
    2. (sync) provider association (neutron_vpnaas/services/vpn/plugin.py:208).
    3. (sync) the driver stores the router gateway's first IPv4 and first IPv6 address in external_v4_ip / external_v6_ip (neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:168-183).
    4. No RPC is sent (observed): an L3 agent only receives services that have at least one connection (neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:60-61).
  • Flow — OVN profile (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:373-435):
    1. (sync) steps 1–2 above.
    2. (sync) schedule the router to a VPN agent (schedule_router, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:375): reuse the agent already hosting the router, else choose among live VPN Agents with the configured scheduler (default LeastRoutersScheduler, neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:39-41), INSERT routervpnagentbindings. No live agent → RouterReschedulingFailed (409).
    3. (sync) ensure one VPN gateway row per router (vpn_ext_gws, status PENDING_CREATE), retried while another request is creating it (RetryRequest(VPNGatewayNotReady), neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:350-370); a gateway in ERROR → VPNGatewayInError (409).
    4. (sync, first service of the router only) create, as admin with an empty project:
      • the VPN gateway port vpn-gw-<router_id> on the router's external network, device_owner network:vpn_router_gateway, bound to the agent host (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:258-273);
      • the transit network vpn-transit-network-<router_id> and subnet vpn-transit-subnet-<router_id> 169.254.0.0/28 (gateway 169.254.0.1, owned by the user's project), attached to the router as an interface, and the namespace port vpn-ns-<router_id> with 169.254.0.2, device_owner network:vpn_namespace (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:169-213);
      • then set the gateway ACTIVE with the created IDs (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:403-405). Any failure sets the gateway ERROR and re-raises.
    5. (sync) store external_v4_ip/external_v6_ip from the VPN gateway port, not from the router gateway (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:323-343): on OVN the tunnel uses a dedicated public IP (observed: router gateway 172.24.4.118, external_v4_ip 172.24.4.16).
    6. On any failure of steps 2–5, the service is set to ERROR and the error is re-raised (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:425-434).
  • Outcome: 201 with status: PENDING_CREATE. The status becomes ACTIVE or DOWN only after a connection exists and the agent reports (see async processing § update_status). A service without connection stays PENDING_CREATE forever, and cannot be updated meanwhile (see PUT).
  • Effects visible elsewhere (OVN): new ports visible to admins in GET /v2.0/ports; a new router interface on 169.254.0.0/28 and a new network owned by an empty project; the router is now bound to a VPN agent (GET /v2.0/routers/{id}/vpn-agents).
  • Limitations:
    • OVN: a failed create leaves a row behind. The 409 RouterReschedulingFailed is returned, but the service was already committed with ERROR (observed vpns-create-no-vpn-agent then vpns-list-after-no-agent). The client must delete it.
    • OVN error path calls a missing attribute when the service is still pending and updated_pending_status is false: set_vpnservice_status raises vpnaas.VPNStateInvalidToUpdate, which does not exist in neutron_vpnaas.extensions.vpnaas (neutron_vpnaas/db/vpn/vpn_db.py:520-523) → AttributeError → 500 [inferred: the only caller passes updated_pending_status=True, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:431-433, so the branch is unreachable today].
    • The no VPN agent exception NoVPNAgentAvailable (503, neutron_vpnaas/extensions/vpnaas.py:46-47) is only raised when the scheduler returns None (concurrent scheduling, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:376-377); the usual "no agent" case is the 409 above.

Technical sheet

  • Request: {"vpnservice": {"router_id" (required), "subnet_id" (null), "name", "description", "admin_state_up" (true), "flavor_id" (null), "project_id"}}.
  • Response: {"vpnservice": {"id", "name", "description", "project_id", "tenant_id", "subnet_id", "router_id", "flavor_id", "admin_state_up", "external_v4_ip", "external_v6_ip", "status"}} (neutron_vpnaas/db/vpn/vpn_db.py:471-483).
  • Effects: vpnservices, providerresourceassociations; OVN: routervpnagentbindings, vpn_ext_gws, Neutron ports, networks, subnets, router interfaces — see interactions.

PUT /vpn/vpnservices/{id}

  • Purpose: rename, describe, or enable/disable (admin_state_up) the service. router_id, subnet_id, flavor_id are immutable.
  • Rules: refused while the service is in a PENDING_* status (VPNStateInvalidToUpdate, 400, neutron_vpnaas/db/vpn/vpn_db.py:88-92, neutron_vpnaas/db/vpn/vpn_db.py:532). Because a service without connection never leaves PENDING_CREATE, a fresh service cannot be renamed until an agent has reported it (observed vpns-update-pending).
  • Flow: UPDATE vpnservices (status unchanged), then cast vpnservice_updated to the agent(s) hosting the router (neutron_vpnaas/services/vpn/service_drivers/base_ipsec.py:185-186). Disabling the service makes the agent stop the IPsec daemon at its next sync (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:315-327).

DELETE /vpn/vpnservices/{id}

  • Rules: refused while connections exist (VPNServiceInUse, 409, neutron_vpnaas/db/vpn/vpn_db.py:537-546).
  • Flow: DELETE vpnservices, remove the provider association, cast vpnservice_updated (neutron_vpnaas/services/vpn/plugin.py:222-228). OVN: if no other service remains on the router, the VPN gateway is set PENDING_DELETE, its gateway port, namespace port, transit router interface, transit subnet and network are deleted, then the vpn_ext_gws row (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:407-443; observed ovn-ports-after-delete → no port left). A failure sets the gateway ERROR and re-raises (500) after the service row is gone [inferred].
  • Note: the router stays bound to its VPN agent (routervpnagentbindings is not cleaned) [inferred from the absence of an unbind call in neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:437-443].

IPsec site connections — /vpn/ipsec-site-connections

POST /vpn/ipsec-site-connections

Functional description

  • Purpose: establish a site-to-site IPsec tunnel between the project's VPN service and a remote gateway, with a pre-shared key.
  • Actor: project member (create_ipsec_site_connection, neutron_vpnaas/policies/ipsec_site_connection.py:23).
  • Preconditions: the VPN service, IKE policy and IPsec policy exist and are visible to the caller (404 otherwise, checked in that order, neutron_vpnaas/db/vpn/vpn_db.py:159-164); referenced endpoint groups exist; the router's external network has a subnet of the peer's IP version.
  • Business rules, in evaluation order:
    1. driver validation first, before any database read: legacy rejects an ah-esp IPsec policy (neutron_vpnaas/services/vpn/plugin.py:169-171, neutron_vpnaas/services/vpn/service_drivers/ipsec_validator.py:46-50);
    2. defaults: dpd → {hold, 30, 120} (neutron_vpnaas/db/vpn/vpn_validator.py:206-238); mtu 1500, initiator bi-directional, local_id '' (attribute map);
    3. mode consistency (neutron_vpnaas/db/vpn/vpn_validator.py:166-204):
      • service with subnet_id → peer_cidrs required (MissingPeerCidrs) and no endpoint group allowed (InvalidEndpointGroup);
      • service without subnet_id → both local_ep_group_id and peer_ep_group_id required (MissingRequiredEndpointGroup) and no peer_cidrs (PeerCidrsInvalid);
    4. endpoint-group mode: the local group must be of type subnet and the peer group of type cidr (WrongEndpointGroupType); every local subnet must be attached to the service's router (SubnetIsNotConnectedToRouter); all local subnets share one IP version, all peer CIDRs share one IP version (MixedIPVersionsForIPSecEndpoints) (neutron_vpnaas/db/vpn/vpn_validator.py:252-262);
    5. legacy mode: each peer CIDR is valid (IPsecSiteConnectionPeerCidrError) and all share one IP version (MixedIPVersionsForPeerCidrs) (neutron_vpnaas/db/vpn/vpn_validator.py:263-266);
    6. local and peer IP versions are equal (MixedIPVersionsForIPSecConnection);
    7. dpd.timeout > dpd.interval (IPsecSiteConnectionDpdIntervalValueError);
    8. mtu ≥ 68 for IPv4, ≥ 1280 for IPv6 (IPsecSiteConnectionMtuError, neutron_vpnaas/db/vpn/vpn_validator.py:38);
    9. peer_address: an IP, or an FQDN resolved synchronously by the API server with getaddrinfo; the first address replaces the FQDN in the database (VPNPeerAddressNotResolved, neutron_vpnaas/db/vpn/vpn_validator.py:82-96);
    10. the router's gateway port must have a fixed IP of the peer's IP version (ExternalNetworkHasNoSubnet, neutron_vpnaas/db/vpn/vpn_validator.py:67-80).
    11. route_mode is forced to static and auth_mode to psk (neutron_vpnaas/db/vpn/vpn_db.py:180-182).
  • Flow:
    1. (sync) validations above, INSERT ipsec_site_connections (PENDING_CREATE) and one ipsecpeercidrs row per legacy peer CIDR (neutron_vpnaas/db/vpn/vpn_db.py:153-203).
    2. (sync, OVN only) recompute the router's extra routes: every peer CIDR of every connection on the router → next hop 169.254.0.2 (the VPN namespace); add the missing ones, remove stale ones (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:297-321, observed ovn-router-routes).
    3. (sync) cast vpnservice_updated {router: {id}} on topic ipsec_agent, server = each live agent hosting the router (legacy: L3 agents, neutron_vpnaas/services/vpn/service_drivers/__init__.py:82-104; OVN: VPN agents, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:477-500).
    4. (async) the agent pulls the full configuration (get_vpn_services_on_host), renders strongSwan/Libreswan files, starts or reloads the daemon in the router namespace (qrouter-<id>, or qvpn-<id> on OVN), then reports statuses with update_status. See async processing.
  • Outcome: 201 with status: PENDING_CREATE, then ACTIVE (tunnel established) or DOWN after the first report. The client polls GET.
  • Effects visible elsewhere: OVN router extra routes (visible in GET /v2.0/routers/{id}); the subnets of the local group become undetachable from the router (SubnetInUseByIPsecSiteConnection, neutron_vpnaas/db/vpn/vpn_db.py:587-608); NAT-exemption rules in the router namespace (legacy).
  • Limitations:
    • the PSK is stored and returned in clear in every GET (is_visible: true, neutron-lib:neutron_lib/api/definitions/vpn.py:425-431; neutron_vpnaas/db/vpn/vpn_db.py:106) and sent in RPC payloads;
    • FQDN resolution happens once, on the API server, and the resolved IP replaces the FQDN in the database: a later DNS change is ignored (the Libreswan agent has a resolver, _get_nexthop, but nothing calls it: neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:267-281);
    • IPv6 peer CIDRs longer than 32 characters do not fit in ipsecpeercidrs.cidr VARCHAR(32) (neutron_vpnaas/db/vpn/vpn_models.py:54) [inferred: DB error → 500 on MySQL strict mode]; endpoint groups (VARCHAR(255)) do not have this limit.

Technical sheet

  • Request: {"ipsec_site_connection": {"vpnservice_id", "ikepolicy_id", "ipsecpolicy_id", "peer_address", "peer_id", "psk" (all required), "name", "description", "local_id", "peer_cidrs", "local_ep_group_id", "peer_ep_group_id", "mtu", "initiator", "dpd": {"action", "interval", "timeout"}, "admin_state_up", "project_id"}}.
  • Response: see neutron_vpnaas/db/vpn/vpn_db.py:94-124: id, project_id, tenant_id, name, description, peer_address, peer_id, local_id, route_mode, mtu, auth_mode, psk, initiator, dpd{action, interval, timeout}, admin_state_up, status, vpnservice_id, ikepolicy_id, ipsecpolicy_id, peer_cidrs[], local_ep_group_id, peer_ep_group_id.

PUT /vpn/ipsec-site-connections/{id}

  • Purpose: change tunnel parameters (peer, PSK, MTU, DPD, initiator, endpoint groups or peer CIDRs, admin state).
  • Rules: the same validations run with the previous values as defaults (neutron_vpnaas/db/vpn/vpn_db.py:217-225); refused while PENDING_* (VPNStateInvalidToUpdate, 400, neutron_vpnaas/db/vpn/vpn_db.py:226) — note that the check comes after the validations, so an invalid body on a pending connection returns the validation error. The update does not change the status (no PENDING_UPDATE; observed conn-update: still ACTIVE). peer_cidrs are replaced as a set (neutron_vpnaas/db/vpn/vpn_db.py:228-244).
  • Flow: legacy driver validation (neutron_vpnaas/services/vpn/plugin.py:193-195), UPDATE, OVN extra-route recomputation, cast vpnservice_updated.

DELETE /vpn/ipsec-site-connections/{id}

  • No status check: a pending connection can be deleted. DELETE row (peer CIDRs cascade), OVN extra-route recomputation (routes removed), cast vpnservice_updated (neutron_vpnaas/services/vpn/plugin.py:177-184). The agent's next sync stops or reconfigures the daemon.

VPN agent scheduler (OVN only) — /agents/{agent_id}/vpn-routers, /routers/{router_id}/vpn-agents

Functional description

  • Purpose: let an administrator see and change which neutron-ovn-vpn-agent hosts the VPN services of a router (one agent per router).
  • Actor: administrator. The controllers enforce get_vpn-routers, create_vpn-router, delete_vpn-router, get_vpn-agents with an empty target (neutron_vpnaas/extensions/vpn_agentschedulers.py:52-54, neutron_vpnaas/extensions/vpn_agentschedulers.py:93-95), and neutron-vpnaas registers none of these rules (neutron_vpnaas/policies/__init__.py:22-29), so Neutron's default rule (rule:admin_or_owner) applies. For a non-admin, the owner check cannot be evaluated on an empty target and oslo.policy raises → **500 PolicyCheckError** (observed sched-list-routers-non-admin) instead of 403.
  • Business rules:
    • POST {"router_id"}: the agent must be of type VPN Agent (InvalidVPNAgent, 404, neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:132-139); a router already hosted by another agent → RouterHostedByVPNAgent (409); hosted by the same agent → no-op 201 with an empty body (observed sched-add-router-again); a binding failure → RouterSchedulingFailed (409) (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:187-214);
    • after binding: **RPC call vpnservice_added_to_agent** to the new agent (2 attempts, blocking the API thread up to 2 × rpc_response_timeout, neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:21-52), then the VPN gateway and namespace ports are rebound to the new host (neutron_vpnaas/services/vpn/ovn_plugin.py:78-84, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:463-473);
    • DELETE: removes the binding and cast vpnservice_removed_from_agent (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:216-230). The router then stays unhosted until the next get_vpn_services_on_host of any agent with vpn_auto_schedule = true (default), which re-schedules it (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:99-103);
    • an AMQP notify event vpn_agent.router.add / vpn_agent.router.remove (publisher router) is emitted (neutron_vpnaas/extensions/vpn_agentschedulers.py:187-190).
  • Response bodies: GET …/vpn-routers → {"routers": [<router>]}; GET …/vpn-agents → {"agents": [<agent>]} where the agent dict is Neutron's (on ML2/OVN it comes from the AgentCache, neutron:neutron/plugins/ml2/drivers/ovn/agent/neutron_agent.py:75-93).

Lifecycle: ipsec_site_connection

stateDiagram-v2
  [*] --> PENDING_CREATE: POST (API)
  PENDING_CREATE --> ACTIVE: update_status (agent RPC, SA established)
  PENDING_CREATE --> DOWN: update_status (agent RPC, routed/connecting)
  ACTIVE --> DOWN: report_status (agent timer, tunnel lost / admin down / daemon stopped)
  DOWN --> ACTIVE: report_status (agent timer)
  ACTIVE --> ACTIVE: PUT (API, status unchanged)
  DOWN --> DOWN: PUT (API, status unchanged)
  ACTIVE --> [*]: DELETE (API)
  DOWN --> [*]: DELETE (API)
  PENDING_CREATE --> [*]: DELETE (API)

PENDING_UPDATE and PENDING_DELETE are never set by neutron-vpnaas for connections; the server only writes PENDING_CREATE (neutron_vpnaas/db/vpn/vpn_db.py:189) and the agent's statuses (neutron_vpnaas/db/vpn/vpn_db.py:285-298). While a connection is pending, the server accepts the agent's status only if the agent flags updated_pending_status (neutron_vpnaas/db/vpn/vpn_db.py:297).

Lifecycle: vpnservice

stateDiagram-v2
  [*] --> PENDING_CREATE: POST (API)
  [*] --> ERROR: POST failed in the OVN driver (row kept, 409/500 returned)
  PENDING_CREATE --> ACTIVE: update_status (agent: daemon running with >= 1 connection status)
  PENDING_CREATE --> DOWN: update_status (agent)
  ACTIVE --> DOWN: report_status (agent timer)
  DOWN --> ACTIVE: report_status (agent timer)
  ACTIVE --> [*]: DELETE (API, no connection left)
  DOWN --> [*]: DELETE
  ERROR --> [*]: DELETE

A service is reported ACTIVE when its daemon answers ipsec status with at least one connection line, DOWN otherwise (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:296-313).

Lifecycle: VPN gateway (vpn_ext_gws, OVN only, internal)

stateDiagram-v2
  [*] --> PENDING_CREATE: first POST vpnservice on the router
  PENDING_CREATE --> ACTIVE: gateway port + transit network created
  PENDING_CREATE --> ERROR: port/network creation failed
  ACTIVE --> PENDING_DELETE: last vpnservice of the router deleted
  PENDING_DELETE --> [*]: ports, transit network and row deleted
  PENDING_DELETE --> ERROR: cleanup failed

A gateway in ERROR blocks every new VPN service on that router (VPNGatewayInError, 409) until an operator fixes the database: no API exposes or deletes it.

Source: reverse-spec-neutron-vpnaas/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.