739253b2e5
- websocket.js: schedule reconnect backoff when token refresh fails, otherwise WebSocket stays dead after 3+ disconnects with failed refresh - daemon/handlers/auth.py: remove two redundant try/except ValueError: raise blocks in webauthn register/authenticate finish handlers - docs/api.md: mark permissions as optional in Create User endpoint
2070 lines
48 KiB
Markdown
2070 lines
48 KiB
Markdown
# REST API Reference
|
|
|
|
All endpoints are served by the management WebUI Flask application bound to `127.0.0.1:9090`, proxied through nginx with SSL termination. Authentication is handled at the Flask layer via JWT — the `Authorization: Bearer <token>` header. Public endpoints (login, WebAuthn authenticate) do not require a token.
|
|
|
|
Every request and response uses `Content-Type: application/json`.
|
|
|
|
## Authentication
|
|
|
|
Most endpoints require a valid JWT access token. The token is obtained by logging in via `POST /api/auth/login` or completing a WebAuthn authentication ceremony.
|
|
|
|
### Obtaining a Token
|
|
|
|
1. Call `POST /api/auth/login` with credentials
|
|
2. Store the returned `access_token`
|
|
3. Include `Authorization: Bearer <access_token>` on all subsequent requests
|
|
4. Refresh before expiry via `POST /api/auth/refresh`
|
|
|
|
### Permission Checks
|
|
|
|
Each request is checked against per-subsystem permissions. `GET` requires `"read"` or `"rw"` on the subsystem. `POST`/`PATCH`/`DELETE` requires `"rw"`. User management endpoints (`/api/auth/users/*`) require `auth: "rw"`.
|
|
|
|
## Conventions
|
|
|
|
### Success Responses
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"data": <value>
|
|
}
|
|
```
|
|
|
|
The `data` field contains the payload, which may be an object, array, string, or `null`.
|
|
|
|
### Error Responses
|
|
|
|
```json
|
|
{
|
|
"ok": false,
|
|
"error": "<human-readable message>"
|
|
}
|
|
```
|
|
|
|
Error responses carry one of the following HTTP status codes:
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
| `400` | Bad request — invalid body, missing required field, or malformed value |
|
|
| `404` | Not found — the requested resource does not exist |
|
|
| `409` | Conflict — the requested operation conflicts with an existing resource |
|
|
| `500` | Internal server error — unexpected failure in the backend |
|
|
|
|
### Route Patterns
|
|
|
|
Resource identification uses **path parameters** whenever possible. Exceptions occur only when the identifier is inherently long (e.g., a rich rule string), in which case the body carries the identifier.
|
|
|
|
---
|
|
|
|
## Auth API
|
|
|
|
Endpoints prefixed with `/api/auth/...`. Manage authentication, session, tokens, and user accounts.
|
|
|
|
### Login
|
|
|
|
#### Password Login
|
|
|
|
```
|
|
POST /api/auth/login
|
|
```
|
|
|
|
Authenticate with username and password. Returns access and refresh tokens.
|
|
|
|
**Auth:** Public — no JWT required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | Yes | Username |
|
|
| `password` | `string` | Yes | Plain-text password (hashed for verification) |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `tokens` | `object` | Contains `access_token`, `refresh_token`, and `session_id` |
|
|
| `access_ttl` | `integer` | Access token lifetime in seconds (default: `900`) |
|
|
| `user` | `object` | User info (`username`, `id`) |
|
|
| `permissions` | `object` | Per-subsystem permissions |
|
|
|
|
Returns HTTP `401` if credentials are invalid.
|
|
|
|
---
|
|
|
|
### Session
|
|
|
|
#### Get Current Session
|
|
|
|
```
|
|
GET /api/auth/session
|
|
```
|
|
|
|
Return the current authenticated user and permissions.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `user` | `object` | User info (`username`, `id`) |
|
|
| `permissions` | `object` | Per-subsystem permissions |
|
|
|
|
Returns HTTP `401` if token is invalid, expired, or blacklisted.
|
|
|
|
#### Logout
|
|
|
|
```
|
|
POST /api/auth/logout
|
|
```
|
|
|
|
Invalidate the current session by blacklisting the access token.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
#### Refresh Tokens
|
|
|
|
```
|
|
POST /api/auth/refresh
|
|
```
|
|
|
|
Rotate token pair. Validates the refresh token, blacklists the old pair, and issues new access and refresh tokens.
|
|
|
|
**Auth:** Refresh token required.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `tokens` | `object` | Contains `access_token`, `refresh_token`, and `session_id` |
|
|
| `access_ttl` | `integer` | Access token lifetime in seconds |
|
|
| `user` | `object` | User info (`username`, `id`) |
|
|
| `permissions` | `object` | Per-subsystem permissions |
|
|
|
|
Returns HTTP `401` if refresh token is invalid, expired, or blacklisted.
|
|
|
|
#### Change Password
|
|
|
|
```
|
|
POST /api/auth/password
|
|
```
|
|
|
|
Change the current user's password.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | No | Auto-injected from JWT context |
|
|
| `oldPassword` | `string` | Yes | Current password |
|
|
| `newPassword` | `string` | Yes | New password |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `400` if old password is incorrect.
|
|
|
|
---
|
|
|
|
### User Management
|
|
|
|
#### List Users
|
|
|
|
```
|
|
GET /api/auth/users
|
|
```
|
|
|
|
List all users. Requires admin permission (`auth: "rw"`).
|
|
|
|
**Auth:** `auth: "rw"` required.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `users` | `[object, ...]` | Array of user summaries (`id`, `username`, `permissions`) |
|
|
|
|
#### Create User
|
|
|
|
```
|
|
POST /api/auth/users
|
|
```
|
|
|
|
Create a new user with password and per-subsystem permissions.
|
|
|
|
**Auth:** `auth: "rw"` required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | Yes | Username |
|
|
| `password` | `string` | Yes | Plain-text password |
|
|
| `permissions` | `object` | No | Per-subsystem permissions (`{ subsystem: "read" \| "rw" }`) |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `409` if username already exists.
|
|
|
|
#### Update User
|
|
|
|
```
|
|
POST /api/auth/users/<username>
|
|
```
|
|
|
|
Update user's permissions. (To change a password, use `POST /api/auth/password`.)
|
|
|
|
**Auth:** `auth: "rw"` required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `permissions` | `object` | No | New per-subsystem permissions |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if user not found.
|
|
|
|
#### Delete User
|
|
|
|
```
|
|
DELETE /api/auth/users/<username>
|
|
```
|
|
|
|
Delete a user and all associated permissions and WebAuthn credentials (CASCADE).
|
|
|
|
**Auth:** `auth: "rw"` required. Cannot delete self.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if user not found.
|
|
|
|
---
|
|
|
|
### WebAuthn
|
|
|
|
#### Begin Registration
|
|
|
|
```
|
|
POST /api/auth/webauthn/register-begin
|
|
```
|
|
|
|
Start WebAuthn credential registration. Returns options for `navigator.credentials.create()`.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | Yes | Username to register for |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `challenge` | `string` | Base64url challenge |
|
|
| `rp` | `object` | Relying party config (`id`, `name`) |
|
|
| `user` | `object` | User info for registration |
|
|
| `excludeCredentials` | `[object, ...]` | Credentials to exclude |
|
|
|
|
#### Finish Registration
|
|
|
|
```
|
|
POST /api/auth/webauthn/register-finish
|
|
```
|
|
|
|
Complete WebAuthn credential registration. Verifies the attestation response and stores the credential in the database.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | Yes | Username |
|
|
| `response` | `object` | Yes | WebAuthn authenticator attestation response |
|
|
| `name` | `string` | No | Display name for this credential |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `400` if verification fails.
|
|
|
|
#### Begin Authentication
|
|
|
|
```
|
|
POST /api/auth/webauthn/authenticate-begin
|
|
```
|
|
|
|
Start WebAuthn authentication. Returns options for `navigator.credentials.get()`.
|
|
|
|
**Auth:** Public — no JWT required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | Yes | Username to authenticate |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `challenge` | `string` | Base64url challenge |
|
|
| `allowCredentials` | `[object, ...]` | Registered credentials for this user |
|
|
|
|
Returns `{"ok": true, "data": {"no_webauthn": true}}` if user has no WebAuthn credentials (use password instead).
|
|
|
|
#### Finish Authentication
|
|
|
|
```
|
|
POST /api/auth/webauthn/authenticate-finish
|
|
```
|
|
|
|
Complete WebAuthn authentication. Verifies the assertion and issues tokens on success.
|
|
|
|
**Auth:** Public — no JWT required.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `username` | `string` | Yes | Username |
|
|
| `assertion_response` | `object` | Yes | WebAuthn authenticator assertion response |
|
|
| `auth_options` | `object` | Yes | Original authentication options from `authenticate-begin` |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `tokens` | `object` | Contains `access_token`, `refresh_token`, and `session_id` |
|
|
| `access_ttl` | `integer` | Access token lifetime in seconds |
|
|
| `user` | `object` | User info (`username`, `id`) |
|
|
| `permissions` | `object` | Per-subsystem permissions |
|
|
|
|
Returns HTTP `400` if verification fails.
|
|
|
|
#### List Credentials
|
|
|
|
```
|
|
GET /api/auth/webauthn/credentials
|
|
```
|
|
|
|
List WebAuthn credentials for the current user.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Response (`data`):**
|
|
|
|
Array of credential objects (`id`, `name`, `transports`, `credentialId`, `signCount`, `createdAt`).
|
|
|
|
#### Credential Counts
|
|
|
|
```
|
|
GET /api/auth/webauthn/credential-counts
|
|
```
|
|
|
|
Return credential counts for all users. Admin endpoint.
|
|
|
|
**Auth:** `auth: "rw"` required.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `counts` | `object` | Dict mapping usernames to credential counts (`{"alice": 2, "bob": 1}`) |
|
|
|
|
#### Remove Credential
|
|
|
|
```
|
|
DELETE /api/auth/webauthn/creds/<credential_id>
|
|
```
|
|
|
|
Remove a WebAuthn credential.
|
|
|
|
**Auth:** Access token required.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if credential not found.
|
|
|
|
---
|
|
|
|
## Firewall API
|
|
|
|
Endpoints prefixed with `/api/firewall/...`. Interact with firewalld for zone management, rich rules, NAT, and masquerade.
|
|
|
|
### Declarative Config
|
|
|
|
The firewall supports a two-step declarative workflow: save config to `config/firewall/config.json`, then apply it to live firewalld. The config tracks `rich_rules` and `forward_ports` with auto-generated `id` fields.
|
|
|
|
#### Get Config
|
|
|
|
```
|
|
GET /api/firewall/config
|
|
```
|
|
|
|
Return the current declarative firewall config.
|
|
|
|
**Response:** `data` contains the config object with a `zones` mapping.
|
|
|
|
#### Save Config
|
|
|
|
```
|
|
POST /api/firewall/config
|
|
```
|
|
|
|
Replace the declarative config. Returns pending changes summary.
|
|
|
|
**Request Body:** Request body must contain `zones`.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `config_saved` | `boolean` | Always `true` |
|
|
| `pending` | `[object, ...]` | List of pending changes |
|
|
| `needs_apply` | `boolean` | Whether changes need to be applied |
|
|
| `unmanaged_zones` | `object` | Zones active on system but not in config |
|
|
|
|
#### Apply Config
|
|
|
|
```
|
|
POST /api/firewall/config/apply
|
|
```
|
|
|
|
Apply the declarative config to live firewalld. Applies targets, services, interfaces, masquerade, rich rules, and forward ports.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `applied_zones` | `[string, ...]` | List of zone names that were applied |
|
|
| `backup` | `string` | Path to the firewall state backup file |
|
|
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
|
|
|
|
#### Check Pending Changes
|
|
|
|
```
|
|
GET /api/firewall/config/pending
|
|
```
|
|
|
|
Compare declarative config against live firewalld state. Returns diff for interfaces, services, targets, masquerade, rich rules, and forward ports.
|
|
|
|
**Response:** Same structure as POST /config response.
|
|
|
|
#### Partial Update Config
|
|
|
|
```
|
|
PATCH /api/firewall/config
|
|
```
|
|
|
|
Deep-merge the provided fields into the existing config. Returns pending changes summary.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `config_saved` | `boolean` | Always `true` |
|
|
| `pending` | `[object, ...]` | List of pending changes |
|
|
| `needs_apply` | `boolean` | Whether changes need to be applied |
|
|
| `unmanaged_zones` | `object` | Zones active on system but not in config |
|
|
|
|
### Zone Management
|
|
|
|
#### List All Zones
|
|
|
|
```
|
|
GET /api/firewall/zones
|
|
```
|
|
|
|
Returns active zone-to-interface mappings and all available zone definitions.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data.active` | `object<name, [interface, ...]>` | Currently assigned interfaces per zone |
|
|
| `data.available` | `[string, ...]` | All zones known to firewalld |
|
|
|
|
---
|
|
|
|
#### Get Zone Details
|
|
|
|
```
|
|
GET /api/firewall/zones/<name>
|
|
```
|
|
|
|
Return detailed configuration for a single zone.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `target` | `string` | Zone target (e.g., `"default"`, `"ACCEPT"`, `"REJECT"`) |
|
|
| `interfaces` | `[string, ...]` | Interfaces assigned to this zone |
|
|
| `services` | `[string, ...]` | Services allowed through the zone |
|
|
| `ports` | `[string, ...]` | Explicit port rules (format: `"443/tcp"`) |
|
|
| `masquerade` | `boolean` | Whether masquerade (NAT) is enabled |
|
|
| `forward_ports` | `[{port, proto, toaddr, toport}, ...]` | Port forward rules |
|
|
| `rich_rules` | `[{rule, id}, ...]` | Rich rule definitions with IDs |
|
|
|
|
Returns HTTP `404` if the zone does not exist.
|
|
|
|
---
|
|
|
|
#### Create Zone
|
|
|
|
```
|
|
POST /api/firewall/zones
|
|
```
|
|
|
|
Create a new firewalld zone.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `name` | `string` | Yes | Zone name |
|
|
| `target` | `string` | No | Zone target; defaults to `"default"` |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `400` if the zone already exists.
|
|
|
|
---
|
|
|
|
#### Delete Zone
|
|
|
|
```
|
|
DELETE /api/firewall/zones/<name>
|
|
```
|
|
|
|
Remove a zone from firewalld.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if the zone does not exist.
|
|
|
|
### Zone Configuration
|
|
|
|
#### Set Zone Interfaces
|
|
|
|
```
|
|
POST /api/firewall/zones/<name>/interfaces
|
|
```
|
|
|
|
Replace all interfaces assigned to the zone with the provided list.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `interfaces` | `[string, ...]` | Yes | List of interface names |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `interfaces` | `[string, ...]` | List of interface names now assigned |
|
|
|
|
---
|
|
|
|
#### Set Zone Services
|
|
|
|
```
|
|
POST /api/firewall/zones/<name>/services
|
|
```
|
|
|
|
Replace all services allowed in the zone with the provided list.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `services` | `[string, ...]` | Yes | List of firewalld service names |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `services` | `[string, ...]` | List of services now allowed |
|
|
|
|
### Rich Rules
|
|
|
|
#### Add Rich Rule
|
|
|
|
```
|
|
POST /api/firewall/rich-rules
|
|
```
|
|
|
|
Add a firewalld rich rule to a zone. The rule is persisted to the declarative config with an auto-generated `id`.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `zone` | `string` | Yes | Zone to add the rule to |
|
|
| `rule` | `string` | Yes | Full rich rule string |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `id` | `string` | 8-character unique ID |
|
|
| `rule` | `string` | Full rich rule string |
|
|
|
|
---
|
|
|
|
#### Remove Rich Rule
|
|
|
|
```
|
|
DELETE /api/firewall/rich-rules/<zone>/<id>
|
|
```
|
|
|
|
Remove a rich rule by zone and auto-generated ID. (The rule string itself is too long for a URL path.)
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `id` | `string` | ID of the removed rule |
|
|
|
|
Returns HTTP `404` if the rule ID is not found.
|
|
|
|
---
|
|
|
|
#### List Rich Rules
|
|
|
|
```
|
|
GET /api/firewall/rich-rules/<zone>
|
|
```
|
|
|
|
Return all rich rules for the specified zone, each with an `id` and `rule` string.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[{id, rule}, ...]` | Rich rules with IDs |
|
|
|
|
### Port Forwarding
|
|
|
|
#### Add Port Forward
|
|
|
|
```
|
|
POST /api/firewall/forward-port
|
|
```
|
|
|
|
Add a port forwarding rule to a zone. The rule is persisted to the declarative config with an auto-generated `id`.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `zone` | `string` | Yes | Zone to add the rule to |
|
|
| `port` | `number` | Yes | External port |
|
|
| `proto` | `string` | Yes | Protocol (`"tcp"` or `"udp"`) |
|
|
| `toaddr` | `string` | No | Internal destination address |
|
|
| `toport` | `number` | No | Internal destination port |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `id` | `string` | 8-character unique ID |
|
|
| `port` | `number` | External port |
|
|
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
|
|
|
|
---
|
|
|
|
#### Remove Port Forward
|
|
|
|
```
|
|
DELETE /api/firewall/forward-port/<zone>/<port>/<proto>
|
|
```
|
|
|
|
Remove a port forwarding rule. Zone, port, and protocol are all path parameters.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `port` | `number` | External port |
|
|
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
|
|
|
|
Returns HTTP `404` if the forward port is not found.
|
|
|
|
### Masquerade (NAT)
|
|
|
|
#### Enable / Disable Masquerade
|
|
|
|
```
|
|
POST /api/firewall/masquerade
|
|
```
|
|
|
|
Toggle masquerade (source NAT) for a zone.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `zone` | `string` | Yes | Zone to configure |
|
|
| `enable` | `boolean` | Yes | `true` to enable, `false` to disable |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `zone` | `string` | Zone name |
|
|
| `masquerade` | `boolean` | Whether masquerade is now enabled |
|
|
|
|
### State
|
|
|
|
#### Get Firewall State
|
|
|
|
```
|
|
GET /api/firewall/state
|
|
```
|
|
|
|
Return current firewall state from the daemon state store. Provides live firewall state data including active zones, services, and interfaces as polled by the daemon.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `object` | Firewall state data from the state collector |
|
|
|
|
### Info
|
|
|
|
#### Available Services
|
|
|
|
```
|
|
GET /api/firewall/services
|
|
```
|
|
|
|
List all service names known to firewalld.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[string, ...]` | Service names |
|
|
|
|
---
|
|
|
|
#### Available Interfaces
|
|
|
|
```
|
|
GET /api/firewall/interfaces
|
|
```
|
|
|
|
List all network interfaces currently available on the system.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[string, ...]` | Interface names |
|
|
|
|
---
|
|
|
|
## DHCP / DNS API
|
|
|
|
Endpoints prefixed with `/api/dhcp/...`. Manage dnsmasq configuration, DHCP leases, and custom DNS records.
|
|
|
|
### Configuration
|
|
|
|
#### Get Configuration
|
|
|
|
```
|
|
GET /api/dhcp/config
|
|
```
|
|
|
|
Return the current DHCP/DNS configuration object.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `object` | Full dnsmasq configuration dictionary |
|
|
|
|
---
|
|
|
|
#### Replace Configuration
|
|
|
|
```
|
|
POST /api/dhcp/config
|
|
```
|
|
|
|
Replace the entire configuration with the provided JSON object.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| *(entire body)* | `object` | Yes | Complete configuration object |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Partial Update Configuration
|
|
|
|
```
|
|
PATCH /api/dhcp/config
|
|
```
|
|
|
|
Deep-merge the provided fields into the existing configuration. Useful for targeted updates (e.g., changing DNS upstream servers without replacing the full config).
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| *(any subset)* | `any` | Yes | Fields to merge into the existing config |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Apply Configuration
|
|
|
|
```
|
|
POST /api/dhcp/apply
|
|
```
|
|
|
|
Write the in-memory configuration to `/etc/dnsmasq.d/vacuum-wall.conf` and reload the dnsmasq service.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
### Status
|
|
|
|
#### Get Service Status
|
|
|
|
```
|
|
GET /api/dhcp/status
|
|
```
|
|
|
|
Return the current service status, config summary, and active lease count.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `service_active` | `boolean` | Whether dnsmasq is running |
|
|
| `config_file_exists` | `boolean` | Whether config file exists on disk |
|
|
| `config_in_sync` | `boolean` | Whether disk config matches expected |
|
|
| `dhcp_ranges` | `number` | Number of DHCP ranges |
|
|
| `static_leases` | `number` | Number of static leases |
|
|
| `custom_dns_records` | `number` | Number of custom DNS records |
|
|
| `upstreams` | `[string, ...]` | Upstream DNS servers |
|
|
| `domain` | `string` | Local DNS domain |
|
|
| `active_leases` | `number` | Number of active leases |
|
|
| `leases` | `[object, ...]` | Active lease objects |
|
|
|
|
### DHCP Ranges
|
|
|
|
#### Add Range
|
|
|
|
```
|
|
POST /api/dhcp/ranges
|
|
```
|
|
|
|
Add or replace the DHCP range for a given interface.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `interface` | `string` | No | Interface name (empty = all interfaces) |
|
|
| `start` | `string` | Yes | Start of IP range |
|
|
| `end` | `string` | Yes | End of IP range |
|
|
| `lease_time` | `string` | No | Lease duration; defaults to `"12h"` |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Remove Range
|
|
|
|
```
|
|
DELETE /api/dhcp/ranges
|
|
```
|
|
|
|
Remove a DHCP range. Body contains identifying fields.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `interface` | `string` | Yes | Interface name |
|
|
| `start` | `string` | Yes | Start of IP range |
|
|
| `end` | `string` | Yes | End of IP range |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
### Static Leases
|
|
|
|
#### Add Static Lease
|
|
|
|
```
|
|
POST /api/dhcp/static-lease
|
|
```
|
|
|
|
Add a static (reserved) DHCP lease.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `mac` | `string` | Yes | MAC address (`"aa:bb:cc:dd:ee:ff"`) |
|
|
| `ip` | `string` | Yes | Reserved IP address |
|
|
| `hostname` | `string` | No | Hostname for the reservation |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `mac` | `string` | MAC address |
|
|
| `ip` | `string` | Reserved IP address |
|
|
| `hostname` | `string` | Hostname |
|
|
|
|
---
|
|
|
|
#### Remove Static Lease
|
|
|
|
```
|
|
DELETE /api/dhcp/static-lease/<mac>
|
|
```
|
|
|
|
Remove a static lease by MAC address.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if no matching lease is found.
|
|
|
|
### Live Leases
|
|
|
|
#### Get Live Leases
|
|
|
|
```
|
|
GET /api/dhcp/leases
|
|
```
|
|
|
|
Return the current DHCP lease table from dnsmasq.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[object, ...]` | Array of lease objects |
|
|
|
|
### DNS Records
|
|
|
|
#### Add DNS Record
|
|
|
|
```
|
|
POST /api/dhcp/dns-record
|
|
```
|
|
|
|
Add a custom DNS A record served by dnsmasq.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `name` | `string` | Yes | Fully qualified domain name |
|
|
| `address` | `string` | Yes | IP address to resolve to |
|
|
| `hostname` | `string` | No | Short hostname |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `string` | Fully qualified domain name |
|
|
| `address` | `string` | IP address |
|
|
| `hostname` | `string` | Short hostname |
|
|
|
|
---
|
|
|
|
#### Remove DNS Record
|
|
|
|
```
|
|
DELETE /api/dhcp/dns-record/<name>
|
|
```
|
|
|
|
Remove a custom DNS record by domain name.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if no matching record is found.
|
|
|
|
---
|
|
|
|
## Proxy API
|
|
|
|
Endpoints prefixed with `/api/proxy/...`. Manage reverse proxy domains, nginx configuration generation, and the management WebUI proxy.
|
|
|
|
### Configuration
|
|
|
|
#### Get Proxy Configuration
|
|
|
|
```
|
|
GET /api/proxy/config
|
|
```
|
|
|
|
Return the current proxy configuration object.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `object` | Full proxy configuration dictionary |
|
|
|
|
---
|
|
|
|
#### Replace Proxy Configuration
|
|
|
|
```
|
|
POST /api/proxy/config
|
|
```
|
|
|
|
Replace the entire proxy configuration with the provided JSON object.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| *(entire body)* | `object` | Yes | Complete proxy configuration object |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Partial Update Proxy Configuration
|
|
|
|
```
|
|
PATCH /api/proxy/config
|
|
```
|
|
|
|
Deep-merge the provided fields into the existing proxy configuration.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| *(any subset)* | `any` | Yes | Fields to merge into the config |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
### SSL
|
|
|
|
#### Apply SSL Snippet
|
|
|
|
```
|
|
POST /api/proxy/ssl-apply
|
|
```
|
|
|
|
Write the global nginx SSL snippet configuration.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
### Domain Management
|
|
|
|
#### List All Domains
|
|
|
|
```
|
|
GET /api/proxy/domains
|
|
```
|
|
|
|
Return all configured proxy domains. The response is flattened by path — each path within a domain produces a separate entry.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[object, ...]` | Array of path-level domain configuration objects |
|
|
|
|
Each entry contains `domain` (string), `path` (string), `backend` (object with `host`, `port`, `proto`), `online` (boolean), `force_ssl` (boolean), and optionally `is_management` or `is_websocket` flags.
|
|
|
|
---
|
|
|
|
#### Add Domain
|
|
|
|
```
|
|
POST /api/proxy/domains
|
|
```
|
|
|
|
Add a new reverse proxy domain. Accepts two modes:
|
|
|
|
**Paths mode (preferred):**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `domain` | `string` | Yes | Domain name to proxy |
|
|
| `paths` | `object` | Yes | Path-to-config map. Each path entry must have a `backend` key with `host`, `port`, `proto`. |
|
|
| `cert` | `string` | No | Certificate type |
|
|
| `force_ssl` | `boolean` | No | HTTPS redirect flag (default `true`) |
|
|
|
|
**Legacy mode (backward compatible):**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `domain` | `string` | Yes | Domain name to proxy |
|
|
| `backend_host` | `string` | Yes | Backend server IP or hostname |
|
|
| `backend_port` | `number` | Yes | Backend server port |
|
|
| `backend_proto` | `string` | No | Backend protocol (`"http"` or `"https"`); defaults to `"http"` |
|
|
| `cert` | `string` | No | Certificate type |
|
|
| `extra_headers` | `object` | No | Extra proxy headers |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `domain` | `string` | Domain name |
|
|
|
|
Returns HTTP `400` if the domain is already configured.
|
|
|
|
---
|
|
|
|
#### Update Domain
|
|
|
|
```
|
|
PUT /api/proxy/domains/<domain>
|
|
```
|
|
|
|
Update one or more fields of an existing domain entry. Only fields present in the body are modified. Supports both domain-level keys (`paths`, `force_ssl`, `cert`, `auth`) and path-level shorthand (`backend`, `headers` for the root path).
|
|
|
|
**Request Body:** Any subset of (`paths`, `backend`, `backend_host`, `backend_port`, `backend_proto`, `cert`, `extra_headers`, `force_ssl`, `auth`).
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `domain` | `string` | Domain name |
|
|
|
|
Returns HTTP `404` if the domain is not configured.
|
|
|
|
---
|
|
|
|
#### Remove Domain
|
|
|
|
```
|
|
DELETE /api/proxy/domains/<domain>
|
|
```
|
|
|
|
Remove a proxy domain and its nginx configuration.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `domain` | `string` | Domain name |
|
|
|
|
Returns HTTP `404` if the domain is not configured.
|
|
|
|
### Apply / Test
|
|
|
|
#### Apply Configuration
|
|
|
|
```
|
|
POST /api/proxy/apply
|
|
```
|
|
|
|
Regenerate nginx configuration files for all proxy domains and reload the nginx service.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `500` if nginx config generation fails or the reload fails.
|
|
|
|
---
|
|
|
|
#### Test Configuration
|
|
|
|
```
|
|
POST /api/proxy/test
|
|
```
|
|
|
|
Run `nginx -t` against the generated configuration without reloading.
|
|
|
|
**Response (valid):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data.valid` | `boolean` | Always `true` |
|
|
| `data.output` | `string` | Raw nginx test output |
|
|
|
|
**Error (invalid):** HTTP `400` with standard `{"ok": false, "error": "<nginx output>"}` response.
|
|
|
|
### Management Proxy
|
|
|
|
>The legacy `POST /api/proxy/management` endpoint has been removed. The management WebUI proxy is now configured as a regular domain entry with `is_management: true` on the root path and `is_websocket: true` on the `/ws` path. Use the standard domain add/update endpoints to configure it.
|
|
|
|
---
|
|
|
|
## Certificate API
|
|
|
|
Endpoints prefixed with `/api/certs/...`. Manage TLS certificates via ACME (ZeroSSL, Let's Encrypt, etc.).
|
|
|
|
### Listing & Details
|
|
|
|
#### List All Certificates
|
|
|
|
```
|
|
GET /api/certs/list
|
|
```
|
|
|
|
Return all managed certificates with metadata.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[object, ...]` | Array of certificate objects |
|
|
|
|
Each certificate object contains `domain`, `expires_at`, `days_until_expiry`, `cert_path`, `key_path`.
|
|
|
|
---
|
|
|
|
#### Get Certificate Details
|
|
|
|
```
|
|
GET /api/certs/<domain>
|
|
```
|
|
|
|
Return details for a single certificate.
|
|
|
|
**Response (`data`):** Fields: `domain`, `expires_at`, `days_until_expiry`, `cert_path`, `key_path`.
|
|
|
|
Returns HTTP `404` if no certificate is found for the domain.
|
|
|
|
### Validation
|
|
|
|
#### Validate Certificate Issuance
|
|
|
|
```
|
|
POST /api/certs/validate
|
|
```
|
|
|
|
Run pre-flight checks before certificate issuance. Verifies domain format, ACME account registration, DNS resolution, and port 80 accessibility.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `domain` | `string` | Yes | Domain to validate |
|
|
|
|
**Response (`data`):** Validation results object with per-check status.
|
|
|
|
Returns HTTP `400` if the domain is missing.
|
|
|
|
### Operations
|
|
|
|
#### Start Certificate Issuance
|
|
|
|
```
|
|
POST /api/certs/issue/start
|
|
```
|
|
|
|
Create a new certificate issuance request. Issuance runs asynchronously in the background.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `domain` | `string` | Yes | Domain to issue the certificate for |
|
|
| `email` | `string` | No | ACME contact email — **deprecated**, ignored in favor of the registered account email |
|
|
| `webroot` | `string` | No | Custom webroot path for HTTP-01 validation |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `request_id` | `string` | Unique identifier for polling issuance status |
|
|
|
|
Returns HTTP `400` if the domain is missing. Returns HTTP `409` if a valid certificate already exists for the domain (renew instead). An ACME account must be registered before issuance (verified by the `account_registered` blocking check in the validation pipeline).
|
|
|
|
---
|
|
|
|
#### Poll Certificate Issuance Status
|
|
|
|
```
|
|
GET /api/certs/issue/<request_id>
|
|
```
|
|
|
|
Poll the status of a certificate issuance request started by `POST /api/certs/issue/start`.
|
|
|
|
**Response (`data`):** Issuance status object containing progress, logs, and result.
|
|
|
|
Returns HTTP `404` if the request ID is not found. The frontend uses `poll()` to repeatedly fetch this endpoint until issuance completes or fails.
|
|
|
|
---
|
|
|
|
#### Renew Certificate
|
|
|
|
```
|
|
POST /api/certs/<domain>/renew
|
|
```
|
|
|
|
Force-renew an existing certificate.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if the certificate is not found. Returns HTTP `500` if renewal fails.
|
|
|
|
---
|
|
|
|
#### Remove Certificate
|
|
|
|
```
|
|
DELETE /api/certs/<domain>
|
|
```
|
|
|
|
Delete a certificate and remove it from auto-renewal tracking.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
Returns HTTP `404` if the certificate is not found.
|
|
|
|
### Account
|
|
|
|
#### Get ACME Account Status
|
|
|
|
```
|
|
GET /api/certs/account
|
|
```
|
|
|
|
Return the ACME account registration status.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `registered` | `boolean` | Whether an ACME account is registered |
|
|
| `email` | `string` | Registered contact email (empty if unregistered) |
|
|
| `ca` | `string` | CA provider (e.g., `"let's encrypt"`, `"ZeroSSL"`) (empty if unregistered) |
|
|
|
|
Returns HTTP `500` if the account status cannot be determined.
|
|
|
|
---
|
|
|
|
#### Register ACME Account
|
|
|
|
```
|
|
POST /api/certs/account/register
|
|
```
|
|
|
|
Register a new ACME account with the specified email and CA provider.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `email` | `string` | Yes | Contact email address |
|
|
| `server` | `string` | No | CA provider: `"letsencrypt"` or `"zerossl"`. Default: `"letsencrypt"` |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `registered` | `boolean` | Always `true` on success |
|
|
| `email` | `string` | Registered contact email |
|
|
| `ca` | `string` | CA provider |
|
|
|
|
Returns HTTP `400` if the email is missing or invalid. Returns HTTP `500` if registration fails. An ACME account must be registered before certificates can be issued.
|
|
|
|
---
|
|
|
|
#### Deactivate ACME Account
|
|
|
|
```
|
|
DELETE /api/certs/account
|
|
```
|
|
|
|
Deactivate the ACME account via `acme.sh --deactivate-account`. Clears the `email` and `ca` fields from `config/acme/config.json`.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `email` | `string` | Empty string indicating the account was deactivated |
|
|
|
|
Returns HTTP `500` if deactivation fails.
|
|
|
|
---
|
|
|
|
#### Set ACME Contact Email
|
|
|
|
```
|
|
POST /api/certs/email
|
|
```
|
|
|
|
Set or update the ACME account contact email.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `email` | `string` | Yes | Contact email address |
|
|
|
|
**Response (`data`):** Returns the set `email` field.
|
|
|
|
#### Generate Self-Signed Certificate
|
|
|
|
```
|
|
POST /api/certs/self-signed
|
|
```
|
|
|
|
Generate a self-signed certificate for a domain. Idempotent — skips if `fullchain.cer` and `<domain>.key` already exist at `data/acme/<domain>/`.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `domain` | `string` | Yes | Domain name for the certificate CN |
|
|
| `days` | `number` | No | Validity in days; defaults to `365` |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `domain` | `string` | Domain name |
|
|
| `cert` | `string` | Path to `fullchain.cer` |
|
|
| `key` | `string` | Path to `<domain>.key` |
|
|
| `generated` | `boolean` | `true` if a new cert was created, `false` if existing cert was reused |
|
|
|
|
## WireGuard API
|
|
|
|
Endpoints prefixed with `/api/wireguard/...`. Manage the WireGuard VPN server, peers, and client configuration.
|
|
|
|
### Configuration
|
|
|
|
#### Get Configuration
|
|
|
|
```
|
|
GET /api/wireguard/config
|
|
```
|
|
|
|
Return the current WireGuard server configuration. The `private_key` field is stripped.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `object` | WireGuard config (`private_key` omitted) |
|
|
|
|
---
|
|
|
|
#### Replace Configuration
|
|
|
|
```
|
|
POST /api/wireguard/config
|
|
```
|
|
|
|
Replace the entire WireGuard configuration. The `private_key` field is stripped from the response.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| *(entire body)* | `object` | Yes | Complete WireGuard configuration object |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Partial Update Configuration
|
|
|
|
```
|
|
PATCH /api/wireguard/config
|
|
```
|
|
|
|
Deep-merge the provided fields into the existing configuration.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| *(any subset)* | `any` | Yes | Fields to merge into the existing config |
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
### Tunnel Control
|
|
|
|
#### Apply Configuration
|
|
|
|
```
|
|
POST /api/wireguard/apply
|
|
```
|
|
|
|
Write the current configuration to `wg0.conf` and bring the tunnel up.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Start Tunnel
|
|
|
|
```
|
|
POST /api/wireguard/up
|
|
```
|
|
|
|
Alias for `/api/wireguard/apply` — write config and bring the tunnel up.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
---
|
|
|
|
#### Bring Tunnel Down
|
|
|
|
```
|
|
POST /api/wireguard/down
|
|
```
|
|
|
|
Bring down the WireGuard tunnel interface (`wg0`).
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `down` | `boolean` | Always `true` on success |
|
|
| `synced` | `[string, ...]` | Subsystems auto-synced as a result |
|
|
|
|
### Status
|
|
|
|
#### Tunnel Status
|
|
|
|
```
|
|
GET /api/wireguard/status
|
|
```
|
|
|
|
Return live tunnel state with interface metrics and per-peer connection statistics.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `up` | `boolean` | Whether the tunnel interface is up |
|
|
| `interface` | `object` | Interface info (listen port, public key) |
|
|
| `peers` | `[object, ...]` | Per-peer stats (handshake, bytes, endpoint) |
|
|
|
|
---
|
|
|
|
#### Initialize
|
|
|
|
```
|
|
POST /api/wireguard/initialize
|
|
```
|
|
|
|
First-time setup: generate server key pair, write initial config. Idempotent.
|
|
|
|
**Response:** `data` is `null` on success.
|
|
|
|
### Peer Management
|
|
|
|
#### List Peers
|
|
|
|
```
|
|
GET /api/wireguard/peers
|
|
```
|
|
|
|
Return all configured peers. Private keys are stripped.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[object, ...]` | Peer objects (private keys omitted) |
|
|
|
|
---
|
|
|
|
#### Add Peer
|
|
|
|
```
|
|
POST /api/wireguard/peers
|
|
```
|
|
|
|
Add a new WireGuard peer. A key pair is auto-generated. Private key stripped from response.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `name` | `string` | Yes | Peer identifier name |
|
|
| `endpoint` | `string` | No | Allowed endpoint address (`"ip:port"`) |
|
|
| `allowed_ips` | `[string, ...]` | No | Allowed IPs; defaults to `["0.0.0.0/0"]` |
|
|
| `persistent_keepalive` | `number` | No | Persistent keepalive interval (seconds) |
|
|
| `preshared_key` | `string` | No | Preshared key |
|
|
|
|
**Response (`data`):** Peer object with `name`, `public_key`, `allowed_ips`, etc. (no `private_key`).
|
|
|
|
---
|
|
|
|
#### Remove Peer
|
|
|
|
```
|
|
DELETE /api/wireguard/peers/<name>
|
|
```
|
|
|
|
Remove a configured peer by name.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `string` | Peer name |
|
|
|
|
Returns HTTP `404` if the peer is not found.
|
|
|
|
---
|
|
|
|
#### Peer Connection Status
|
|
|
|
```
|
|
GET /api/wireguard/peer-status
|
|
```
|
|
|
|
Return live per-peer connection status from `wg show`.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `[object, ...]` | Live peer status (handshake time, bytes, endpoint) |
|
|
|
|
### Client Configuration
|
|
|
|
#### Generate Client Config
|
|
|
|
```
|
|
POST /api/wireguard/generate-client
|
|
```
|
|
|
|
Generate a complete WireGuard client configuration file. The returned config includes the peer's private key for provisioning.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `name` | `string` | Yes | Peer name to generate config for |
|
|
| `server_endpoint` | `string` | Yes | Server public address (`"ip:port"`) |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `config` | `string` | Complete client config text (`[Interface]` + `[Peer]`) |
|
|
|
|
This is the only endpoint that returns a WireGuard private key. All other endpoints strip private keys from responses.
|
|
|
|
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
|
|
|
|
Endpoints prefixed with `/api/network/...`. Manage systemd-networkd interface configuration including static addresses, routes, DNS, DHCP client settings, and link parameters.
|
|
|
|
### Interface Management
|
|
|
|
#### List All Interfaces
|
|
|
|
```
|
|
GET /api/network/interfaces
|
|
```
|
|
|
|
Return all configured interfaces with their network config and runtime state from `networkctl`.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data.interfaces` | `object` | Map of interface name to `{config, runtime}` |
|
|
| `data.timestamp` | `string` | Timestamp of runtime data collection |
|
|
|
|
---
|
|
|
|
#### Get Interface Details
|
|
|
|
```
|
|
GET /api/network/interfaces/<name>
|
|
```
|
|
|
|
Return config and runtime state for a specific interface.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `string` | Interface name |
|
|
| `config` | `object` | Full networkd config entry for this interface |
|
|
| `runtime` | `object` | Runtime state from `networkctl` (addresses, gateway, DNS, state) |
|
|
|
|
Returns HTTP `400` if the interface name is invalid (contains path components, spaces, or characters outside `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`). Returns HTTP `404` if the interface is not found in config.
|
|
|
|
---
|
|
|
|
#### Save and Apply Interface
|
|
|
|
```
|
|
POST /api/network/interfaces/<name>
|
|
```
|
|
|
|
Save network config for an interface, render the `.network` file, copy it to `/etc/systemd/network/`, and reload networkd for that interface.
|
|
|
|
**Request Body:** Any networkd config keys (e.g., `addresses`, `gateway`, `dns`, `routes`, `dhcp`, `link`, `dhcp_client`).
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `string` | Interface name |
|
|
| `applied` | `boolean` | `true` if deploy to systemd-networkd succeeded, `false` if the system call was unavailable |
|
|
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
|
|
|
|
Returns HTTP `400` if the interface name is invalid.
|
|
|
|
---
|
|
|
|
#### Reload Interface
|
|
|
|
```
|
|
POST /api/network/interfaces/<name>/reload
|
|
```
|
|
|
|
Reload networkd for a single interface (runs `networkctl reload <name>`).
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `string` | Interface name |
|
|
| `reloaded` | `boolean` | Always `true` on success |
|
|
|
|
Returns HTTP `400` if the interface name is invalid.
|
|
|
|
### Full Sync
|
|
|
|
#### Apply All Interfaces
|
|
|
|
```
|
|
POST /api/network/apply
|
|
```
|
|
|
|
Full sync: generate all `.network` files, remove stale files, copy to `/etc/systemd/network/`, reload all interfaces, and sync DNS upstreams to dnsmasq.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `applied` | `number` | Number of interfaces applied |
|
|
| `files` | `[string, ...]` | Paths of generated files |
|
|
| `cleaned` | `[string, ...]` | Paths of removed stale files |
|
|
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
|
|
|
|
### Helpers
|
|
|
|
#### Infer DHCP Ranges
|
|
|
|
```
|
|
GET /api/network/infer-dhcp-ranges
|
|
```
|
|
|
|
Suggest candidate DHCP ranges based on static interface IPs. For each interface with a static IPv4 address, calculates a usable address range in the subnet.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data.ranges` | `object` | Map of interface name to `{subnet, prefix, start, end}` |
|
|
|
|
---
|
|
|
|
#### Infer Firewall Zones
|
|
|
|
```
|
|
GET /api/network/infer-zones
|
|
```
|
|
|
|
Suggest firewalld zone assignments for configured interfaces based on heuristics:
|
|
- Interface name contains `wg` → `wan`
|
|
- DHCP-enabled or public-facing IP → `wan`
|
|
- Has explicit routes → `management`
|
|
- Everything else → `lan`
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data.zones` | `object` | Map of interface name to suggested zone (`"lan"`, `"wan"`, `"management"`) |
|
|
|
|
Returns HTTP `500` if the value cannot be verified after write.
|
|
|
|
---
|
|
|
|
## Status API
|
|
|
|
Endpoints prefixed with `/api/status/...`. Aggregate status across all subsystems.
|
|
|
|
### Pending Changes
|
|
|
|
#### Check All Pending Changes
|
|
|
|
```
|
|
GET /api/status/pending
|
|
```
|
|
|
|
Aggregate pending changes across all subsystems. Useful for the dashboard to show which subsystems need configuration applied.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `subsystems` | `object` | Map of subsystem name to pending status |
|
|
| `total_changes` | `number` | Total count of pending changes across all subsystems |
|
|
|
|
---
|
|
|
|
#### Apply All Pending Changes
|
|
|
|
```
|
|
POST /api/status/apply-all
|
|
```
|
|
|
|
Apply pending changes for all subsystems in dependency order.
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `applied` | `[string, ...]` | List of subsystems that were applied |
|
|
| `errors` | `[object, ...]` | Any errors encountered during apply |
|
|
|
|
### Sysctl
|
|
|
|
#### Set Kernel Parameter
|
|
|
|
```
|
|
POST /api/network/sysctl/set
|
|
```
|
|
|
|
Set a sysctl kernel parameter value via `sysctl -w`, then verify by reading it back.
|
|
|
|
**Request Body:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| `name` | `string` | Yes | Kernel parameter name (e.g., `"net.ipv4.ip_forward"`) |
|
|
| `value` | `string` | Yes | Value to set |
|
|
|
|
**Response (`data`):**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `string` | Parameter name |
|
|
| `value` | `string` | Value set |
|
|
|
|
Returns HTTP `500` if the value cannot be verified after write.
|
|
|
|
---
|
|
|
|
## Logs API
|
|
|
|
Endpoints prefixed with `/api/logs/...`. Return log lines as JSON strings, wrapped in the standard `{"ok": true, "data": ...}` response contract.
|
|
|
|
### System Journal
|
|
|
|
#### Get Journal Entries
|
|
|
|
```
|
|
GET /api/logs/journal
|
|
```
|
|
|
|
Return recent system journal entries.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `string` | Raw journal log text |
|
|
|
|
### Nginx Logs
|
|
|
|
#### Nginx Access Log
|
|
|
|
```
|
|
GET /api/logs/nginx/access
|
|
```
|
|
|
|
Return recent nginx access log entries.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `string` | Raw access log text |
|
|
|
|
Returns HTTP `404` if the log file does not exist.
|
|
|
|
---
|
|
|
|
#### Nginx Error Log
|
|
|
|
```
|
|
GET /api/logs/nginx/error
|
|
```
|
|
|
|
Return recent nginx error log entries.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `string` | Raw error log text |
|
|
|
|
Returns HTTP `404` if the log file does not exist.
|
|
|
|
### Dnsmasq Log
|
|
|
|
#### Dnsmasq Entries
|
|
|
|
```
|
|
GET /api/logs/dnsmasq
|
|
```
|
|
|
|
Return recent dnsmasq journal entries.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `string` | Raw dnsmasq journal text |
|
|
|
|
### Application Log
|
|
|
|
#### App Log Entries
|
|
|
|
```
|
|
GET /api/logs/app
|
|
```
|
|
|
|
Return recent application log entries.
|
|
|
|
**Response:**
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `data` | `string` | Raw application log text |
|
|
|
|
Returns HTTP `404` if the log file does not exist.
|
|
|
|
---
|
|
|
|
## WebSocket Protocol
|
|
|
|
The daemon exposes a WebSocket at `/ws` (port 9091) for real-time state change notifications. On connect, the server sends:
|
|
|
|
```json
|
|
{"type": "init", "versions": {"firewall": 0, "dnsmasq": 0, ...}}
|
|
```
|
|
|
|
### Message Types
|
|
|
|
- **`versions`** — Structural state change. `updated` contains subsystem names whose version counters changed. Triggers full re-fetch.
|
|
- **`tick`** — Volatile-only change (stats, counters, DHCP IPs). `subsystems` contains affected subsystem names. Triggers lightweight per-subsystem re-fetch.
|
|
- **`notify`** — Single-topic notification. `topic` is the subsystem name. |