# ISP360 WireGuard Integration Specification

**Version:** 1.0  
**Date:** 2026-06-13  
**Prepared by:** Polakis Technologies  

Companion: [DATABASE_DESIGN.md](DATABASE_DESIGN.md) | [MIKROTIK_INTEGRATION.md](MIKROTIK_INTEGRATION.md) | [RBAC_SPEC.md](RBAC_SPEC.md) | [UI_SHELL_PATTERN.md](UI_SHELL_PATTERN.md)

---

## 1. Overview

ISP360 manages **WireGuard VPN** infrastructure for African ISPs: hub-and-spoke remote access, site-to-site backhaul, technician tunnels to CPE routers, and customer-managed VPN endpoints. All VPN operations are tenant-scoped and abstracted through two core classes:

| Class | File | Responsibility |
|-------|------|----------------|
| `WireGuardManager` | `includes/WireGuardManager.php` | Server/peer lifecycle, config generation, tunnel sync |
| `VpnKeyManager` | `includes/VpnKeyManager.php` | Key generation, encryption at rest, rotation history |

WireGuard is the **primary** `vpn_type` in `vpn_servers`. OpenVPN/IPsec remain schema-supported for future phases but are out of scope for Wave 3 implementation.

### Design Principles

| Principle | Implementation |
|-----------|----------------|
| Never store plaintext private keys | `private_key_encrypted`, `key_data_encrypted` via `Encryption.php` |
| ISP360 is source of truth | Peers defined in DB; pushed to Linux WG host and/or MikroTik |
| Tenant isolation | Every query filters `tenant_id = tenant_context()` |
| Async push for routers | MikroTik WireGuard config via `router_commands` queue |
| Health via cron | `vpn_peer_status` updated by background poller, not page load |
| Idempotent provisioning | Upsert peer by `(tenant_id, peer_code)` |

### Deployment Targets

| Target | Use Case | Push Method |
|--------|----------|-------------|
| Linux WireGuard host | Central VPN concentrator | SSH + `wg` / config file deploy |
| MikroTik RouterOS 7+ | Edge/site router WG interface | `RouterManager::pushWireGuardPeer()` |
| Client config download | Remote staff / customer | QR + `.conf` file from `vpn_peers` |

---

## 2. Architecture

```mermaid
flowchart TB
    subgraph ui [Admin UI — root pages]
        VpnServers["vpn_servers.php"]
        VpnPeers["vpn_peers.php"]
        VpnTunnels["vpn_tunnels.php"]
        VpnLogs["vpn_logs.php"]
        VpnServerEdit["vpn_server_edit.php"]
        VpnPeerEdit["vpn_peer_edit.php"]
    end

    subgraph ajax [AJAX Layer]
        GetServers["ajax/get_vpn_servers.php"]
        SaveServer["ajax/save_vpn_server.php"]
        GetPeers["ajax/get_vpn_peers.php"]
        SavePeer["ajax/save_vpn_peer.php"]
        GenKeys["ajax/generate_vpn_keys.php"]
        PushCfg["ajax/push_vpn_config.php"]
        RotateKeys["ajax/rotate_vpn_keys.php"]
        RevokePeer["ajax/revoke_vpn_peer.php"]
    end

    subgraph core [Core Classes]
        WGM["WireGuardManager.php"]
        VKM["VpnKeyManager.php"]
        Enc["Encryption.php"]
        RM["RouterManager.php"]
    end

    subgraph data [Database]
        VpnSrv["vpn_servers"]
        VpnPeer["vpn_peers"]
        VpnKey["vpn_keys"]
        VpnTun["vpn_tunnels"]
        VpnStat["vpn_peer_status"]
        VpnLog["vpn_logs"]
        VpnRot["vpn_key_rotation_history"]
        Routers["routers"]
        CmdQ["router_commands"]
    end

    subgraph cron [Background]
        StatusCron["cron/vpn_status_sync.php"]
        RotateCron["cron/vpn_key_rotation.php"]
        CmdProc["cron/router_command_processor.php"]
    end

    subgraph targets [VPN Targets]
        LinuxWG["Linux wg0"]
        MikroTik["MikroTik /interface/wireguard"]
    end

    VpnServers --> GetServers
    VpnPeerEdit --> GenKeys
    VpnPeerEdit --> PushCfg
    SaveServer --> WGM
    SavePeer --> WGM
    GenKeys --> VKM
    PushCfg --> WGM
    RotateKeys --> VKM
    WGM --> VKM
    WGM --> Enc
    WGM --> RM
    WGM --> VpnSrv
    WGM --> VpnPeer
    WGM --> VpnTun
    WGM --> VpnLog
    VKM --> VpnKey
    VKM --> VpnRot
    RM --> CmdQ
    RM --> Routers
    CmdProc --> RM
    StatusCron --> WGM
    RotateCron --> VKM
    RM --> MikroTik
    WGM --> LinuxWG
```

