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

Reverse-spec · Services

neutron-vpnaas — Rewrite pack

Rewrite pack — neutron-vpnaas — branch stable/2026.2

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

Target: an OpenKeel (Go, Kubernetes-native) re-implementation of the VPN service on the OVN path, keeping the Networking v2.0 /vpn API compatible for openstacksdk/OSC/Horizon, and either keeping or replacing the agent side. The legacy L3-agent path is documented for completeness but is not a target.

1. Contents of _rewrite/

FileUse in a rewrite
[routes-ovn_stable-2026.2.json](_rewrite/routes-ovn_stable-2026.2.json), [routes-legacy_stable-2026.2.json](_rewrite/routes-legacy_stable-2026.2.json)route table extracted from the running neutron-server per profile: method, path, handler file:line, policy, success status, shadowed (registered again by another extension, never dispatched), routes_artifact (Routes new/edit), and the effective attribute maps of the five collections after all extensions were applied (validators, defaults, allow_post/put, is_visible, is_filter, is_sort_key)
[openapi_stable-2026.2.json](_rewrite/openapi_stable-2026.2.json)OpenAPI 3.0.3 built from the OVN route table and the observed exchanges (14 paths, 35 operations); x-neutron keeps the original attribute definition, x-handler, x-policy, x-profiles, x-not-implemented (the six scheduler routes that answer 500). Types and enums are authoritative; behaviour is in files 01–05
[exchanges-legacy_stable-2026.2.json](_rewrite/exchanges-legacy_stable-2026.2.json), [exchanges-ovn_stable-2026.2.json](_rewrite/exchanges-ovn_stable-2026.2.json)85 + 108 recorded request/response pairs (scenario steps, user, roles), each with the RPC messages it emitted, plus the replies of the server-side RPC endpoints called as an agent would (kind: rpc-server) — the reference for differential testing
[observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md)49 distinct (operation, status, type, message) combinations, UUIDs templated
[rpc-frames_stable-2026.2.json](_rewrite/rpc-frames_stable-2026.2.json)every VPN RPC message (topic, server, version, method, args, caller context) and every agent→server reply recorded in the scenarios
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json)the 25 documented VPN operations of neutron-lib's api-ref, with parameters and samples (outdated on algorithms, see § 8)
[schema-mysql-migrated_stable-2026.2.sql](_rewrite/schema-mysql-migrated_stable-2026.2.sql)the 9 VPN tables as created by neutron-db-manage upgrade heads on MariaDB 11.4 — the production schema
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql), [schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md)DDL from the ORM models; the diff against the migrated schema is empty (columns, enum values and order, nullability, PK, unique, FK and ON DELETE)
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml)the 20 default rules (ADMIN_OR_PROJECT_MEMBER, project scope) with their deprecated forms
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf)server options (vpn_scheduler_driver, vpn_auto_schedule, allow_automatic_vpnagent_failover, [service_providers])
[config-ovn-vpn-agent_stable-2026.2.ini](_rewrite/config-ovn-vpn-agent_stable-2026.2.ini), [config-vpn-agent_stable-2026.2.ini](_rewrite/config-vpn-agent_stable-2026.2.ini)agent option samples ([vpnagent], [ipsec], [strongswan], [libreswan], [pluto], [ovs], [ovn]); template paths shown as <site-packages>/…
tools/vpnaas_capture.py, tools/summarize.py, tools/build_openapi.pythe reproducible extraction: vpnaas_capture.py <legacy|ovn> <vpnaas> <neutron> <neutron-lib> <out> <slug> loads Neutron's paste pipeline with a reference profile, walks the Routes mapper, replays the scenario, intercepts oslo.messaging cast/call
[rewrite-pack-index_stable-2026.2.md](_rewrite/rewrite-pack-index_stable-2026.2.md)index

Extraction environment: Python 3.12.7 venv with neutron-lib, neutron and neutron-vpnaas installed from the local clones and requirements stable/2026.2 @ 3b75407b upper-constraints (oslo.messaging 18.3.1, oslo.db 18.1.1, SQLAlchemy 2.0.52, ovsdbapp 2.19.0, WebOb 1.8.11). Capture profile: SQLite in memory, fake:// transport, auth_strategy = noauth with the identity injected by NeutronKeystoneContext, enforce_scope and enforce_new_defaults on. The OVN profile runs the OVN service plugin and driver on an ML2/openvswitch core so that no OVN database is needed: the VPN agent is an agents row instead of an AgentCache entry, and the RPC frames contain a few unrelated ML2/OVS notifications (neutron-vo-*, l3_agent routers_updated) that a real OVN deployment does not send.

