WireGuard access classes, firewall nftables fixes, network sync event refactor
- WireGuard: refactor to multi-interface 'access classes' model; extract config generation and helpers into lib/wireguard.py; add per-class up/down endpoints and API routes; update UI with class management pages and QR code component - Firewall: fix zone creation with --new-zone before --set-target; skip masquerade on public zone; add masquerade propagation for nftables backend so NAT works when internal zones exit via public - Network: rename sync event subsystem 'network' -> 'networkd'; always stamp config hash even when deployment fails (fixes pending-changes detection) - DHCP: add new API endpoint and update frontend page - State/Sync: update state collectors and sync buses for new subsystems - Docs: update API and config documentation for new endpoints and schemas
This commit is contained in:
+80
@@ -1320,6 +1320,86 @@ This is the only endpoint that returns a WireGuard private key. All other endpoi
|
||||
|
||||
Returns HTTP `404` if the peer is not found.
|
||||
|
||||
### Access Classes
|
||||
|
||||
Manage VPN access classes that categorize peers by access level (e.g., full LAN access, internet-only).
|
||||
|
||||
#### List Access Classes
|
||||
|
||||
```
|
||||
GET /api/wireguard/classes
|
||||
```
|
||||
|
||||
Return all configured access classes.
|
||||
|
||||
**Response (`data`):**
|
||||
|
||||
Object keyed by class identifier, each with `name` and `description` fields.
|
||||
|
||||
#### Create Access Class
|
||||
|
||||
```
|
||||
POST /api/wireguard/classes
|
||||
```
|
||||
|
||||
Create a new access class.
|
||||
|
||||
**Request Body:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `key` | `string` | Yes | Class identifier (alphanumeric) |
|
||||
| `name` | `string` | No | Display name (defaults to key) |
|
||||
| `description` | `string` | No | Description text |
|
||||
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `key` | `string` | Class key |
|
||||
| `name` | `string` | Display name |
|
||||
| `description` | `string` | Description |
|
||||
|
||||
Returns HTTP `409` if the key already exists.
|
||||
|
||||
#### Update Access Class
|
||||
|
||||
```
|
||||
PATCH /api/wireguard/classes
|
||||
```
|
||||
|
||||
Update an existing access class.
|
||||
|
||||
**Request Body:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `key` | `string` | Yes | Class identifier |
|
||||
| `name` | `string` | No | New display name |
|
||||
| `description` | `string` | No | New description |
|
||||
|
||||
**Response (`data`):** Updated class object with `key`, `name`, `description`.
|
||||
|
||||
Returns HTTP `404` if the class is not found.
|
||||
|
||||
#### Delete Access Class
|
||||
|
||||
```
|
||||
DELETE /api/wireguard/classes
|
||||
```
|
||||
|
||||
Remove an access class. Cannot delete a class that has peers assigned to it.
|
||||
|
||||
**Request Body:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `key` | `string` | Yes | Class identifier |
|
||||
|
||||
**Response (`data`):** `{ "key": "<key>" }`
|
||||
|
||||
Returns HTTP `404` if the class is not found. Returns HTTP `409` if peers reference the class.
|
||||
|
||||
---
|
||||
|
||||
## Network API
|
||||
|
||||
+59
-3
@@ -268,7 +268,7 @@ The application reads `.account.conf` to determine registration status. If the f
|
||||
|
||||
**File**: `config/wireguard/config.json`
|
||||
|
||||
This file defines the WireGuard server interface and all connected peers. The application renders it into `/etc/wireguard/wg0.conf` and applies it with `wg-quick`. The file is created automatically when `initialize()` generates the server key pair via `wg genkey` / `wg pubkey`.
|
||||
This file defines the WireGuard server interface, access classes, and all connected peers. The application renders it into `/etc/wireguard/wg0.conf` and applies it with `wg-quick`. The file is created automatically when `initialize()` generates the server key pair and pre-seeds default access classes.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -278,9 +278,31 @@ This file defines the WireGuard server interface and all connected peers. The ap
|
||||
"private_key": "<generated>",
|
||||
"public_key": "<generated>",
|
||||
"addresses": ["10.137.0.1/24"],
|
||||
"server_endpoint": "vpn.example.com:51820",
|
||||
"description": "Main WireGuard server",
|
||||
"post_up": null,
|
||||
"post_down": null
|
||||
},
|
||||
"access_classes": {
|
||||
"full": {
|
||||
"name": "Full LAN Access",
|
||||
"description": "Peers get full access to internal networks",
|
||||
"subnet": "10.137.0.0/24",
|
||||
"listen_port": 51820,
|
||||
"lan_access": true,
|
||||
"private_key": "<generated>",
|
||||
"public_key": "<generated>"
|
||||
},
|
||||
"internet": {
|
||||
"name": "Internet Only",
|
||||
"description": "Peers can only reach the internet",
|
||||
"subnet": "10.137.1.0/24",
|
||||
"listen_port": 51821,
|
||||
"lan_access": false,
|
||||
"private_key": "<generated>",
|
||||
"public_key": "<generated>"
|
||||
}
|
||||
},
|
||||
"peers": {
|
||||
"alice": {
|
||||
"public_key": "<auto-generated>",
|
||||
@@ -288,7 +310,9 @@ This file defines the WireGuard server interface and all connected peers. The ap
|
||||
"endpoint": "203.0.113.1:51820",
|
||||
"allowed_ips": ["0.0.0.0/0"],
|
||||
"persistent_keepalive": 25,
|
||||
"preshared_key": null
|
||||
"preshared_key": null,
|
||||
"description": "Alice's office laptop",
|
||||
"access_class": "full"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,9 +327,39 @@ This file defines the WireGuard server interface and all connected peers. The ap
|
||||
| `private_key` | string | Yes (after init) | Base64-encoded private key for the server interface. Generated automatically by `initialize()` via `wg genkey`. |
|
||||
| `public_key` | string | Yes (after init) | Corresponding public key. Generated automatically by `initialize()` via `wg pubkey`. |
|
||||
| `addresses` | array | No | IP address(es) assigned to the server interface in CIDR notation (e.g., `10.137.0.1/24`). Default: `["10.137.0.1/24"]`. |
|
||||
| `server_endpoint` | string | No | External hostname:port for client connection. Used in generated client configs. Default: `""`. |
|
||||
| `description` | string | No | Free-text description of the WireGuard server. Default: `""`. |
|
||||
| `post_up` | string | No | Shell command to run after the interface is brought up. Common uses: adding NAT rules, enabling IP forwarding for the tunnel. Set to `null` to omit. Default: `null`. |
|
||||
| `post_down` | string | No | Shell command to run after the interface is brought down. Used to clean up rules added by `post_up`. Set to `null` to omit. Default: `null`. |
|
||||
|
||||
### Access Classes
|
||||
|
||||
Access classes define categories of VPN access. Each class gets its own WireGuard interface (``wg-<key>``), firewall zone (``vpn-<key>``), subnet, and listen port. Peers are assigned to a class and their config is rendered to that class's interface. Pre-seeded with `full` and `internet` defaults on first initialization. Manageable via `GET/POST/PATCH/DELETE /api/wireguard/classes`. Per-class tunnel lifecycle: `POST /api/wireguard/classes/<key>/up`, `POST /api/wireguard/classes/<key>/down`.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `name` | string | Yes | Human-readable display name for the class. |
|
||||
| `description` | string | No | Optional description of what access level this class provides. Default: `""`. |
|
||||
| `subnet` | string | Yes | CIDR subnet for the class's WireGuard interface (e.g., ``10.137.0.0/24``). Server address is derived as ``<base>.1/<prefix>``. |
|
||||
| `listen_port` | integer | Yes | UDP port for the class's WireGuard interface. Must be unique per class. |
|
||||
| `lan_access` | boolean | No | When ``true``, the sync subscriber adds inter-zone accept rules for internal subnets, allowing peers to reach the LAN. When ``false``, peers can only reach the internet via masquerade. Default: ``false``. |
|
||||
| `private_key` | string | Yes (auto) | Base64-encoded private key for the class's WireGuard interface. Auto-generated via ``POST /api/wireguard/classes/keys/<key>``. |
|
||||
| `public_key` | string | Yes (auto) | Corresponding public key. Auto-generated with ``private_key``. |
|
||||
|
||||
### Multi-Interface Behavior
|
||||
|
||||
When peers are assigned to an access class, the daemon:
|
||||
|
||||
1. Renders a separate ``wg-<key>.conf`` for each class that has assigned peers.
|
||||
2. Each class interface gets its own private/public key pair.
|
||||
3. The sync subscriber creates a ``vpn-<key>`` firewall zone per class with masquerade enabled.
|
||||
4. Classes with ``lan_access=true`` get additional inter-zone rules for internal subnets.
|
||||
5. ``apply`` brings up all class interfaces independently. Per-class ``up``/``down`` endpoints control individual tunnels.
|
||||
|
||||
### Legacy Single-Interface Mode
|
||||
|
||||
When no peers are assigned to any access class, the system falls back to the legacy single-interface mode where all peers share ``wg0``.
|
||||
|
||||
### Peer Fields
|
||||
|
||||
Peers are stored in an object keyed by a human-readable identifier (e.g., `alice`, `office-laptop`). Each peer entry defines a WireGuard peer configuration. When `add_peer()` is called, the peer's key pair is auto-generated. The `private_key` is stored for client configuration generation but stripped from all API responses.
|
||||
@@ -318,6 +372,8 @@ Peers are stored in an object keyed by a human-readable identifier (e.g., `alice
|
||||
| `allowed_ips` | array | No | CIDR blocks that traffic from this peer is allowed to route. Default: `[]` (no routing restrictions from the server side). `["0.0.0.0/0"]` allows all traffic. `["10.137.0.0/16"]` restricts traffic to the VPN subnet. |
|
||||
| `persistent_keepalive` | integer | No | Keepalive interval in seconds. `25` is recommended for peers behind NAT. Set to `0` or `null` to disable. Default: `null`. |
|
||||
| `preshared_key` | string | No | Optional pre-shared key for post-quantum resistance. Use `wg genpsk` to generate. Default: `null`. |
|
||||
| `description` | string | No | Optional description for the peer. Default: `""`. |
|
||||
| `access_class` | string | No | Key of the access class this peer belongs to (e.g., `"full"`, `"internet"`). `null` means unassigned. Default: `null`. |
|
||||
|
||||
### Client Configuration Generation
|
||||
|
||||
@@ -488,7 +544,7 @@ are updated automatically through the event bus.
|
||||
| Trigger Subsystem | Affected Subsystem | What Happens |
|
||||
|---|---|---|
|
||||
| dnsmasq (DHCP range) | firewall | Zone gains `dhcp`/`dns` services. Removing the last range removes them. DHCP ranges also back-propagate gateway (interface IP) so clients receive their default route. |
|
||||
| wireguard (peer add/remove) | firewall | `vpn` zone is created or maintained with `wg0` interface, masquerade, UDP 51820 rule, and inter-zone accept rules for each peer's allowed_ips subnets. Cleanup runs when no active peers exist. |
|
||||
| wireguard (peer add/remove) | firewall | Per-class `vpn-<key>` zones are created with `wg-<key>` interface, masquerade, UDP port rule, and inter-zone accept rules (only when ``lan_access=true``). Falls back to single `vpn` zone in legacy mode. Cleanup removes stale rules when classes have no peers. |
|
||||
| firewall (zone changes) | dnsmasq | Stale DHCP ranges (for interfaces no longer in any zone) are automatically removed. Masquerade-enabled zones ensure DHCP ranges carry the gateway. Zones with dhcp service but no range are logged as warnings. |
|
||||
| network (interface config) | firewall | Zone interface assignments in firewall config are updated — new interfaces are flagged, stale ones removed. |
|
||||
| network (interface config) | dnsmasq | Suggested DHCP ranges are logged when an interface has a static IP but no DHCP range. |
|
||||
|
||||
Reference in New Issue
Block a user