---

## 3. Database Tables

| Table | Role |
|-------|------|
| `vpn_servers` | VPN concentrator registry (listen port, pool, server keys) |
| `vpn_peers` | Peer definitions (keys, allowed IPs, endpoint, customer link) |
| `vpn_keys` | Encrypted key material (private/public/PSK) |
| `vpn_tunnels` | Active tunnel state per peer (handshake, bytes, status) |
| `vpn_peer_status` | Latest polled status snapshot (1 row per peer) |
| `vpn_logs` | Operational event log |
| `vpn_routes` | Allowed routes pushed to server |
| `vpn_acl` | Firewall-style rules per server |
| `vpn_server_interfaces` | Interface stats mirror (wg0, etc.) |
| `vpn_key_rotation_history` | Audit trail for key rotations |

### 3.1 Entity Relationships

```
tenants
  └── vpn_servers (1:N)
        ├── vpn_server_interfaces (1:N)
        ├── vpn_peers (1:N)
        │     ├── vpn_keys (1:N)
        │     ├── vpn_tunnels (1:N)
        │     └── vpn_peer_status (1:1)
        ├── vpn_routes (1:N)
        └── vpn_acl (1:N)
routers.router_id → vpn_servers.router_id (optional MikroTik host)
customers.customer_id → vpn_peers.customer_id (optional)
```

### 3.2 Key Field Semantics

| Field | Notes |
|-------|-------|
| `vpn_servers.server_code` | Unique per tenant; used in config filenames (`wg-{code}.conf`) |
| `vpn_servers.private_key_encrypted` | Server private key; never exposed in UI |
| `vpn_servers.public_key` | Shown in peer configs as `[Peer]` endpoint key |
| `vpn_peers.allowed_ips` | Comma-separated CIDRs; default `assigned_ip/32` |
| `vpn_peers.persistent_keepalive` | Default `25` for NAT traversal |
| `vpn_peers.expires_at` | Optional; cron disables expired peers |
| `vpn_tunnels.last_handshake_at` | Updated from `wg show` or MikroTik API |

---

## 4. Class: `VpnKeyManager`

**File:** `includes/VpnKeyManager.php`

Handles WireGuard key generation and encrypted storage. No network operations.

### 4.1 Key Generation