2. HTTP behaviour that the contracts do not show

2.1 Pipeline

The VPN API is served by Neutron (neutron:etc/neutron/api-paste.ini): cors → http_proxy_to_wsgi → request_id → catch_errors → osprofiler → authtoken → keystonecontext → extensions (Routes) → base Controller. Consequences a rewrite must reproduce:

  • authentication failures (401) come from keystonemiddleware before any VPN code;
  • the body is validated against the attribute map before the policy check of the attributes and before the plugin; unknown keys → 400 Unrecognized attribute(s); read-only keys on PUT → 400 Cannot update read-only attribute;
  • GET of another project's resource → 404 with the plugin's …NotFound type (the plugin filters by project); a reader without member gets 404 HTTPNotFound on GET and 403 PolicyNotAuthorized on writes (observed);
  • uncaught exceptions → 500 HTTPInternalServerError with the generic message.

2.2 Error body

{"NeutronError": {"type": "<ExceptionClass>", "message": "<text>", "detail": ""}}. Clients (openstacksdk) surface message; no client parses type for VPN [inferred from openstacksdk:openstack/network/v2/vpn_*.py, which declare no error mapping]. Keep the exact type names anyway: they are the only machine-readable code.

2.3 Other cross-cutting rules

  • Responses carry project_id and tenant_id (same value). Requests accept either.
  • Collections support fields, filters on every is_filter attribute (including psk, which openstacksdk exposes as a query parameter: openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:27-47), sorting and pagination.
  • Bulk create is emulated (native_bulk: false): {"ikepolicies": [ … ]} creates one by one inside Neutron's controller [inferred from Neutron's base controller].
  • DELETE returns 204 with an empty body; POST 201.
  • The extension list (GET /v2.0/extensions) advertises vpnaas, vpn-endpoint-groups, vpn-flavors, vpn-aes-ccm-gcm, vpn-aes-ctr, vpn-no-sha1-3des, and on OVN vpn-agent-scheduler (route tables). Clients and Horizon gate features on these aliases.

3. Core algorithms: formal specification

3.1 Effective attribute validation (the extension merge)

Neutron applies extensions in order and replaces each extended attribute definition (neutron:neutron/api/extensions.py:353-364); every extension's own map is then overwritten with the merged result (neutron-lib:neutron_lib/api/extensions.py:146-149). Order = sorted file names in neutron_vpnaas/extensions/ (neutron:neutron/api/extensions.py:447). Result: vpn-no-sha1-3des (processed after vpn-aes-ctr) defines the final auth_algorithm and encryption_algorithm lists, computed from the base lists without CTR (neutron-lib:neutron_lib/api/definitions/vpn_no_sha1_3des.py:33-41). The effective lists are in 01 § Effective attribute validation and in the route tables.

Rewrite decision: reproducing the original means rejecting aes-*-ctr while advertising vpn-aes-ctr. Recommended: accept CTR (the DB enum, the strongSwan and Libreswan dialect maps already support it, neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:81-83, neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:64-66) and record the deviation.

3.2 Connection validation (order matters for the error returned)

For POST /vpn/ipsec-site-connections (neutron_vpnaas/services/vpn/plugin.py:166-175, neutron_vpnaas/db/vpn/vpn_db.py:153-203):

driver_validator(conn)                              # legacy only: ah-esp → 400 IpsecValidationFailure
apply dpd defaults {hold, 30, 120}
404 if vpnservice, ikepolicy, ipsecpolicy missing   # in this order
mode check (service.subnet_id set?)                 # MissingPeerCidrs | InvalidEndpointGroup | PeerCidrsInvalid | MissingRequiredEndpointGroup
if endpoint groups:
    local group exists (404) / type == subnet (WrongEndpointGroupType)
    each local subnet has a port on service.router (SubnetIsNotConnectedToRouter)
    local subnets same IP version (MixedIPVersionsForIPSecEndpoints)
    peer group exists / type == cidr; peer CIDRs same IP version
