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

Reverse-spec · Services

neutron-vpnaas — Interactions

Interactions — 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.

SystemProtocolLibraryDirectionSync/asyncProfile
Neutron database (MariaDB)SQLSQLAlchemy 2 / oslo.db via neutron_lib.db.apineutron-server → DBsyncboth
RabbitMQAMQP 0-9-1, oslo.messaging RPCneutron_lib.rpcserver ↔ agentscast/callboth
RabbitMQ (notifications)oslo.messaging notifierneutron_lib.rpc.get_notifierserver → busasyncOVN (scheduler API)
OVN Southbound DBOVSDB JSON-RPC (RFC 7047), TCP/SSL 6642ovsdbapp / python-ovs IDLserver (read, events) and VPN agent (read/write)async events + transactionsOVN
OVN Northbound DBOVSDBvia Neutron's ML2/OVN (not called directly)server → NBsyncOVN
Local Open vSwitch DBOVSDB, [ovs] ovsdb_connection (default unix:/usr/local/var/run/openvswitch/db.sock)ovsdbappVPN agent → local OVSDBsyncOVN
Neutron core / L3 pluginsin-process Python callsneutron_lib.plugins.directoryservice plugin → core/L3 pluginssyncboth
Linux kernel / processesip netns exec, ipsec, iptables, sysctl, mount --bindrootwrap/privsep, neutron-vpn-netns-wrapperagents → hostsyncboth
DNSgetaddrinfoPython socketneutron-server → resolversync, inside the API requestboth

There is no cache (Redis/memcached) and no outbound REST call in neutron-vpnaas: everything goes through Neutron's plugins in the same process.

Database

The schema is managed by the Neutron sub-project branch neutron-vpnaas (pyproject.toml:54-55); heads e50641731f1a (contract) and 842256a43ce2 (expand, neutron_vpnaas/db/migration/alembic_migrations/versions/2026.2/expand/842256a43ce2_remove_deprecated_sha1_and_3des.py:28-29). The migrated schema (_rewrite/schema-mysql-migrated_stable-2026.2.sql, MariaDB 11.4) and the ORM models are identical (_rewrite/schema-diff_stable-2026.2.md).

TableModelPurposeForeign keys
ikepoliciesneutron_vpnaas/db/vpn/vpn_models.py:86IKE policies—
ipsecpoliciesneutron_vpnaas/db/vpn/vpn_models.py:62IPsec policies—
vpnservicesneutron_vpnaas/db/vpn/vpn_models.py:158VPN services (status, tunnel IPs)subnets.id, routers.id, flavors.id
ipsec_site_connectionsneutron_vpnaas/db/vpn/vpn_models.py:109connections (PSK in clear, VARCHAR(255))vpnservices, ikepolicies, ipsecpolicies, vpn_endpoint_groups ×2
ipsecpeercidrsneutron_vpnaas/db/vpn/vpn_models.py:51legacy peer CIDRs, PK (cidr, ipsec_site_connection_id), cidr VARCHAR(32)ipsec_site_connections ON DELETE CASCADE
vpn_endpoint_groupsneutron_vpnaas/db/vpn/vpn_models.py:194endpoint groups (endpoint_type enum subnet,cidr,vlan,network,router)—
vpn_endpointsneutron_vpnaas/db/vpn/vpn_models.py:179endpoints, PK (endpoint, endpoint_group_id)vpn_endpoint_groups ON DELETE CASCADE
vpn_ext_gwsneutron_vpnaas/db/vpn/vpn_ext_gw_db.py:54OVN: per-router VPN gateway (status, gateway port, transit port/network/subnet), router_id uniquerouters.id; ports.id ×2, networks.id, subnets.id ON DELETE SET NULL
routervpnagentbindingsneutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:54OVN: router → VPN agent, PK (router_id, vpn_agent_id), router_id uniquerouters.id ON DELETE CASCADE; **no FK to agents** (OVN agents are not in the agents table)

Neutron tables read or written: routers, ports/ipallocations/portbindings (gateway port IPs, VPN ports), subnets, networks, routerl3agentbindings (legacy hosting), agents (legacy L3 agent), providerresourceassociations (service → driver), flavors/flavorserviceprofilebindings, routerroutes (OVN extra routes).

Tables per operation