```php
class VpnKeyManager
{
    /**
     * Generate Curve25519 WireGuard keypair via `wg genkey` / `wg pubkey`.
     * Falls back to sodium_crypto_box_keypair if wg CLI unavailable.
     *
     * @return array{private: string, public: string}
     */
    public function generateKeyPair(): array;

    /**
     * Generate preshared key (optional extra symmetric layer).
     */
    public function generatePresharedKey(): string;

    /**
     * Store key in vpn_keys; returns key_id.
     * Private/PSK stored in key_data_encrypted only.
     */
    public function storeKey(
        int $tenant_id,
        string $key_type,
        string $key_name,
        string $plaintext,
        ?int $server_id = null,
        ?int $peer_id = null,
        ?int $created_by = null
    ): int;

    /**
     * Decrypt key for config generation or push.
     * Logs access to vpn_logs (event_type: key_access).
     */
    public function decryptKey(int $key_id, int $tenant_id): string;

    /**
     * Rotate peer keys: deactivate old, generate new, write rotation history.
     */
    public function rotatePeerKeys(
        int $peer_id,
        int $tenant_id,
        string $rotation_type = 'manual',
        ?int $rotated_by = null
    ): array;
}
```

### 4.2 Encryption

Uses `Encryption.php` (AES-256-GCM, tenant-scoped key from `tenant_encryption_keys`):

- `vpn_servers.private_key_encrypted` — VARBINARY envelope
- `vpn_peers.preshared_key_encrypted` — optional PSK
- `vpn_keys.key_data_encrypted` — canonical encrypted store for all key types

**Fingerprint:** SHA256 base64 of public key stored in `vpn_keys.fingerprint` for dedup and rotation audit.

---

## 5. Class: `WireGuardManager`

**File:** `includes/WireGuardManager.php`

High-level VPN orchestrator. All UI and cron jobs call this class.

### 5.1 Constructor

```php
class WireGuardManager
{
    private PDO $pdo;
    private VpnKeyManager $keyManager;
    private ?RouterManager $routerManager;

    public function __construct(PDO $pdo, ?RouterManager $routerManager = null);
}
```

### 5.2 Server Operations

```php
/** Create or update vpn_servers row. Generates server keypair on create. */
public function saveServer(int $tenant_id, array $data, int $user_id): int;

/** Enable/disable server; logs to vpn_logs. */
public function setServerEnabled(int $server_id, int $tenant_id, bool $enabled): void;

/** Build server-side wg-quick config INI for Linux deployment. */
public function buildServerConfig(int $server_id, int $tenant_id): string;

/** Sync vpn_server_interfaces from `wg show` or remote SSH. */
public function syncServerInterfaces(int $server_id, int $tenant_id): void;
```

### 5.3 Peer Operations

```php
/** Create/update peer; auto-assign IP from address_pool if empty. */
public function savePeer(int $tenant_id, array $data, int $user_id): int;

/** Generate client .conf file content for download. */
public function buildClientConfig(int $peer_id, int $tenant_id): string;

/** Revoke peer: disable, remove from runtime, queue router removal. */
public function revokePeer(int $peer_id, int $tenant_id, int $user_id): void;

/** Push peer to all configured targets (Linux + linked router). */
public function pushPeerConfig(int $peer_id, int $tenant_id, int $user_id): array;
```

### 5.4 MikroTik Integration

When `vpn_servers.router_id` is set, peer push delegates to `RouterManager`:

```php
// RouterManager.php — new methods
public function pushWireGuardPeer(int $router_id, int $tenant_id, array $peerConfig): string;
public function removeWireGuardPeer(int $router_id, int $tenant_id, string $publicKey): void;
public function getWireGuardPeers(int $router_id, int $tenant_id): array;
```

**RouterOS 7 API paths:**

| Operation | Command |
|-----------|---------|
| Add interface | `/interface/wireguard/add` |
| Add peer | `/interface/wireguard/peers/add` |
| List peers | `/interface/wireguard/peers/print` |
| Remove peer | `/interface/wireguard/peers/remove` |

Queued as `router_commands.command_type = 'vpn_push_peer'` to avoid blocking UI.

### 5.5 Status & Tunnel Sync

```php
/**
 * Poll peer handshake + transfer stats.
 * Updates vpn_peer_status, upserts vpn_tunnels.
 */
public function syncPeerStatus(int $server_id, int $tenant_id): int;

/** Batch sync all enabled servers for tenant (cron entry point). */
public function syncAllServers(?int $tenant_id = null): void;
```