else:
    peer_cidrs valid CIDRs (IPsecSiteConnectionPeerCidrError), same version (MixedIPVersionsForPeerCidrs)
    local version = service.subnet.ip_version
local version == peer version (MixedIPVersionsForIPSecConnection)
dpd.timeout > dpd.interval (IPsecSiteConnectionDpdIntervalValueError)
mtu >= {4: 68, 6: 1280}[local version] (IPsecSiteConnectionMtuError)
peer_address: IP, else getaddrinfo()[0] replaces it (VPNPeerAddressNotResolved)
router gateway has a fixed IP of the peer's version (ExternalNetworkHasNoSubnet)
INSERT … status=PENDING_CREATE, route_mode=static, auth_mode=psk

PUT runs the same chain with previous values as defaults (neutron_vpnaas/db/vpn/vpn_validator.py:221-227), then the pending-state check, then a set-wise replacement of peer_cidrs; status is not changed.

3.3 Status reconciliation (agent ⇄ server)

  • Server rule (neutron_vpnaas/db/vpn/vpn_db.py:285-298, neutron_vpnaas/db/vpn/vpn_db.py:761-763): write(new) iff not pending(current) or report.updated_pending_status.
  • Agent rule (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:760-811): per router, one status capture; report iff pending flag, service status or connection-status map differs from the cache; first report after start forces updated_pending_status on every connection; vanished connections reported DOWN + flag; cache updated before the RPC.
  • Status derivation: strongSwan INSTALLED → ACTIVE, ROUTED|CONNECTING → DOWN; Libreswan "established … newest" → ACTIVE, other lines → DOWN; exit code 1|3 → no connection; service = ACTIVE iff ≥ 1 connection line. A Kubernetes-native agent (controller watching the VPN objects, or a DaemonSet per gateway node) can keep the same server rule and replace the polling by strongSwan's vici events [suggestion].

3.4 Scheduling (OVN)

  • Candidates: live agents of type VPN Agent (neutron_vpnaas/scheduler/vpn_agent_scheduler.py:86-91); AZ variant: agents whose AZ is in the router's availability_zone_hints or [DEFAULT] default_availability_zones (neutron_vpnaas/scheduler/vpn_agent_scheduler.py:155-185).
  • Choice: LeastRoutersScheduler — an agent with zero routers if any (arbitrary among them: set.pop()), else the one with the smallest binding count (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:367-382); ChanceScheduler — random.choice. Not deterministic: tests must compare the set of valid outcomes.
  • A router already bound keeps its agent; one agent per router (unique key).
  • Auto-schedule of all unhosted routers to the calling agent on every get_vpn_services_on_host when vpn_auto_schedule (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:99-103) — with the default scheduler this ignores load balancing entirely (whichever agent syncs first takes every unhosted router).
  • Failover: § 05 § 7.

3.5 OVN data-plane model

Per router with ≥ 1 VPN service:

  • VPN gateway port on the external network (own public IP = tunnel endpoint, vpnservice.external_v4_ip/v6_ip), bound to the agent host;
  • transit network/subnet 169.254.0.0/28 attached to the router at 169.254.0.1; namespace port 169.254.0.2;
  • router extra routes: each peer CIDR → 169.254.0.2; protected against removal;
  • agent namespace qvpn-<router_id>: external interface vg… with the gateway IP and default route, internal vr… 169.254.0.2/28, routes to each local CIDR via 169.254.0.1, IP forwarding on; strongSwan auto=route connections between the gateway IP and the peer. On Kubernetes/OVN (or Kube-OVN) the same model maps to a pod (or a Multus-attached gateway) with two interfaces: one on the provider network with the public IP, one on a per-router transit segment, plus static routes on the logical router [suggestion].

3.6 Configuration rendering

Inputs: the get_vpn_services_on_host dict (03 § Sample frames). Translation tables: neutron_vpnaas/services/vpn/device_drivers/ipsec.py:137-166 (common), neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:76-123, neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:61-92. Templates: neutron_vpnaas/services/vpn/device_drivers/template/strongswan/ipsec.conf.template:1-38, neutron_vpnaas/services/vpn/device_drivers/template/libreswan/ipsec.conf.template:1-91. Rules: ike=<enc>-<auth>-<dh>; ESP uses <enc>-<dh> (no auth) when the encryption is CCM/GCM ('cm' in encryption_algorithm); AH uses ah=<auth>-<dh>; ah-esp has no rendering (strongSwan would get an ESP line); lifetimes in seconds; local_id defaults to the tunnel IP; non-IP IDs prefixed with @; PSK as 0s<base64>. transform_protocol=ah-esp must be rejected by an OVN rewrite too (the original accepts it on OVN, § 8).

