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-libstable/2026.2@1ee72052251a;neutron:= neutronstable/2026.2@eef1a0f7ee36.
| System | Protocol | Library | Direction | Sync/async | Profile |
|---|---|---|---|---|---|
| Neutron database (MariaDB) | SQL | SQLAlchemy 2 / oslo.db via neutron_lib.db.api | neutron-server → DB | sync | both |
| RabbitMQ | AMQP 0-9-1, oslo.messaging RPC | neutron_lib.rpc | server ↔ agents | cast/call | both |
| RabbitMQ (notifications) | oslo.messaging notifier | neutron_lib.rpc.get_notifier | server → bus | async | OVN (scheduler API) |
| OVN Southbound DB | OVSDB JSON-RPC (RFC 7047), TCP/SSL 6642 | ovsdbapp / python-ovs IDL | server (read, events) and VPN agent (read/write) | async events + transactions | OVN |
| OVN Northbound DB | OVSDB | via Neutron's ML2/OVN (not called directly) | server → NB | sync | OVN |
| Local Open vSwitch DB | OVSDB, [ovs] ovsdb_connection (default unix:/usr/local/var/run/openvswitch/db.sock) | ovsdbapp | VPN agent → local OVSDB | sync | OVN |
| Neutron core / L3 plugins | in-process Python calls | neutron_lib.plugins.directory | service plugin → core/L3 plugins | sync | both |
| Linux kernel / processes | ip netns exec, ipsec, iptables, sysctl, mount --bind | rootwrap/privsep, neutron-vpn-netns-wrapper | agents → host | sync | both |
| DNS | getaddrinfo | Python socket | neutron-server → resolver | sync, inside the API request | both |
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).
| Table | Model | Purpose | Foreign keys |
|---|---|---|---|
ikepolicies | neutron_vpnaas/db/vpn/vpn_models.py:86 | IKE policies | — |
ipsecpolicies | neutron_vpnaas/db/vpn/vpn_models.py:62 | IPsec policies | — |
vpnservices | neutron_vpnaas/db/vpn/vpn_models.py:158 | VPN services (status, tunnel IPs) | subnets.id, routers.id, flavors.id |
ipsec_site_connections | neutron_vpnaas/db/vpn/vpn_models.py:109 | connections (PSK in clear, VARCHAR(255)) | vpnservices, ikepolicies, ipsecpolicies, vpn_endpoint_groups ×2 |
ipsecpeercidrs | neutron_vpnaas/db/vpn/vpn_models.py:51 | legacy peer CIDRs, PK (cidr, ipsec_site_connection_id), cidr VARCHAR(32) | ipsec_site_connections ON DELETE CASCADE |
vpn_endpoint_groups | neutron_vpnaas/db/vpn/vpn_models.py:194 | endpoint groups (endpoint_type enum subnet,cidr,vlan,network,router) | — |
vpn_endpoints | neutron_vpnaas/db/vpn/vpn_models.py:179 | endpoints, PK (endpoint, endpoint_group_id) | vpn_endpoint_groups ON DELETE CASCADE |
vpn_ext_gws | neutron_vpnaas/db/vpn/vpn_ext_gw_db.py:54 | OVN: per-router VPN gateway (status, gateway port, transit port/network/subnet), router_id unique | routers.id; ports.id ×2, networks.id, subnets.id ON DELETE SET NULL |
routervpnagentbindings | neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:54 | OVN: router → VPN agent, PK (router_id, vpn_agent_id), router_id unique | routers.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
| Operation | Reads | Writes |
|---|---|---|
| IKE / IPsec policy CRUD | ikepolicies/ipsecpolicies, ipsec_site_connections (in-use check) | ikepolicies/ipsecpolicies |
| Endpoint group CRUD | vpn_endpoint_groups, vpn_endpoints, subnets (validation), ipsec_site_connections (in-use) | vpn_endpoint_groups, vpn_endpoints |
| POST vpnservice (legacy) | routers, ports (gateway), subnets, flavors | vpnservices, providerresourceassociations |
| POST vpnservice (OVN) | + agents (AgentCache), routervpnagentbindings, vpn_ext_gws | + routervpnagentbindings, vpn_ext_gws, Neutron ports, networks, subnets, router interface |
| POST/PUT connection | vpnservices, ikepolicies, ipsecpolicies, vpn_endpoint_groups, vpn_endpoints, subnets, ports, routers | ipsec_site_connections, ipsecpeercidrs; OVN: routerroutes |
| DELETE vpnservice (OVN, last on router) | vpnservices, vpn_ext_gws | delete vpnservices, Neutron ports/subnet/network, vpn_ext_gws |
get_vpn_services_on_host | join vpnservices ⋈ ipsec_site_connections ⋈ (routerl3agentbindings ⋈ providerresourceassociations | routervpnagentbindings), vpn_endpoints ⋈ subnets | OVN: routervpnagentbindings (auto-schedule) |
update_status | vpnservices, ipsec_site_connections | status columns |
check_vpn_agents | routervpnagentbindings | delete/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 noSELECT … FOR UPDATE. - Concurrency is arbitrated by unique keys:
vpn_ext_gws.router_id(two first services on the same router: the second insert fails withDBDuplicateEntry, retried by@retry_if_session_inactiveuntil the first gateway isACTIVE,neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:350-370), androutervpnagentbindings.router_id(DBDuplicateEntry→ "already scheduled",neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:164-185). - The OVN
_setupruns inside oneCONTEXT_WRITERthat 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 toERRORin that same transaction before re-raising [inferred: whether theERRORupdate survives depends on the outer transaction rollback; the observed service row did keepERROR]. - 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
| Exchange | Routing key / queue | Producer | Consumer |
|---|---|---|---|
neutron (topic) | ipsec_driver | L3/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 call | caller |
neutron notifications (notifications.info) | event types vpn_agent.router.add, vpn_agent.router.remove | neutron-server, publisher router.<host> | any listener |
Messages
| Method | Mode | Topic / server | Version | Arguments | Reply | Source |
|---|---|---|---|---|---|---|
vpnservice_updated | cast | ipsec_agent / agent host | 1.0 | router: {id} | — | neutron_vpnaas/services/vpn/service_drivers/__init__.py:106-110 |
get_vpn_services_on_host | call | ipsec_driver | 1.0 | host | list of service dicts (05 § 2) | neutron_vpnaas/services/vpn/device_drivers/ipsec.py:438-445 |
update_status | call | ipsec_driver | 1.0 | status: [...] (05 § 3) | null | neutron_vpnaas/services/vpn/device_drivers/ipsec.py:448-455 |
get_vpn_transit_network_details | call | ipsec_driver | 1.0 | router_id | {gw_port, transit_port, transit_gateway_ip, external_network} | neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:211-214 |
get_subnet_info | call | ipsec_driver | 1.0 | subnet_id | subnet dict or null | neutron_vpnaas/services/vpn/device_drivers/ovn_ipsec.py:216-219 |
vpnservice_added_to_agent | call ×2 attempts | ipsec_agent / host | default (1.0) | router_ids: [...] | null | neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:44-52 |
vpnservice_removed_from_agent | cast | ipsec_agent / host | default | router_id | — | neutron_vpnaas/api/rpc/agentnotifiers/vpn_rpc_agent_api.py:38-42 |
agent_updated | cast | ipsec_agent / host | default | payload: {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)
| Actor | Tables | Operations | Source |
|---|---|---|---|
| neutron-server (every worker, via the L3 plugin's SB IDL) | Chassis_Private | monitor; event ChassisVPNAgentWriteEvent | neutron_vpnaas/services/vpn/ovn/agent_monitor.py:48-89 |
| OVN VPN agent | SB_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)
| Call | Target | Purpose | Source |
|---|---|---|---|
l3_plugin.get_router | L3 | gateway check, AZ hints, extra routes | neutron_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_subnet | core | endpoint-group validation, get_subnet_info | neutron_vpnaas/db/vpn/vpn_validator.py:323, neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:90 |
l3_plugin.get_l3_agents_hosting_routers | L3 | legacy notification targets | neutron_vpnaas/services/vpn/service_drivers/__init__.py:92-95 |
core_plugin.get_agents / get_agent | core (ML2/OVN: AgentCache) | VPN agents, liveness | neutron_vpnaas/db/vpn/vpn_agentschedulers_db.py:320-336 |
create_network, create_subnet, create_port, l3_plugin.add_router_interface | core / L3 | OVN gateway and transit objects | neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:169-273 |
delete_port(l3_port_check=False), remove_router_interface, delete_subnet, delete_network | core / L3 | OVN cleanup (NotFound ignored) | neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:215-256 |
l3_plugin.add_extraroutes / remove_extraroutes | L3 | OVN routes to peer CIDRs via 169.254.0.2 | neutron_vpnaas/services/vpn/service_drivers/ovn_ipsec.py:314-321 |
core_plugin.update_port({'binding:host_id'}) | core | rebind VPN ports after rescheduling | neutron_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 types | service → driver | neutron_vpnaas/services/vpn/plugin.py:87-157 |
flavors plugin (get_flavor, get_flavor_next_provider) | flavors | provider from flavor | neutron_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.
| Command | When | Source |
|---|---|---|
ipsec --piddir | strongSwan process object creation | neutron_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 command | each wrapped command | neutron_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 handling | neutron_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 ACCEPT | legacy, per local/peer IPv4 pair | neutron_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 delete | OVN namespace management | neutron_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.