**Status mapping:**

| WireGuard state | `vpn_tunnels.status` |
|-----------------|----------------------|
| Recent handshake (< 3 min) | `up` |
| Stale handshake | `down` |
| Handshake in progress | `connecting` |
| Parse/API failure | `error` |

### 5.6 Logging

```php
public function log(
    int $tenant_id,
    string $event_type,
    string $message,
    string $level = 'info',
    ?int $server_id = null,
    ?int $peer_id = null,
    ?array $details = null
): void;
```

Standard `event_type` values: `server_created`, `peer_created`, `peer_revoked`, `config_pushed`, `key_rotated`, `handshake_stale`, `push_failed`, `sync_complete`.

---

## 6. Workflows

### 6.1 Create VPN Server

```mermaid
sequenceDiagram
    participant A as Admin
    participant UI as vpn_server_edit.php
    participant AJAX as save_vpn_server.php
    participant WGM as WireGuardManager
    participant VKM as VpnKeyManager
    participant DB as MySQL

    A->>UI: Fill server form
    UI->>AJAX: POST server data
    AJAX->>AJAX: permission_check vpn.create
    AJAX->>WGM: saveServer()
    WGM->>VKM: generateKeyPair()
    VKM-->>WGM: keys
    WGM->>DB: INSERT vpn_servers
    WGM->>DB: INSERT vpn_keys (server private)
    WGM->>DB: INSERT vpn_logs
    WGM-->>AJAX: server_id
    AJAX-->>UI: JSON success
    UI-->>A: Redirect vpn_servers.php
```

**Required fields:** `server_code`, `server_name`, `listen_port` (default 51820), `address_pool` (e.g. `10.200.0.0/24`).

### 6.2 Provision Remote Access Peer

```mermaid
sequenceDiagram
    participant A as Admin
    participant UI as vpn_peer_edit.php
    participant AJAX as save_vpn_peer.php
    participant WGM as WireGuardManager
    participant DB as MySQL

    A->>UI: Select server, name, customer
    UI->>AJAX: POST peer data
    AJAX->>WGM: savePeer()
    WGM->>WGM: assign IP from pool
    WGM->>WGM: generate peer keypair
    WGM->>DB: INSERT vpn_peers + vpn_keys
    WGM->>DB: INSERT vpn_tunnels (status=down)
    A->>UI: Click Push Config
    UI->>AJAX: push_vpn_config.php
    AJAX->>WGM: pushPeerConfig()
    alt MikroTik linked
        WGM->>DB: INSERT router_commands
    else Linux host
        WGM->>WGM: SSH wg set peer
    end
    WGM->>DB: INSERT vpn_logs
```

### 6.3 Key Rotation

```mermaid
sequenceDiagram
    participant Cron as vpn_key_rotation.php
    participant VKM as VpnKeyManager
    participant WGM as WireGuardManager
    participant DB as MySQL

    Cron->>DB: SELECT keys WHERE valid_to < NOW() OR scheduled
    loop Each expiring key
        Cron->>VKM: rotatePeerKeys()
        VKM->>DB: Deactivate old vpn_keys
        VKM->>DB: INSERT vpn_key_rotation_history
        VKM->>DB: INSERT new vpn_keys
        Cron->>WGM: pushPeerConfig()
    end
```

**Manual rotation:** `ajax/rotate_vpn_keys.php` with `permission_check('vpn.rotate_keys')`.

### 6.4 Tunnel Monitoring

`vpn_tunnels.php` displays live data from `vpn_tunnels` JOIN `vpn_peer_status`:

| Column | Source |
|--------|--------|
| Peer name | `vpn_peers.peer_name` |
| Status badge | `vpn_tunnels.status` |
| Assigned IP | `vpn_peer_status.assigned_ip` |
| Last handshake | `vpn_peer_status.last_handshake_at` |
| RX / TX | `vpn_peer_status.rx_bytes` / `tx_bytes` |
| Latency | `vpn_peer_status.latency_ms` |