3.7 Test data

The project's tests are Python unit tests with mocks; there is no gabbi or Tempest-style suite in the repository, so no conformance_capture.py run was made. The language-independent conformance data are the two exchange files (193 exchanges), produced by the real server, and the observed-error table. Not encoded: concurrency (duplicate gateway creation, double scheduling), RPC timeouts, the agent's command execution (no namespaces on the capture host).

4. What the clients expect

ExpectationWhere the client relies on it
resource keys and paths ipsec_site_connection(s) at /vpn/ipsec-site-connections, vpnservice(s), ikepolicy/ikepolicies, ipsecpolicy/ipsecpolicies, endpoint_group(s) at /vpn/endpoint-groupsopenstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:17-19 and the sibling vpn_*.py files
query parameters accepted as filters (psk, peer_address, status, admin_state_up, external_v4_ip…)openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:27-47, openstacksdk:openstack/network/v2/vpn_service.py:30-41
project_id with tenant_id alias; dpd as an object; peer_cidrs as a listopenstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:62, openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:107-109
PUT with partial bodies (only changed attributes)openstacksdk commit [inferred from allow_commit = True, openstacksdk:openstack/network/v2/vpn_ipsec_site_connection.py:23]
the agents (if kept unchanged) require the RPC contract of 03 § RabbitMQ: topics, method names, argument names, version 1.0, reply shapes including the ORM-shaped gw_port/transit_port dictsneutron_vpnaas/services/vpn/device_drivers/ipsec.py:429-455, neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:120-179
Neutron itself expects the callbacks (router interface/gateway/subnet/port/network delete protections) and the ovn_maintenance_periodics hook05 § 12

5. Database strategy

  • Reuse the schema (it matches the migrations exactly). Keep the enum value lists and their order; ipsecpeercidrs.cidr VARCHAR(32) should be widened to 43+ for IPv6 (or peer CIDRs stored only through endpoint groups).
  • Integrity is in FKs (unlike Keystone); routervpnagentbindings.vpn_agent_id has no FK because OVN agents are not rows.
  • No locking: concurrency relies on the unique keys vpn_ext_gws.router_id and routervpnagentbindings.router_id, and on retrying the whole transaction (RetryRequest).
  • Statuses are free strings (VARCHAR(16)); values used: PENDING_CREATE, ACTIVE, DOWN, ERROR, and PENDING_DELETE for gateways.
  • The neutron-vpnaas tables reference Neutron core tables (routers, subnets, ports, networks, flavors): a separate OpenKeel VPN service must either share Neutron's database or replace these FKs by API lookups and clean-up hooks (router/subnet deletion protections are callbacks today).

6. Authentication, policies, configuration and CLI

  • Policies: 20 rules, ADMIN_OR_PROJECT_MEMBER, project scope (_rewrite/policies_stable-2026.2.yaml). The scheduler API has no rules: the rewrite should define get_vpn-routers, create_vpn-router, delete_vpn-router, get_vpn-agents as admin-only and return 403 (the original returns 500 to non-admins).
  • Configuration: server [service_providers] service_provider = VPN:<name>:<driver>:default, vpn_scheduler_driver, vpn_auto_schedule, allow_automatic_vpnagent_failover, agent_down_time (Neutron); agent options in the two .ini samples.
  • Start-up guard: the server refuses to start if a VPN service references a removed provider (neutron_vpnaas/services/vpn/plugin.py:87-111).
  • Root helper: the agents need root (rootwrap filters etc/neutron/rootwrap.d/vpnaas.filters), namespace creation, mount --bind inside the namespace, and the ipsec binaries.

