332d14e37d
- daemon: send full snapshot on connect; versions/tick now carry the full state of one subsystem (subsystem + data); no legacy updated/subsystems payloads; refresh_state and POST /status/refresh broadcast per-subsystem versions with data - client: modelSet() patches models in place; onMessage/topic refresh retired; 3s initial-load fallback via new POST /api/status/refresh - schema: lib/schema.py TypedDicts + hoover/schema.js defaults + docs/state-model.md as single source of truth for state shapes - system: poll at 1s, volatile metrics registered, dashboard uses a dedicated system model (status model removed) - firewall: refuse to strip both https and ssh from the default zone (409, force override via UI confirm); set_zone_services persists services to the declarative config; collector exposes default_zone - UI: pages migrate to flat state shapes; post-mutation modelFetch refreshes removed (WS delta covers it) - tests: ws snapshot/delta/broadcast, refresh-state, schema types, model-set/js ws handler and reconnect fallback
651 lines
34 KiB
Markdown
651 lines
34 KiB
Markdown
# Configuration Reference
|
|
|
|
This document describes the JSON configuration files used by Vacuum Wall to manage each subsystem. All persistent configuration is stored in the `config/` directory as declarative JSON. Runtime artifacts and generated files live in `data/`. The application renders these declarations into the format expected by each underlying service.
|
|
|
|
## DHCP/DNS Configuration
|
|
|
|
**File**: `config/dnsmasq/config.json`
|
|
|
|
This file defines all DHCP server settings and DNS resolution behavior for the dnsmasq service. The application renders it into `/etc/dnsmasq.d/vacuum-wall.conf`.
|
|
|
|
```json
|
|
{
|
|
"dhcp": {
|
|
"ranges": [
|
|
{
|
|
"interface": "eth1",
|
|
"start": "192.168.2.100",
|
|
"end": "192.168.2.200",
|
|
"lease_time": "12h",
|
|
"gateway": "192.168.2.1",
|
|
"dns": "192.168.2.1"
|
|
}
|
|
],
|
|
"static_leases": [
|
|
{
|
|
"mac": "aa:bb:cc:dd:ee:ff",
|
|
"ip": "192.168.2.50",
|
|
"hostname": "printer"
|
|
}
|
|
]
|
|
},
|
|
"dns": {
|
|
"upstreams": ["8.8.8.8", "1.1.1.1"],
|
|
"domain": "lan",
|
|
"custom_records": [
|
|
{
|
|
"name": "nas.lan",
|
|
"address": "192.168.2.10",
|
|
"hostname": "nas"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### DHCP Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `ranges` | array | No | One or more DHCP address pools. Each range defines a subnet from which addresses are leased. Default: `[]`. |
|
|
| `ranges[].interface` | string | Yes | Network interface on which to serve this DHCP range (e.g., `eth1`). |
|
|
| `ranges[].start` | string | Yes | First IP address in the pool. |
|
|
| `ranges[].end` | string | Yes | Last IP address in the pool. |
|
|
| `ranges[].lease_time` | string | No | DHCP lease duration. Accepts values like `12h`, `1d`, `30m`. Default: `12h`. |
|
|
| `ranges[].gateway` | string | No | Default gateway advertised to DHCP clients. Typically the router's LAN IP. |
|
|
| `ranges[].dns` | string | No | DNS server address advertised to DHCP clients. Typically the Vacuum Wall host's LAN IP. |
|
|
| `static_leases` | array | No | Fixed IP assignments tied to MAC addresses. Clients with matching MACs always receive the specified IP. Default: `[]`. |
|
|
| `static_leases[].mac` | string | Yes | MAC address of the client (colon-separated lowercase hex). |
|
|
| `static_leases[].ip` | string | Yes | The IP address to assign to this MAC. Must be outside the dynamic pool ranges. |
|
|
| `static_leases[].hostname` | string | No | Hostname to associate with the lease. Used for reverse DNS and mDNS. |
|
|
|
|
### DNS Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `upstreams` | array | Yes | Upstream DNS servers to forward unresolved queries to. Supports IPv4 and IPv6 addresses. Default: `["8.8.8.8", "1.1.1.1"]`. |
|
|
| `domain` | string | No | Local domain suffix. Hostnames without a FQDN are resolved within this domain (e.g., `printer` becomes `printer.lan`). Default: `null`. |
|
|
| `custom_records` | array | No | Static DNS A records for internal services and devices. Default: `[]`. |
|
|
| `custom_records[].name` | string | Yes | Fully qualified domain name (e.g., `nas.lan`). |
|
|
| `custom_records[].address` | string | Yes | The IP address to resolve the name to. |
|
|
| `custom_records[].hostname` | string | No | Short hostname without the domain suffix. Adds a reverse DNS entry as well. |
|
|
|
|
Additional dnsmasq directives can be appended verbatim by placing plain-text files in `data/dnsmasq/fragments/`. Each file's contents are concatenated into the generated config. This is useful for advanced options not covered by the JSON schema (e.g., `bogus-priv`, `cache-size`, `log-queries`).
|
|
|
|
## Nginx Configuration
|
|
|
|
**File**: `config/nginx/config.json`
|
|
|
|
This file defines reverse proxy domains with path-based routing, and global SSL settings. The application renders it into per-domain server block files in `data/nginx/sites-enabled/` and into the shared SSL snippet at `/etc/nginx/snippets/vacuum-wall-ssl.conf`.
|
|
|
|
```json
|
|
{
|
|
"domains": {
|
|
"app.example.com": {
|
|
"force_ssl": true,
|
|
"cert": "acme",
|
|
"auth": {
|
|
"user": "admin",
|
|
"htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
|
|
},
|
|
"paths": {
|
|
"/": {
|
|
"backend": {
|
|
"host": "192.168.2.50",
|
|
"port": 8080,
|
|
"proto": "http"
|
|
},
|
|
"headers": {
|
|
"X-Forwarded-Proto": "https"
|
|
}
|
|
},
|
|
"/api": {
|
|
"backend": {
|
|
"host": "192.168.2.51",
|
|
"port": 3000,
|
|
"proto": "http"
|
|
},
|
|
"auth": null
|
|
}
|
|
}
|
|
},
|
|
"mgmt.example.com": {
|
|
"force_ssl": true,
|
|
"cert": "acme",
|
|
"paths": {
|
|
"/": {
|
|
"backend": {
|
|
"host": "127.0.0.1",
|
|
"port": 9090,
|
|
"proto": "http"
|
|
},
|
|
"is_management": true,
|
|
"auth": {
|
|
"user": "admin",
|
|
"htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
|
|
}
|
|
},
|
|
"/ws": {
|
|
"backend": {
|
|
"host": "127.0.0.1",
|
|
"port": 9091,
|
|
"proto": "http"
|
|
},
|
|
"is_websocket": true
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"ssl": {
|
|
"protocols": "TLSv1.2 TLSv1.3",
|
|
"ciphers": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
|
|
"prefer_server_ciphers": false
|
|
}
|
|
}
|
|
```
|
|
|
|
### Domain Entries
|
|
|
|
The `domains` object maps domain names (keys) to proxy configurations. Each entry produces a separate nginx `server` block. All routing is path-based — a domain can proxy multiple paths to different backends.
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `paths` | object | Yes | Path-to-config map. Each key is a URL path (e.g., `"/"`, `"/api"`). No catch-all unless `"/"` is explicitly defined. |
|
|
| `force_ssl` | boolean | No | Enable HTTPS redirect. HTTP requests to this domain receive a 301 redirect to HTTPS. Default: `true`. |
|
|
| `cert` | string | No | Certificate provisioning method. One of: `"acme"`, `"file"`, or `"selfsigned"`. Omit for domains that don't need a dedicated cert. |
|
|
| `auth` | object | No | Domain-level HTTP basic auth configuration (`{ user, htpasswd }`). Applies to all paths unless overridden at the path level. |
|
|
|
|
### Path Entries
|
|
|
|
Each entry under `paths` defines a location block and its proxy backend.
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `backend` | object | Yes | The upstream service for this path. |
|
|
| `backend.host` | string | Yes | IP address or hostname of the backend service. |
|
|
| `backend.port` | integer | Yes | Port the backend service is listening on. |
|
|
| `backend.proto` | string | No | Protocol: `http` or `https`. Default: `http`. |
|
|
| `headers` | object | No | Custom proxy headers (key-value pairs). Supports nginx variable interpolation. |
|
|
| `auth` | object \| null | No | Path-level auth override. `{ user, htpasswd }` replaces domain-level auth. `null` disables auth for this path. |
|
|
| `is_management` | boolean | No | Marks this path as the Vacuum Wall WebUI backend. Suppresses security headers (X-Frame-Options, etc.) so the SPA works correctly. |
|
|
| `is_websocket` | boolean | No | Marks this path as a WebSocket pass-through. Disables auth, sets Upgrade/Connection headers, uses extended timeouts. |
|
|
|
|
### Auth Inheritance Rules
|
|
|
|
- Domain-level `auth` applies to all paths unless overridden.
|
|
- Path-level `auth: null` means "no auth" for that path.
|
|
- Path-level `auth: { ... }` overrides domain-level for that path.
|
|
- No other domain-level settings inherit — `headers` is path-only.
|
|
|
|
### Path ordering
|
|
|
|
Nginx evaluates `location` blocks by specificity: more specific prefixes (e.g., `/api`) always match before `/` by nginx's own priority rules. The order of keys in the `paths` dict does not affect routing behavior.
|
|
|
|
### Certificate Types
|
|
|
|
The `cert` field is a string that selects the provisioning method:
|
|
|
|
| Value | Description |
|
|
|---|---|
|
|
| `acme` | Vacuum Wall uses acme.sh to request and renew an ACME certificate via the HTTP-01 challenge. The nginx configuration serves ACME challenge files at `/.well-known/acme-challenge/`. |
|
|
| `file` | Use a pre-existing certificate and private key from the local file system. Vacuum Wall will not attempt to renew these certificates. |
|
|
| `selfsigned` | Vacuum Wall generates a self-signed certificate and private key on first apply. Useful for internal domains or testing. The generated certificate is stored at `data/certs/`. |
|
|
|
|
### Management Domain
|
|
|
|
The Vacuum Wall admin interface is configured as a regular domain entry under `domains`, with `is_management: true` on the path pointing to the Flask app. A second path (`/ws`) with `is_websocket: true` provides WebSocket pass-through for real-time state updates. This replaces the legacy `management` top-level key.
|
|
|
|
The application can create the `.htpasswd` file programmatically via `write_htpasswd()` (using passlib's SHA-256 crypt). Manual creation is also possible:
|
|
|
|
```bash
|
|
htpasswd -bc data/nginx/.htpasswd admin yourpassword
|
|
```
|
|
|
|
### Backward Compatibility
|
|
|
|
Config files using the legacy format are auto-migrated on first load:
|
|
- Domain entries with a top-level `backend` key are wrapped into `paths["/"]`.
|
|
- A legacy `management` top-level key is migrated into `domains[management.domain]` with `is_management` on the root path and a `/ws` WebSocket path.
|
|
|
|
### Global SSL Settings
|
|
|
|
The `ssl` block defines TLS parameters applied to all HTTPS server blocks via the shared snippet.
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `protocols` | string | No | nginx `ssl_protocols` directive value. Default: `TLSv1.2 TLSv1.3`. |
|
|
| `ciphers` | string | No | nginx `ssl_ciphers` directive value. Default is a curated AEAD-only cipher string. |
|
|
| `prefer_server_ciphers` | boolean | No | Whether to prefer server cipher order. Default: `false`. |
|
|
|
|
## ACME (Certificate) Configuration
|
|
|
|
**File**: `config/acme/config.json`
|
|
|
|
This file stores the ACME account settings used by acme.sh for certificate provisioning. Account registration, modification, and deactivation are performed through the WebUI at the Certificates page — not by editing this file directly.
|
|
|
|
```json
|
|
{
|
|
"email": "admin@example.com",
|
|
"ca": "letsencrypt"
|
|
}
|
|
```
|
|
|
|
### ACME Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `email` | string | No | Contact email for the ACME account. Used for certificate expiry notifications and recovery. Populated automatically when an account is registered via the WebUI. Default: `""`. |
|
|
| `ca` | string | No | ACME CA provider. One of: `"letsencrypt"` (Let's Encrypt), `"zerossl"` (ZeroSSL). Populated automatically when an account is registered. Default: `""`. |
|
|
|
|
### Account Registration
|
|
|
|
ACME account registration is handled entirely through the WebUI. When the user registers an account:
|
|
|
|
1. The user navigates to the Certificates page and clicks "Register Account".
|
|
2. Provides an email address and selects a CA provider (Let's Encrypt or ZeroSSL).
|
|
3. The backend calls `acme.sh --register-account` with the provided parameters.
|
|
4. On success, the `email` and `ca` fields in `config/acme/config.json` are populated, and acme.sh writes its `.account.conf` file under `data/acme/`.
|
|
|
|
Before any certificate can be issued, an ACME account must be registered. The certificate validation flow includes a blocking check (`account_registered`) that prevents issuance if no account exists.
|
|
|
|
### Account Management
|
|
|
|
After registration, the account can be managed from the WebUI:
|
|
|
|
- **Update email**: The Settings modal allows changing the contact email, which triggers an update via `acme.sh --register-account -u`.
|
|
- **Deactivate account**: The Settings modal includes a button to deactivate the account via `acme.sh --deactivate-account`, which clears the `email` and `ca` fields and removes the ACME account.
|
|
|
|
### ACME Home Directory
|
|
|
|
acme.sh stores its state under `data/acme/` (the ACME home directory). Key files:
|
|
|
|
- `.account.conf` — ACME account credentials and settings (contains `ACME_LEEMAIL`, `ACME_MCA`).
|
|
- `<domain>/` — Per-domain certificate and key files issued by acme.sh.
|
|
|
|
The application reads `.account.conf` to determine registration status. If the file is missing or lacks required keys, the account is considered unregistered.
|
|
|
|
## Auth Configuration
|
|
|
|
**File**: `config/auth/config.json`
|
|
|
|
This file defines JWT settings and WebAuthn Relying Party configuration for the authentication system.
|
|
|
|
```json
|
|
{
|
|
"jwt": {
|
|
"access_token_ttl": 900,
|
|
"refresh_token_ttl": 604800,
|
|
"algorithm": "HS256"
|
|
},
|
|
"webauthn": {
|
|
"rp_name": "Vacuum Wall",
|
|
"rp_id": "<management-domain>",
|
|
"origin": "https://<management-domain>"
|
|
}
|
|
}
|
|
```
|
|
|
|
### JWT Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `access_token_ttl` | integer | No | Access token lifetime in seconds. Default: `900` (15 minutes). |
|
|
| `refresh_token_ttl` | integer | No | Refresh token lifetime in seconds. Default: `604800` (7 days). |
|
|
| `algorithm` | string | No | JWT signing algorithm. Default: `"HS256"`. |
|
|
|
|
**Note:** JWT signing secrets are per-user, not shared. Each user's secret is auto-generated as a 32-byte base64url token (`secrets.token_urlsafe(32)`) and stored in the `users.jwt_secret` database column. Secrets are rotated on password change to invalidate all prior sessions.
|
|
|
|
### WebAuthn Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `rp_name` | string | Yes | Display name for the WebAuthn Relying Party. Shown during credential registration. |
|
|
| `rp_id` | string | Yes | Domain for WebAuthn credential binding. Must match the management domain. |
|
|
| `origin` | string | Yes | HTTPS URL for WebAuthn origin check. Must match `https://<rp_id>`. |
|
|
|
|
## Database Schema
|
|
|
|
The SQLite database at `data/auth.db` stores authentication data across four tables. Created automatically on first access via `get_db()`.
|
|
|
|
### users
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `id` | INTEGER | Auto-increment primary key |
|
|
| `username` | TEXT | Unique username |
|
|
| `password_hash` | TEXT | Argon2id password hash |
|
|
| `jwt_secret` | TEXT | Per-user JWT signing secret (32-byte base64url) |
|
|
| `created_at` | INTEGER | Unix timestamp (auto-set) |
|
|
|
|
### permissions
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `id` | INTEGER | Auto-increment primary key |
|
|
| `username` | TEXT | Foreign key to `users.username` (CASCADE on delete) |
|
|
| `subsystem` | TEXT | Subsystem name (e.g., `"firewall"`, `"dhcp"`, `"auth"`) |
|
|
| `level` | TEXT | Permission level: `"read"` or `"rw"` |
|
|
|
|
UNIQUE constraint on `(username, subsystem)`.
|
|
|
|
### token_blacklist
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `jti` | TEXT | Primary key — JWT unique identifier |
|
|
| `token_type` | TEXT | `"access"` or `"refresh"` |
|
|
| `expires` | INTEGER | Unix timestamp of token expiry |
|
|
|
|
Used to invalidate tokens on logout and password change. Expired entries are cleaned on every refresh operation.
|
|
|
|
### webauthn_creds
|
|
|
|
| Column | Type | Description |
|
|
|---|---|---|
|
|
| `id` | INTEGER | Auto-increment primary key |
|
|
| `username` | TEXT | Foreign key to `users.username` (CASCADE on delete) |
|
|
| `credential_id` | TEXT | Base64url-encoded credential ID |
|
|
| `public_key` | TEXT | Base64url-encoded public key |
|
|
| `sign_count` | INTEGER | Signature counter (replay prevention) |
|
|
| `name` | TEXT | User-assigned display name |
|
|
| `transports` | TEXT | JSON array of transport types |
|
|
|
|
UNIQUE constraint on `(username, credential_id)`.
|
|
|
|
## WireGuard Configuration
|
|
|
|
**File**: `config/wireguard/config.json`
|
|
|
|
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
|
|
{
|
|
"interface": {
|
|
"name": "wg0",
|
|
"listen_port": 51820,
|
|
"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>",
|
|
"private_key": "<auto-generated>",
|
|
"endpoint": "203.0.113.1:51820",
|
|
"allowed_ips": ["0.0.0.0/0"],
|
|
"persistent_keepalive": 25,
|
|
"preshared_key": null,
|
|
"description": "Alice's office laptop",
|
|
"access_class": "full"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Interface Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `name` | string | No | WireGuard interface name. Default: `wg0`. |
|
|
| `listen_port` | integer | No | Port the WireGuard interface listens on. Default: `51820`. Must be opened in the firewall. |
|
|
| `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.
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `public_key` | string | Yes | The peer's public key. Auto-generated when the peer is added. |
|
|
| `private_key` | string | No | The peer's private key, stored for generating downloadable client configuration files. Auto-generated when the peer is added. Stripped from all API responses — the WebUI never exposes peer private keys over the network. |
|
|
| `endpoint` | string | No | The peer's public endpoint (IP:port). Required for server-initiated connections (e.g., the server reaching out to a peer behind a firewall). Leave empty or `null` for peer-initiated connections where the peer connects to the server. Default: `null`. |
|
|
| `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
|
|
|
|
When a peer's `private_key` is set (which is the case when `add_peer()` auto-generates it), the WebUI can generate a complete WireGuard client configuration file that the user can download and import into their WireGuard client app. The generated config includes the peer's interface settings, the server as a `[Peer]` entry, and the appropriate `Endpoint` and `AllowedIPs` values. The `private_key` field is written into the client config file for download but is never returned by the API. `generate_client_conf()` computes the client IP address from the server's subnet and the peer's sorted index position.
|
|
|
|
### Applying Configuration
|
|
|
|
When configuration is saved through the WebUI or API, the application:
|
|
|
|
1. Renders the `wg0.conf` file from the JSON configuration.
|
|
2. Writes the file to `/etc/wireguard/wg0.conf` with `600` permissions via `sudo cp`.
|
|
3. Runs `sudo wg-quick up <name>` to apply the configuration.
|
|
4. Returns success or error status to the caller.
|
|
|
|
If the interface is already up, `wg-quick up` will reconfigure it in place without dropping existing connections.
|
|
|
|
## Firewall Configuration
|
|
|
|
**File**: `config/firewall/config.json`
|
|
|
|
This file defines the declarative firewalld zone configuration. The application compares it against the live firewalld state via `_compute_pending_changes()` and applies incremental changes. Runtime state backups are stored in `data/firewall/rules.json`.
|
|
|
|
```json
|
|
{
|
|
"zones": {
|
|
"public": {
|
|
"interfaces": ["eth0"],
|
|
"services": ["dhcp", "dns", "https", "ssh"],
|
|
"target": "DEFAULT",
|
|
"masquerade": true,
|
|
"forward_ports": [
|
|
{
|
|
"id": "abc123",
|
|
"port": 443,
|
|
"proto": "tcp",
|
|
"toaddr": "192.168.2.50",
|
|
"toport": 8080
|
|
}
|
|
],
|
|
"rich_rules": [
|
|
{
|
|
"rule": "rule family=\"ipv4\" source address=\"10.0.0.0/8\" reject"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Zone Fields
|
|
|
|
The `zones` object maps zone names (keys) to zone configurations. Each zone corresponds to a firewalld zone applied via `firewall-cmd`.
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `interfaces` | array | No | Network interfaces assigned to this zone. Computed against live state to detect pending changes. Default: `[]`. |
|
|
| `services` | array | No | Firewalld services to allow in this zone (e.g., `ssh`, `https`, `dns`, `dhcp`). Default: `[]`. |
|
|
| `target` | string | No | Zone target policy. One of: `DEFAULT`, `ACCEPT`, `DROP`, `REJECT`. The code maps these to firewalld's canonical target values (`default`, `ACCEPT`, `DROP`, `REJECT`). Default: `DEFAULT`. |
|
|
| `masquerade` | boolean | No | Enable IP masquerading (NAT) for this zone. Default: `false`. |
|
|
| `forward_ports` | array | No | Port forwarding rules. Each entry has an auto-generated `id` field and the standard firewalld forward-port fields. Default: `[]`. |
|
|
| `forward_ports[].id` | string | No | Auto-generated unique identifier for the port forwarding rule. Not user-settable. |
|
|
| `forward_ports[].port` | integer | Yes | Destination port to forward. |
|
|
| `forward_ports[].proto` | string | Yes | Protocol: `tcp` or `udp`. |
|
|
| `forward_ports[].toaddr` | string | No | Internal IP address to forward to. Omit for broadcast forwarding. |
|
|
| `forward_ports[].toport` | integer | No | Internal port to forward to. Omit to keep the same port. |
|
|
| `rich_rules` | array | No | Rich rule entries for advanced firewall policies. Default: `[]`. |
|
|
| `rich_rules[].rule` | string | Yes | The full firewalld rich rule string, e.g., `rule family="ipv4" source address="10.0.0.0/8" reject`. |
|
|
|
|
### Applying Firewall Configuration
|
|
|
|
The `config_pending()` function compares the declarative config in `config/firewall/config.json` against the live firewalld state returned by the daemon (via `daemon.handlers.firewall.get_state()`). It returns a diff indicating which zones have pending changes for interfaces, services, target, masquerade, forward ports, and rich rules. Zones that exist live but not in config are reported as `unmanaged_zones`.
|
|
|
|
Both `/api/firewall/zones/<name>/services` and `/api/firewall/config/apply` reconcile **remove-then-add** against the live zone, so anything opened outside the declarative config (e.g. directly via `firewall-cmd`) is reverted on the next apply. Service changes made through the API are persisted to `config.json` to prevent this drift.
|
|
|
|
**Management-lockout guard.** The firewalld *default zone* is the catch-all for interfaces with no explicit assignment (typically the WAN), and it carries the management plane (nginx https) plus remote recovery (ssh). Changing the default zone's service set so that **neither `https` nor `ssh`** remains raises `409 Conflict` — from `POST /firewall/zones/<name>/services` and `POST /firewall/config/apply` — before any mutation runs. Send `"force": true` in the request body to override (the UI shows a confirm dialog with this effect on the Zones page). If the default zone cannot be determined, the guard fails closed.
|
|
|
|
## Networkd (IP Configuration)
|
|
|
|
**File**: `config/network/config.json`
|
|
|
|
This file defines static IP configuration for network interfaces managed by systemd-networkd. The application renders each interface entry into a `50-<name>.network` INI file in `data/networkd/`, which the handler copies to `/etc/systemd/network/`.
|
|
|
|
```json
|
|
{
|
|
"interfaces": {
|
|
"eth0": {
|
|
"addresses": ["192.168.1.1/24"],
|
|
"gateway": "192.168.1.254",
|
|
"dns": ["8.8.8.8", "1.1.1.1"],
|
|
"dhcp": "no"
|
|
},
|
|
"eth1": {
|
|
"dhcp": "ipv4",
|
|
"dns_default_route": true,
|
|
"dhcp_client": {
|
|
"hostname": "router",
|
|
"use_dns": true
|
|
}
|
|
},
|
|
"wg0": {
|
|
"addresses": [{"address": "10.137.0.1/24"}],
|
|
"routes": [
|
|
{
|
|
"destination": "10.0.0.0/8",
|
|
"gateway": "10.137.0.2"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Interface Entry Fields
|
|
|
|
Each key in the `interfaces` object is an interface name (e.g., `eth0`, `eth1`, `wg0`). The value is a dict with the following keys:
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `addresses` | `array` | IPv4 addresses. Each item is either a bare CIDR string (`"192.168.1.1/24"`) or a dict with `address`, `label`, `scope`, `route_metric`, `duplicate_address_detection`, `manage_temporary_address`, `add_prefix_route`. Renders to `[Address]` sections. |
|
|
| `ipv6_addresses` | `array` | Same as `addresses`, but for IPv6. |
|
|
| `gateway` | `string` | Default IPv4 gateway (`[Network] Gateway=`). |
|
|
| `ipv6_gateway` | `string` | Default IPv6 gateway (`[Network] IPv6Gateway=`). |
|
|
| `dns` | `array` | IPv4 DNS servers (`[Network] DNS=`, one per line). |
|
|
| `ipv6_dns` | `array` | IPv6 DNS servers (`[Network] IPv6DNS=`). |
|
|
| `domains` | `array` | Search domains (`[Network] Domains=`). |
|
|
| `ipv6_domains` | `array` | IPv6 search domains (`[Network] IPv6Domains=`). |
|
|
| `dns_default_route` | `boolean` | Whether DNS is the default route for resolution (`[Network] DNSDefaultRoute=`). |
|
|
| `dhcp` | `string` | DHCP mode: `"yes"`, `"ipv4"`, `"ipv6"`, `"no"`. Controls `[Network] DHCP=` and whether `[DHCPv4]`/`[DHCPv6]` sections are rendered. |
|
|
| `routes` | `array` | Static routes. Each dict has `destination`, `gateway`, `metric`, `table`, `type`, `scope`, `gateway_on_link`, `ipv6_preference`, `initial_congestion_window`, `initial_advertised_receive_window`, `quick_ack`, `fast_open_no_cookie`, `mtu_bytes`, `protocol`, `next_hop`, `multi_path_route`. Renders to `[Route#N]` sections. |
|
|
| `link` | `object` | Link settings: `mtu_bytes`, `mac_address`, `arp`, `multicast`, `all_multicast`, `promiscuous`, `unmanaged`, `activation_policy`, `required_for_online`. Renders to `[Link]` section. |
|
|
| `dhcp_client` | `object` | DHCP client settings. Shared keys for both `[DHCPv4]` and `[DHCPv6]`: `hostname`, `duid_type`, `duid_raw_data`, `iaid`, `client_identifier`, `rapid_commit`, `anonymize`, `use_dns`, `use_ntp`, `use_sip`, `use_captive_portal`, `use_mtu`, `use_hostname`, `use_domains`, `use_routes`, `route_metric`, `send_decline`, `net_label`, `nft_set`, `ip_service_type`, `socket_priority`, `bootp`, `label`, `max_attempts`, `listen_port`, `server_port`, `mud_url`, `boot_filename`, `send_option`, `send_vendor_option`, `user_class`, `vendor_class_identifier`, `request_options`. |
|
|
| `bind_carrier` | `array` | Carrier interfaces to bind to. |
|
|
| `ignore_carrier_loss` | `boolean` | Ignore carrier loss events. |
|
|
| `keep_configuration` | `boolean` | Keep configuration on stop. |
|
|
| `configure_without_carrier` | `boolean` | Configure even without carrier. |
|
|
| `link_local_addressing` | `string` | Link-local addressing mode. |
|
|
| `ipv6_link_local_address_generation_mode` | `string` | IPv6 link-local address generation mode. |
|
|
| `ipv6_stable_secret_address` | `string` | Stable secret for IPv6 address generation. |
|
|
| `ipv4_ll_start_address` | `string` | Link-local IPv4 start address. |
|
|
| `ipv4_ll_route` | `boolean` | Add route to link-local IPv4 address. |
|
|
| `default_route_on_device` | `boolean` | Always add default route via this device. |
|
|
| `ipv6_hop_limit` | `int` | IPv6 hop limit. |
|
|
| `ipv6_retransmission_time_sec` | `string` | IPv6 retransmission timeout. |
|
|
| `ipv4_duplicate_address_detection_timeout_sec` | `string` | IPv4 DAD timeout. |
|
|
| `ipv4_reverse_path_filter` | `string` | IPv4 reverse path filtering mode. |
|
|
| `ipv4_accept_local` | `boolean` | Accept packets to local addresses as non-local. |
|
|
| `ipv4_route_localnet` | `boolean` | Route local network traffic. |
|
|
| `ipv4_proxy_arp` | `boolean` | Enable proxy ARP. |
|
|
| `ipv4_proxy_arp_private_vlan` | `boolean` | Private VLAN proxy ARP. |
|
|
| `ipv6_proxy_ndp` | `boolean` | Enable IPv6 proxy NDP. |
|
|
| `ipv6_proxy_ndp_address` | `string` | IPv6 proxy NDP address. |
|
|
| `ipv6_send_ra` | `boolean` | Send IPv6 Router Advertisements. |
|
|
| `m_pls_routing` | `boolean` | Enable MPLS routing. |
|
|
| `keep_master` | `boolean` | Keep master on stop. |
|
|
| `ip_family` | `string` | IP family to use. |
|
|
|
|
Keys not in the recognized set will be saved to `config.json` but won't be rendered to `.network` files. A warning is logged identifying any unrecognized keys.
|
|
|
|
### DNS Upstream Sync
|
|
|
|
When `POST /api/network/apply` is called, the handler automatically collects public DNS servers from all networkd interface configs (via `collect_upstream_dns()`), filters out local/private-range addresses, and syncs the deduplicated list to dnsmasq's upstream DNS configuration. This keeps dnsmasq's upstream resolvers in sync with whatever DNS the WAN interface receives (whether statically configured or via DHCP).
|
|
|
|
### Generated Files
|
|
|
|
Each interface config entry produces a `50-<name>.network` file in `data/networkd/`. During apply, these are copied to `/etc/systemd/network/` and stale files (not matching any config entry) are removed. File generation uses the `systemd.syntax(7)` naming convention: first section is bare (`[Address]`, `[Route]`), subsequent sections use `#` suffix (`[Address#1]`, `[Route#2]`).
|
|
|
|
## Cross-Subsystem Dependencies
|
|
|
|
Some subsystems depend on each other. When you modify one, related subsystems
|
|
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 | 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. |
|
|
|
|
Note: The firewall "Apply" button is still needed to push config changes to
|
|
firewalld. Sync only updates the declarative JSON.
|
|
|
|
Additionally, on daemon startup, `lib/system_import.py` reconciles live system
|
|
configs (dnsmasq, wireguard, networkd, nginx, firewall) with the declarative
|
|
JSON. This prevents drift when configs were created by the install script or
|
|
edited manually in system files. Reconciliation only writes when the existing
|
|
JSON differs or is missing — no data is lost on re-run. |