Auto-refresh via AJAX every 30s (`ajax/get_vpn_tunnels.php`).

---

## 7. Config Templates

### 7.1 Server Config (Linux `wg-quick`)

```ini
# ISP360 generated — {server_code} — tenant {tenant_id}
[Interface]
Address = {server_tunnel_ip}/24
ListenPort = {listen_port}
PrivateKey = {server_private_key}
MTU = {mtu}
# PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# {peer_code} — {peer_name}
PublicKey = {peer_public_key}
PresharedKey = {preshared_key_optional}
AllowedIPs = {allowed_ips}
PersistentKeepalive = {keepalive}
```

### 7.2 Client Config

```ini
[Interface]
PrivateKey = {peer_private_key}
Address = {assigned_ip}/32
DNS = {dns_servers}
MTU = {mtu}

[Peer]
PublicKey = {server_public_key}
PresharedKey = {preshared_key_optional}
Endpoint = {listen_address}:{listen_port}
AllowedIPs = {routes_or_0.0.0.0/0}
PersistentKeepalive = 25
```

### 7.3 MikroTik Peer (RouterOS 7)

```
/interface wireguard peers add
    interface=wg-isp360
    public-key="{peer_public_key}"
    preshared-key="{psk}"
    allowed-address={allowed_ips}
    endpoint-address={endpoint}
    persistent-keepalive=25s
```

---

## 8. Cron Jobs

| Script | Schedule | Purpose |
|--------|----------|---------|
| `cron/vpn_status_sync.php` | `*/2 * * * *` | Poll handshake/bytes for all servers |
| `cron/vpn_key_rotation.php` | `0 0 1 * *` | Monthly scheduled rotation |
| `cron/router_command_processor.php` | `* * * * *` | Process queued MikroTik VPN pushes |

### 8.1 `vpn_status_sync.php` Pseudocode

```php
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/WireGuardManager.php';

$wgm = new WireGuardManager($pdo);
$wgm->syncAllServers(); // all tenants
```

### 8.2 Expired Peer Cleanup

Within `vpn_status_sync.php`, after sync:

```sql
UPDATE vpn_peers SET is_enabled = 0
WHERE expires_at IS NOT NULL AND expires_at < NOW() AND is_enabled = 1;
```

Then queue `revoke` commands for disabled peers.

---

## 9. Admin UI Pages

All VPN admin pages follow the **ISP360 UI shell pattern** (see [UI_SHELL_PATTERN.md](UI_SHELL_PATTERN.md)): same structural approach as `dashboard.php`, `login.php`, and `logout.php` — PDO, `layouts/`, ISP360 design tokens, schema-aligned queries.

| Page | Permission | Primary Tables |
|------|------------|----------------|
| `vpn_servers.php` | `vpn.view` | `vpn_servers` |
| `vpn_server_edit.php` | `vpn.create` \| `vpn.edit` | `vpn_servers`, `vpn_server_interfaces` |
| `vpn_peers.php` | `vpn.view` | `vpn_peers` |
| `vpn_peer_edit.php` | `vpn.edit` | `vpn_peers`, `vpn_keys` |
| `vpn_tunnels.php` | `vpn.view` | `vpn_tunnels`, `vpn_peer_status` |
| `vpn_logs.php` | `vpn.view` | `vpn_logs` |

### 9.1 Page Bootstrap Template

```php
<?php
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/tenant.php';

requireLogin();
require_permission('vpn.view');
$tenant_id = require_tenant_context();

$page_title = 'VPN Servers';
// ... page logic with $pdo prepared statements ...
include __DIR__ . '/layouts/app-start.php';
// ... content ...
include __DIR__ . '/layouts/app-end.php';
```

### 9.2 AJAX Endpoints