OperationReadsWrites
IKE / IPsec policy CRUDikepolicies/ipsecpolicies, ipsec_site_connections (in-use check)ikepolicies/ipsecpolicies
Endpoint group CRUDvpn_endpoint_groups, vpn_endpoints, subnets (validation), ipsec_site_connections (in-use)vpn_endpoint_groups, vpn_endpoints
POST vpnservice (legacy)routers, ports (gateway), subnets, flavorsvpnservices, providerresourceassociations
POST vpnservice (OVN)+ agents (AgentCache), routervpnagentbindings, vpn_ext_gws+ routervpnagentbindings, vpn_ext_gws, Neutron ports, networks, subnets, router interface
POST/PUT connectionvpnservices, ikepolicies, ipsecpolicies, vpn_endpoint_groups, vpn_endpoints, subnets, ports, routersipsec_site_connections, ipsecpeercidrs; OVN: routerroutes
DELETE vpnservice (OVN, last on router)vpnservices, vpn_ext_gwsdelete vpnservices, Neutron ports/subnet/network, vpn_ext_gws
get_vpn_services_on_hostjoin vpnservices ⋈ ipsec_site_connections ⋈ (routerl3agentbindings ⋈ providerresourceassociations | routervpnagentbindings), vpn_endpoints ⋈ subnetsOVN: routervpnagentbindings (auto-schedule)
update_statusvpnservices, ipsec_site_connectionsstatus columns
check_vpn_agentsroutervpnagentbindingsdelete/insert routervpnagentbindings, ports (binding host)

Representative queries (reconstructed)

-- in-use check before updating/deleting an IKE policy (neutron_vpnaas/db/vpn/vpn_db.py:349-351)
SELECT * FROM ipsec_site_connections WHERE ikepolicy_id = :id LIMIT 1;

-- subnet used by a connection's local endpoint group on a router (neutron_vpnaas/db/vpn/vpn_db.py:587-604)
SELECT c.* FROM ipsec_site_connections c
  JOIN vpn_endpoint_groups g ON g.id = c.local_ep_group_id AND g.endpoint_type = 'subnet'
  JOIN vpn_endpoints e ON e.endpoint_group_id = c.local_ep_group_id AND e.endpoint = :subnet_id
  JOIN vpnservices s ON s.id = c.vpnservice_id AND s.router_id = :router_id
LIMIT 1;

-- routers with VPN services but no VPN agent (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:338-348)
SELECT s.router_id FROM vpnservices s
  LEFT OUTER JOIN routervpnagentbindings b ON s.router_id = b.router_id
 WHERE b.vpn_agent_id IS NULL;

-- least-loaded agent (neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:367-382)
SELECT vpn_agent_id, count(router_id) AS count FROM routervpnagentbindings
 WHERE vpn_agent_id IN (:candidates) GROUP BY vpn_agent_id ORDER BY count;
-- a candidate absent from the result (0 routers) wins; ties among them are broken by set.pop() (arbitrary)

Transactions and locks

  • Every write uses CONTEXT_WRITER (one transaction per method); there is no SELECT … FOR UPDATE.
  • Concurrency is arbitrated by unique keys: vpn_ext_gws.router_id (two first services on the same router: the second insert fails with DBDuplicateEntry, retried by @retry_if_session_inactive until the first gateway is ACTIVE, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:350-370), and routervpnagentbindings.router_id (DBDuplicateEntry → "already scheduled", neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:164-185).
  • The OVN _setup runs inside one CONTEXT_WRITER that wraps calls to the core and L3 plugins (port, network, subnet, router interface creation) (neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:373-405); the gateway row is updated to ERROR in that same transaction before re-raising [inferred: whether the ERROR update survives depends on the outer transaction rollback; the observed service row did keep ERROR].
  • In-use checks are check-then-act inside the same transaction (no lock): a connection created concurrently with a policy delete may reference a deleted policy only if the FK allows it — it does not (FKs exist), so the DB rejects it [inferred].

RabbitMQ (oslo.messaging RPC)

Topology

ExchangeRouting key / queueProducerConsumer
neutron (topic)ipsec_driverL3/OVN VPN agents (call)neutron-server, IPsecVpnDriverCallBack / IPsecVpnOvnDriverCallBack
neutron (topic)ipsec_agent.<host>neutron-server (cast/call with server=<host>)the agent on <host> (device driver object)
neutron (topic)reply_<uuid>consumer of a callcaller
neutron notifications (notifications.info)event types vpn_agent.router.add, vpn_agent.router.removeneutron-server, publisher router.<host>any listener