7. Conformance strategy

  1. Replay exchanges-ovn_stable-2026.2.json against the rewrite in a fresh database, in order: for each step compare status, NeutronError.type, and the response body with UUIDs and timestamps abstracted (same approach as tools/summarize.py); the setup-* steps create Neutron networks/routers and need a Neutron (or equivalent) API.
  2. Validate every success body against openapi_stable-2026.2.json component schemas.
  3. RPC: if the agents are kept, compare the emitted frames of each step (rpc arrays) by (mode, topic, server, method, args); if the agents are replaced, the frames define the information each node must receive.
  4. Algorithms: scheduling outcomes must be in the valid set (§ 3.4); status reconciliation can be unit-tested from § 3.3. Original test fixtures for reference: neutron_vpnaas/tests/unit/db/vpn/test_vpn_db.py, neutron_vpnaas/tests/unit/services/vpn/service_drivers/test_ovn_ipsec.py, neutron_vpnaas/tests/unit/services/vpn/device_drivers/test_ipsec.py [not replayed].

8. Gaps found while building the pack

  • vpn-aes-ctr advertised but CTR rejected (§ 3.1; observed).
  • OVN: ah-esp accepted (observed), no rendering for it.
  • OVN: router gateway can be cleared under an active VPN service (observed 200); legacy refuses (409).
  • OVN: failed POST vpnservices leaves an ERROR row behind (observed).
  • Scheduler API: 500 for non-admins (missing rules), six routes without controller methods (500), duplicate POST binding returns 201.
  • ikepolicies/ipsecpolicies routes registered three times (by vpnaas, vpn-aes-ctr, vpn-no-sha1-3des, neutron_vpnaas/extensions/vpn_aes_ctr.py:26-37, neutron_vpnaas/extensions/vpn_no_sha1_3des.py:26-37); the first wins; harmless but 20 shadowed routes.
  • Dead code: VPNStateInvalidToUpdate reference in set_vpnservice_status (neutron_vpnaas/db/vpn/vpn_db.py:522), RouterHasVPNExternal, Libreswan _get_nexthop, agent_updated (no handler), AZVPNAgentSchedulerDbMixin.get_router_availability_zones uses router.vpn_agents, a relationship that does not exist (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:395-396).
  • api-ref outdated: sha1/3des still listed and used in samples; missing 409 on PUT policy, 400 on endpoint-group delete in use, scheduler API absent.
  • PSK readable by every project member through GET and filterable.

9. Non-functional requirements

  • API latency: POST vpnservices on OVN performs ~10 core/L3 plugin calls (ports, network, subnet, router interface) in one request; POST connection with an FQDN does a synchronous DNS lookup.
  • vpnservice_added_to_agent blocks the API thread up to 2 × rpc_response_timeout.
  • Agent polling: 3 processes per router per minute (ip netns exec, wrapper, ipsec).
  • Failover detection: between agent_down_time and agent_down_time + agent_down_time // 2 seconds after an agent stops (75–112 s with defaults), only with allow_automatic_vpnagent_failover.
  • Secrets: PSK in clear in DB, API, RPC; base64 (not encryption) in ipsec.secrets (0600).

10. Rewrite checklist

  • [ ] Serve the 25 VPN operations (+4 scheduler operations, admin-only) with the attribute maps of routes-ovn_stable-2026.2.json; decide on CTR (§ 3.1).
  • [ ] Error bodies NeutronError{type,message,detail} with the exact types and messages of observed-errors_stable-2026.2.md; 404 for foreign resources; 403 vs 404 per role.
  • [ ] Validation chain and order of § 3.2, including DNS resolution and IP-version checks; reject ah-esp on every driver.
  • [ ] PENDING_CREATE on create; no status change on update; pending-state update refusal; server status rule § 3.3.
  • [ ] In-use protections: policies (409), endpoint groups (400), services (409); router interface / subnet / port / network / extra-route / gateway protections (decide whether the OVN gateway-clear protection should exist).
  • [ ] OVN gateway model § 3.5 (gateway port, transit network, extra routes) with retry on concurrent creation and clean-up of the last service; delete the ERROR row on failed creation.
  • [ ] Agent registration and liveness (SB Chassis_Private keys or a Kubernetes-native equivalent), scheduling § 3.4, failover § 05 § 7.
  • [ ] Agent data plane: namespace/pod per router, strongSwan configuration § 3.6, status reporting § 3.3.
  • [ ] Policies file compatible with policies_stable-2026.2.yaml (+ scheduler rules).
  • [ ] Replay exchanges-ovn_stable-2026.2.json (§ 7) and validate against openapi_stable-2026.2.json.

Source: reverse-spec-neutron-vpnaas/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.