| Endpoint | Method | Permission | Action |
|----------|--------|------------|--------|
| `ajax/get_vpn_servers.php` | GET | `vpn.view` | DataTables JSON |
| `ajax/save_vpn_server.php` | POST | `vpn.create` / `vpn.edit` | Upsert server |
| `ajax/get_vpn_peers.php` | GET | `vpn.view` | DataTables JSON |
| `ajax/save_vpn_peer.php` | POST | `vpn.edit` | Upsert peer |
| `ajax/generate_vpn_keys.php` | POST | `vpn.edit` | Regenerate peer keys |
| `ajax/push_vpn_config.php` | POST | `vpn.push_config` | Push to target |
| `ajax/rotate_vpn_keys.php` | POST | `vpn.rotate_keys` | Rotate keys |
| `ajax/revoke_vpn_peer.php` | POST | `vpn.delete` | Revoke peer |
| `ajax/get_vpn_tunnels.php` | GET | `vpn.view` | Tunnel status JSON |
| `ajax/get_vpn_logs.php` | GET | `vpn.view` | Log viewer JSON |

---

## 10. RBAC Permissions

| Code | Action |
|------|--------|
| `vpn.view` | List servers, peers, tunnels, logs |
| `vpn.create` | Create servers and peers |
| `vpn.edit` | Edit peers, regenerate keys |
| `vpn.delete` | Revoke/delete peers |
| `vpn.rotate_keys` | Manual key rotation |
| `vpn.push_config` | Push config to Linux/MikroTik |

**Roles with VPN access:** ISP Admin (full), Technician (full), Super Admin (bypass).

---

## 11. Security

| Risk | Mitigation |
|------|------------|
| Private key exposure | Encrypted at rest; decrypt only in memory during push |
| Cross-tenant peer access | `tenant_id` enforced on every query |
| Stale peer access | `expires_at` + cron disable |
| Brute-force on VPN UI | Standard login_history rate limits |
| Config download leak | Signed download token, 5-minute expiry, audit log |
| MikroTik push failure | Retry via `router_commands` (max 3 attempts) |

---

## 12. Error Handling

| Error | HTTP | User Message |
|-------|------|--------------|
| Pool exhausted | 422 | No available IPs in address pool |
| Duplicate peer_code | 409 | Peer code already exists |
| Server disabled | 422 | VPN server is disabled |
| Router offline | 502 | Cannot push — router unreachable (queued) |
| wg CLI missing | 500 | Server configuration error (logged) |
| Invalid key format | 400 | Generated key validation failed |

All errors logged to `vpn_logs` with `log_level = 'error'`.

---

## 13. Implementation Checklist (Wave 3)

- [ ] `includes/VpnKeyManager.php`
- [ ] `includes/WireGuardManager.php`
- [ ] `RouterManager` WireGuard methods
- [ ] Cron: `vpn_status_sync.php`, `vpn_key_rotation.php`
- [ ] AJAX endpoints (10 files)
- [ ] Root pages: 6 VPN admin pages
- [ ] Menu entries in `includes/menu_config.php` (Network → VPN)
- [ ] Integration test: Linux wg peer add/remove
- [ ] Integration test: MikroTik RouterOS 7 peer push

---

## 14. Related Documents

| Document | Relevance |
|----------|-----------|
| [MIKROTIK_INTEGRATION.md](MIKROTIK_INTEGRATION.md) | Router command queue, API-SSL |
| [RADIUS_SPEC.md](RADIUS_SPEC.md) | Parallel network module patterns |
| [UI_SPECIFICATIONS.md](UI_SPECIFICATIONS.md) | Field-level screen specs for VPN pages |
| [UI_SHELL_PATTERN.md](UI_SHELL_PATTERN.md) | Layout, auth, and design language |
| [RBAC_SPEC.md](RBAC_SPEC.md) | Permission codes and login flow |