Messages

MethodModeTopic / serverVersionArgumentsReplySource
vpnservice_updatedcastipsec_agent / agent host1.0router: {id}—neutron_vpnaas/services/vpn/service_drivers/__init__.py:106-110
get_vpn_services_on_hostcallipsec_driver1.0hostlist of service dicts (05 § 2)neutron_vpnaas/services/vpn/device_drivers/ipsec.py:438-445
update_statuscallipsec_driver1.0status: [...] (05 § 3)nullneutron_vpnaas/services/vpn/device_drivers/ipsec.py:448-455
get_vpn_transit_network_detailscallipsec_driver1.0router_id{gw_port, transit_port, transit_gateway_ip, external_network}neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:211-214
get_subnet_infocallipsec_driver1.0subnet_idsubnet dict or nullneutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:216-219
vpnservice_added_to_agentcall ×2 attemptsipsec_agent / hostdefault (1.0)router_ids: [...]nullneutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:44-52
vpnservice_removed_from_agentcastipsec_agent / hostdefaultrouter_id—neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:38-42
agent_updatedcastipsec_agent / hostdefaultpayload: {admin_state_up}— (no handler)neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:33-36

Agent → server calls use an admin context without session (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:477, neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:209); server → agent messages carry the caller's context (user, project, roles, request ID) for API-triggered casts, or an admin context for scheduling. Timeouts: [DEFAULT] rpc_response_timeout (60 s) for every call.

Sample frames (observed payloads, envelope reconstructed)

vpnservice_updated, cast by neutron-server to the agent of vpn-host-1 after POST /vpn/ipsec-site-connections (routing key ipsec_agent.vpn-host-1):

{"oslo.version": "2.0",
 "oslo.message": "{\"method\": \"vpnservice_updated\", \"args\": {\"router\": {\"id\": \"325d31a5-e906-49d6-8eb9-0886290aeede\"}}, \"version\": \"1.0\", \"namespace\": null, \"_unique_id\": \"<uuid>\", \"_context_user_id\": \"alice-user\", \"_context_project_id\": \"project-a\", \"_context_roles\": [\"member\", \"reader\"], \"_context_is_admin\": false, \"_context_request_id\": \"req-9cbec1f8-ce1a-4568-84e6-b22f542e8b78\"}"}

update_status, call from an agent (routing key ipsec_driver, reply on reply_<uuid>):

{"method": "update_status", "version": "1.0", "_msg_id": "<uuid>", "_reply_q": "reply_<uuid>",
 "args": {"status": [{"id": "f68add5a-29b7-4301-93b3-0f8bee7d3bde", "status": "ACTIVE", "updated_pending_status": true,
   "ipsec_site_connections": {"8c4abb91-8577-4e96-80de-208dbe3ea6eb": {"status": "ACTIVE", "updated_pending_status": true}}}]},
 "_context_is_admin": true, "_context_project_id": null}

get_vpn_services_on_host reply (one service, abridged; full recording in _rewrite/exchanges-ovn_stable-2026.2.json):

[{"id": "<vpnservice>", "router_id": "<router>", "status": "PENDING_CREATE", "admin_state_up": true,
  "subnet": {"cidr": "10.0.0.0/24"}, "external_ip": "172.24.4.167", "external_v4_ip": "172.24.4.167", "external_v6_ip": null,
  "ipsec_site_connections": [{"id": "<conn>", "peer_address": "172.24.4.233", "peer_id": "172.24.4.233", "local_id": "",
     "psk": "secret", "mtu": 1500, "initiator": "bi-directional", "dpd_action": "hold", "dpd_interval": 30, "dpd_timeout": 120,
     "status": "PENDING_CREATE", "admin_state_up": true,
     "ikepolicy": {"auth_algorithm": "sha256", "encryption_algorithm": "aes-128", "phase1_negotiation_mode": "main",
                   "lifetime_units": "seconds", "lifetime_value": 3600, "ike_version": "v1", "pfs": "group5", "…": "…"},
     "ipsecpolicy": {"transform_protocol": "esp", "encapsulation_mode": "tunnel", "…": "…"},
     "peer_cidrs": ["192.168.10.0/24", "192.168.20.0/24"], "local_cidrs": ["10.0.0.0/24", "10.1.0.0/24"],
     "local_ip_vers": 4, "external_ip": "172.24.4.167"}]}]

