VPN
AiFw ships two working VPN stacks: WireGuard for client/road-warrior and site-to-site use, and IKEv2 IPsec (strongSwan-powered, tunnel mode, PSK or X.509 auth) for site-to-site interop with other vendors. Pass rules compile into the dedicated aifw-vpn anchor and everything is managed through the API or CLI — no hand-edited wg-quick files. OpenVPN is intentionally not shipped; see the comparison page for the reasoning.
WireGuard
A WireGuard tunnel in AiFw is a server-side instance with its own keypair, listen port, and tunnel address. Peers (clients) are added one at a time and AiFw can auto-generate the client keypair so you can download a ready-to-import .conf file.
Quickstart
In the Web UI, go to VPN → WireGuard → Add tunnel. Pick an interface name (wg0, wg1, …), a listen port, and a tunnel address with prefix (e.g. 10.0.0.1/24). The keypair is generated automatically when the row is created. Then add peers and click Download config to get the client .conf file.
Create a tunnel:
curl -X POST https://aifw.local/api/v1/vpn/wg \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "office-vpn",
"interface": "wg0",
"listen_port": 51820,
"address": "10.0.0.1/24",
"dns": "10.0.0.1",
"mtu": 1420
}'
Add a peer with an auto-generated keypair (the server keeps the private key so it can build the client config):
curl -X POST "https://aifw.local/api/v1/vpn/wg/$TID/peers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "laptop",
"allowed_ips": ["10.0.0.2/32"],
"persistent_keepalive": 25
}'
Download the peer’s ready-to-import config (server fills in [Interface] private key, [Peer] public key, Endpoint, AllowedIPs):
curl "https://aifw.local/api/v1/vpn/wg/$TID/peers/$PID/config?split_tunnel=false" \
-H "Authorization: Bearer $TOKEN" -o laptop.conf
Bring the tunnel up:
curl -X POST "https://aifw.local/api/v1/vpn/wg/$TID/start" \
-H "Authorization: Bearer $TOKEN"
Split-tunnel routes
Set split_routes on the tunnel to a comma-separated list of CIDRs (e.g. "172.29.0.0/16, 10.0.0.0/8") and request split_tunnel=true on the peer config download. Clients then only route those networks through the VPN. Without split_routes, AiFw masks the tunnel’s own address to its network boundary and uses that.
HA failover
WireGuard tunnels survive a CARP failover provided peers run with PersistentKeepalive. The wireguard-go process binds wildcard so the new master receives traffic on the CARP VIP automatically. See HA cluster for the full survival matrix.
IPsec
AiFw ships real IKEv2 site-to-site IPsec (tunnel mode) powered by strongSwan: the engine renders swanctl configuration, charon negotiates IKE and installs kernel Security Associations/policies, and live tunnel state is always read back from charon — never from a database status column. Authentication is pre-shared key or X.509 certificate (from the built-in ACME store or pasted PEM). NAT-T (UDP 4500 encapsulation), rekeying, dead-peer detection, and unattended recovery after reboot are all handled by the daemon.
Deliberately not offered: IKEv1 (deprecated, RFC 9395), AH, and transport mode — modern ESP with AEAD ciphers covers those use cases, and AiFw only advertises what its functional test matrix proves. Road-warrior/mobile IKEv2 (EAP) is not supported yet; use WireGuard for client VPN.
Defaults: aes256gcm16-prfsha256-ecp256 (IKE) / aes256gcm16-ecp256 (ESP), 4h IKE / 1h child rekey, 30s DPD.
Quickstart (PSK site-to-site)
In the Web UI, go to VPN → IPsec Tunnels → Add IPsec Tunnel: name, remote endpoint, the subnets on each side, and a pre-shared key (16+ characters). Or via the API:
curl -X POST https://aifw.local/api/v1/vpn/ipsec/tunnels \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "office-to-dc",
"remote_addr": "198.51.100.1",
"psk": "use-a-long-random-secret-here",
"local_ts": ["10.0.0.0/24"],
"remote_ts": ["10.1.0.0/24"]
}'
With the default start_action: "start", charon initiates as soon as the config loads. Check the negotiated state:
curl -H "Authorization: Bearer $TOKEN" https://aifw.local/api/v1/vpn/ipsec/tunnels/<id>/status
# → ike_state ESTABLISHED, child SA INSTALLED, bytes/rekey counters
The peer must mirror the config: same PSK, swapped local/remote endpoints and traffic selectors, IKEv2, matching proposals. Any RFC-compliant IKEv2 peer should interoperate; strongSwan↔strongSwan is what AiFw’s release matrix tests.
Certificate authentication
Set auth_method: "cert" with either cert_source: "acme" + acme_cert_id (a cert issued by the built-in ACME client) or cert_source: "manual" with local_cert_pem/local_key_pem pasted. Add the CA that signed the peer’s certificate as ca_cert_pem, and set local_id/remote_id to the certificate identities (e.g. CN=fw1.example.com). Private keys are written root-only into strongSwan’s private/ directory and are redacted in every API response.
Behavior details
- Each enabled tunnel renders to
/usr/local/etc/swanctl/conf.d/aifw-<id>.conf(root, 0600). The database is the source of truth; files are regenerated on every change and at service startup, so tunnels re-establish unattended after reboot or restore-from-backup. - A failed apply rolls back to the previous working configuration — a bad tunnel can’t take down good ones. charon’s diagnostic is returned in the error response.
- Firewall openings are scoped per tunnel into the
aifw-vpnanchor: IKE (UDP 500/4500) and ESP to/from the configured peer only, plus decrypted traffic onenc0. start_action:start(initiate + keep up via DPD restart),trap(negotiate on first matching packet),none(respond only / manual start).- Legacy pre-data-plane “IPsec SA” records (from AiFw ≤ 5.104) are kept read-only and clearly flagged; they never carried traffic and can be deleted or recreated as tunnels.
Troubleshooting
- Status shows DOWN — check the peer is reachable on UDP 500/4500 and PSK/identities match.
POST .../tunnels/<id>/startreturns charon’s negotiation error verbatim (e.g.AUTHENTICATION_FAILED= wrong PSK/cert identity). - Established but no traffic — verify the traffic selectors cover the actual source/destination addresses on both sides; in tunnel mode only packets matching
local_ts ↔ remote_tsare protected. - On the box:
sudo swanctl --list-sas(negotiated state),setkey -D/setkey -DP(kernel SAD/SPD).
CLI
aifw vpn wg-add --name wg0 --interface wg0 --port 51820 --address 10.0.0.1/24
aifw vpn wg-peer-add --tunnel <id> --name laptop --pubkey <key> --endpoint 1.2.3.4:51820
aifw vpn ipsec-add --name office --remote 198.51.100.1 --psk <secret> \
--local-ts 10.0.0.0/24 --remote-ts 10.1.0.0/24 # PSK; use UI/API for cert auth
aifw vpn ipsec-status
aifw vpn ipsec-start <uuid>
aifw vpn ipsec-stop <uuid>
aifw vpn list
aifw vpn remove <uuid>
API endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/vpn/wg |
List WireGuard tunnels |
POST |
/api/v1/vpn/wg |
Create a tunnel (auto-generates keypair) |
PUT |
/api/v1/vpn/wg/{id} |
Update a tunnel |
DELETE |
/api/v1/vpn/wg/{id} |
Delete a tunnel |
POST |
/api/v1/vpn/wg/{id}/start |
Bring tunnel up |
POST |
/api/v1/vpn/wg/{id}/stop |
Bring tunnel down |
GET |
/api/v1/vpn/wg/{id}/status |
Live tunnel status |
GET |
/api/v1/vpn/wg/{id}/peers |
List peers |
POST |
/api/v1/vpn/wg/{id}/peers |
Add a peer |
GET |
/api/v1/vpn/wg/{id}/peers/next-ip |
Suggest the next free peer IP |
DELETE |
/api/v1/vpn/wg/{tid}/peers/{pid} |
Delete a peer |
GET |
/api/v1/vpn/wg/{tid}/peers/{pid}/config |
Download peer .conf file |
GET |
/api/v1/vpn/ipsec/tunnels |
List IPsec tunnels (secrets redacted) |
POST |
/api/v1/vpn/ipsec/tunnels |
Create a tunnel (applies + loads charon) |
GET/PUT/DELETE |
/api/v1/vpn/ipsec/tunnels/{id} |
Get / update / delete a tunnel |
POST |
/api/v1/vpn/ipsec/tunnels/{id}/start |
Initiate the tunnel now |
POST |
/api/v1/vpn/ipsec/tunnels/{id}/stop |
Terminate the IKE SA |
GET |
/api/v1/vpn/ipsec/tunnels/{id}/status |
Live negotiated state from charon |
GET |
/api/v1/vpn/ipsec/status |
Live state of all tunnels |
GET |
/api/v1/vpn/ipsec |
List legacy read-only SA records (legacy: true) |
DELETE |
/api/v1/vpn/ipsec/{id} |
Delete a legacy record |
Configuration
| Field | Default | Notes |
|---|---|---|
| Anchor name | aifw-vpn |
All WG/IPsec pass rules live here |
ike_proposal (IPsec) |
aes256gcm16-prfsha256-ecp256 |
swanctl proposal string, validated against a curated allowlist |
esp_proposal (IPsec) |
aes256gcm16-ecp256 |
AEAD + PFS group |
ike_lifetime_secs / esp_lifetime_secs |
14400 / 3600 |
Rekey times |
dpd_delay_secs (IPsec) |
30 |
0 disables dead-peer detection |
persistent_keepalive (WG) |
unset | Set on the peer (typically 25) so the tunnel survives NAT timeouts and HA failovers |
split_routes (WG) |
unset | Comma-separated CIDRs; falls back to the masked tunnel network |