Notification

vpn_agent.router.add / vpn_agent.router.remove, priority INFO, publisher router.<host>, payload {"agent": {"id": "<agent_id>", "router_id": "<router_id>"}} (neutron_vpnaas/extensions/vpn_agentschedulers.py:187-190).

OVN Southbound (OVSDB)

ActorTablesOperationsSource
neutron-server (every worker, via the L3 plugin's SB IDL)Chassis_Privatemonitor; event ChassisVPNAgentWriteEventneutron_vpnaas/services/vpn/ovn/agent_monitor.py:48-89
OVN VPN agentSB_Global (monitor), Chassis (monitor), Chassis_Private (monitor of own row, write external_ids)db_add Chassis_Private <chassis> external_ids {neutron:ovn-vpnagent-id: <uuid5>}; db_get … nb_cfg; db_set … external_ids {neutron:ovn-vpnagent-sb-cfg: <n>}neutron_vpnaas/agent/ovn/vpn/agent.py:143-158

Keys (neutron_vpnaas/services/vpn/common/constants.py:36-38): neutron:ovn-vpnagent-sb-cfg, neutron:description-vpnagent, neutron:ovn-vpnagent-id. Connection settings come from Neutron's [ovn] options (ovn_sb_connection, SSL files, ovsdb_timeout, ovsdb_probe_interval) (neutron_vpnaas/agent/ovn/vpn/ovsdb.py:33-62), registered by neutron_vpnaas/services/vpn/ovn_agent.py:55-62.

Sample transaction sent by the agent on each SB_Global update (RFC 7047 transact):

{"id": 42, "method": "transact", "params": ["OVN_Southbound",
  {"op": "mutate", "table": "Chassis_Private", "where": [["name", "==", "<chassis>"]],
   "mutations": [["external_ids", "delete", ["set", ["neutron:ovn-vpnagent-sb-cfg"]]],
                 ["external_ids", "insert", ["map", [["neutron:ovn-vpnagent-sb-cfg", "1234"]]]]]}]}

(ovsdbapp's db_set on a map column; the exact operation list is library-generated [inferred].)

The Northbound DB is touched only indirectly: the gateway and namespace ports, the transit network/subnet, the router interface and the router's extra routes are created through Neutron's plugins, and ML2/OVN translates them into NB Logical_Switch_Port, Logical_Switch, Logical_Router_Port and Logical_Router_Static_Route rows. ping_all_chassis bumps NB_Global.nb_cfg (05 § 7).

Local Open vSwitch DB (OVN agent)

Read Open_vSwitch.external_ids:system-id to find the chassis name (neutron_vpnaas/agent/ovn/vpn/agent.py:160-168); the interface driver ([DEFAULT] interface_driver = openvswitch) creates the internal ports of the qvpn- veths on br-int with external_ids:iface-id=<neutron port id>, which makes ovn-controller bind them [inferred from Neutron's OVS interface driver].

Neutron plugins (in-process)

CallTargetPurposeSource
l3_plugin.get_routerL3gateway check, AZ hints, extra routesneutron_vpnaas/db/vpn/vpn_validator.py:276, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:305
core_plugin.get_ports(filters={fixed_ips: subnet, device_id: router})core"is this subnet on the router"neutron_vpnaas/db/vpn/vpn_validator.py:281-285
core_plugin.get_subnetcoreendpoint-group validation, get_subnet_infoneutron_vpnaas/db/vpn/vpn_validator.py:323, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:90
l3_plugin.get_l3_agents_hosting_routersL3legacy notification targetsneutron_vpnaas/services/vpn/service_drivers/__init__.py:92-95
core_plugin.get_agents / get_agentcore (ML2/OVN: AgentCache)VPN agents, livenessneutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:320-336
create_network, create_subnet, create_port, l3_plugin.add_router_interfacecore / L3OVN gateway and transit objectsneutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:169-273
delete_port(l3_port_check=False), remove_router_interface, delete_subnet, delete_networkcore / L3OVN cleanup (NotFound ignored)neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:215-256
l3_plugin.add_extraroutes / remove_extraroutesL3OVN routes to peer CIDRs via 169.254.0.2neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:314-321
core_plugin.update_port({'binding:host_id'})corerebind VPN ports after reschedulingneutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:459-461
ServiceTypeManager (add_resource_association, get_provider_names_by_resource_ids, del_resource_associations)Neutron service typesservice → driverneutron_vpnaas/services/vpn/plugin.py:87-157
flavors plugin (get_flavor, get_flavor_next_provider)flavorsprovider from flavorneutron_vpnaas/services/vpn/plugin.py:129-150

Host commands (agents)

All commands run as root through Neutron's root helper; the ipsec commands additionally go through neutron-vpn-netns-wrapper (pyproject.toml:48), allowed by etc/neutron/rootwrap.d/vpnaas.filters.

CommandWhenSource
ipsec --piddirstrongSwan process object creationneutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:129-131
ip netns exec <ns> neutron-vpn-netns-wrapper --mount_paths=/etc:<cfg>/etc,<piddir>:<cfg>/var/run [--rootwrap_config=…] --cmd=ipsec,<args>every strongSwan command (start, stroke up-nb <id>, status, rereadsecrets, reload, stop)neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:147-163
… --mount_paths=/etc:<cfg>/etc,/run:<cfg>/var/run --cmd=ipsec,<args>Libreswan (_stackmanager start/start, checknss, pluto …, whack --listen/--status/--initiate/--terminate/--shutdown)neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:107-127
inside the wrapper: mount --bind <new> <path> then the commandeach wrapped commandneutron_vpnaas/services/vpn/common/netns_wrapper.py:141-156
rm -rf <cfg>, rm -rf/cp -a <strongswan.d>, rm -f ipsec.secrets, chown --from=<uid> root:root …config handlingneutron_vpnaas/services/vpn/device_drivers/ipsec.py:262-265, neutron_vpnaas/services/vpn/device_drivers/strongswan_ipsec.py:165-171, neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:140-158
iptables (via Neutron's IptablesManager): -A neutron-l3-agent-POSTROUTING -s <local> -d <peer> -m policy --dir out --pol ipsec -j ACCEPTlegacy, per local/peer IPv4 pairneutron_vpnaas/services/vpn/device_drivers/ipsec.py:558-563
ip netns add qvpn-<router>, sysctl -w net.ipv4.ip_forward=1 (and IPv6 forwarding if enabled), veth plug, ip addr, ip route add default via <ext gw>, ip route add <local cidr> via 169.254.0.1 proto static metric 100, ip netns deleteOVN namespace managementneutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:69-204

Files written (per router, [ipsec] config_base_dir, default $state_path/ipsec/<router_id>/)

etc/ipsec.conf, etc/ipsec.secrets (0600, contains the PSKs base64-encoded with the 0s prefix), strongSwan: etc/strongswan.conf and a copy of /etc/strongswan.d in etc/strongswan.d; Libreswan: etc/ipsec.conf.old/ipsec.secrets.old when restart_check_config, etc/hosts, etc/resolv.conf, etc/pki/nssdb/, var/run/pluto.pid|.ctl, log/ (neutron_vpnaas/services/vpn/device_drivers/ipsec.py:120-135, neutron_vpnaas/services/vpn/device_drivers/libreswan_ipsec.py:129-181). 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.

Sample ipsec.conf rendered by the strongSwan template for one endpoint-group connection (values of the recorded OVN scenario):

# Configuration for <vpnservice id>
config setup

conn %default
        keylife=20m
        rekeymargin=3m
        keyingtries=1
        authby=psk
        mobike=no

conn <connection id>
    keyexchange=ikev1
    left=172.24.4.167
    leftsubnet=10.0.0.0/24,10.1.0.0/24
    leftid=172.24.4.167
    leftfirewall=yes
    right=172.24.4.233
    rightsubnet=192.168.10.0/24,192.168.20.0/24
    rightid=172.24.4.233
    auto=route
    dpdaction=hold
    dpddelay=30s
    dpdtimeout=120s
    ike=aes128-sha256-modp1536
    ikelifetime=3600s
    esp=aes128-sha256-modp1536
    lifetime=3600s
    type=tunnel

DNS

socket.getaddrinfo(peer_address) in the API request when peer_address is not an IP (neutron_vpnaas/db/vpn/vpn_validator.py:82-93), with the process's default resolver timeout: a slow resolver delays the API response.

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