# 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 and HTTP basic authentication. Requests target the management domain (e.g., `https://wall.lan/api/...`). Every request and response uses `Content-Type: application/json`. ## Conventions ### Success Responses ```json { "ok": true, "data": } ``` The `data` field contains the payload, which may be an object, array, string, or `null`. ### Error Responses ```json { "ok": false, "error": "" } ``` 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 | | `500` | Internal server error — unexpected failure in the backend | --- ## Firewall API Endpoints prefixed with `/api/firewall/...`. Interact with firewalld for zone management, rich rules, NAT, and masquerade. ### 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` | Currently assigned interfaces per zone | | `data.available` | `[string, ...]` | All zones known to firewalld | --- #### Get Zone Details ``` GET /api/firewall/zones/ ``` 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: number, proto: string, toaddr: string, toport: number}, ...]` | Port forward rules | | `rich_rules` | `[string, ...]` | Rich rule definitions | 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. --- #### Delete Zone ``` DELETE /api/firewall/zones/ ``` 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//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 to the zone | --- #### Set Zone Services ``` POST /api/firewall/zones//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 in the zone | ### Firewall Rules #### Add Rich Rule ``` POST /api/firewall/rich-rules ``` Add a firewalld rich rule to a zone. **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 | | `rule` | `string` | Full rich rule string | --- #### Remove Rich Rule ``` DELETE /api/firewall/rich-rules ``` Remove an existing rich rule from a zone. The `rule` string must match exactly. **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `zone` | `string` | Yes | Zone the rule belongs to | | `rule` | `string` | Yes | Exact rich rule string to remove | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `zone` | `string` | Zone name | | `rule` | `string` | Exact rich rule string that was removed | --- #### List Rich Rules ``` GET /api/firewall/rich-rules/ ``` Return all rich rules for the specified zone. **Response:** | Field | Type | Description | |-------|------|-------------| | `data` | `[string, ...]` | Rich rule strings | ### 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 for the zone | --- #### Add Port Forward ``` POST /api/firewall/forward-port ``` Add a port forwarding rule to a zone. **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 | | `port` | `number` | External port | | `proto` | `string` | Protocol (`"tcp"` or `"udp"`) | --- #### Remove Port Forward ``` DELETE /api/firewall/forward-port ``` Remove a port forwarding rule. The body must match the original rule exactly. **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `zone` | `string` | Yes | Zone the rule belongs 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 | | `port` | `number` | External port | | `proto` | `string` | Protocol (`"tcp"` or `"udp"`) | ### 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. ### 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 | --- #### 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 for the reservation | --- #### Remove Static Lease ``` DELETE /api/dhcp/static-lease?mac=aa:bb:cc:dd:ee:ff ``` Remove a previously configured static lease. **Query Parameters:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `mac` | `string` | Yes | MAC address of the lease to remove | **Response:** `data` is `null` on success. Returns HTTP `404` if no matching lease is found. ### 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 | **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=nas.lan ``` Remove a custom DNS record. **Query Parameters:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Fully qualified domain name to remove | **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. ### Domain Management #### List All Domains ``` GET /api/proxy/domains ``` Return all configured proxy domains. **Response:** | Field | Type | Description | |-------|------|-------------| | `data` | `[object, ...]` | Array of domain configuration objects | --- #### Add Domain ``` POST /api/proxy/domains ``` Add a new reverse proxy domain. **Request Body:** | 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"` | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `domain` | `string` | Domain name | Returns HTTP `400` if the domain is already configured. --- #### Get Domain Details ``` GET /api/proxy/domains/ ``` Return the configuration for a single proxy domain. **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `domain` | `string` | Domain name | | `backend_host` | `string` | Backend server address | | `backend_port` | `number` | Backend server port | | `backend_proto` | `string` | Backend protocol | Returns HTTP `404` if the domain is not configured. --- #### Update Domain ``` PUT /api/proxy/domains/ ``` Update one or more fields of an existing domain entry. Only the fields present in the body are modified. **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `backend_host` | `string` | No | Backend server IP or hostname | | `backend_port` | `number` | No | Backend server port | | `backend_proto` | `string` | No | Backend protocol | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `domain` | `string` | Domain name | Returns HTTP `404` if the domain is not configured. --- #### Remove Domain ``` DELETE /api/proxy/domains/ ``` 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. Useful for validating changes before applying. **Response:** | Field | Type | Description | |-------|------|-------------| | `data.valid` | `boolean` | Whether the configuration syntax is valid | | `data.output` | `string` | Raw nginx test output | ### Management #### Configure Management WebUI Proxy ``` POST /api/proxy/management ``` Configure the nginx proxy block for the management WebUI itself, including optional HTTP basic authentication. **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `domain` | `string` | Yes | Management domain (e.g., `"wall.lan"`) | | `flask_host` | `string` | No | Flask app bind host; defaults to `"127.0.0.1"` | | `flask_port` | `number` | No | Flask app bind port; defaults to `9090` | | `auth_user` | `string` | No | Username for basic auth. An `.htpasswd` entry is created when this field is present. | | `auth_pass` | `string` | No | Password for basic auth. Used together with `auth_user`. | **Response:** `data` is `null` on success. If `auth_user` and `auth_pass` are provided, the endpoint creates or updates the corresponding `.htpasswd` file entry. --- ## Certificate API Endpoints prefixed with `/api/certs/...`. Manage TLS certificates via ACME (Let's Encrypt / certbot). ### 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: | Field | Type | Description | |-------|------|-------------| | `domain` | `string` | Domain the certificate covers | | `expires_at` | `string` | Expiration date (ISO 8601) | | `days_until_expiry` | `number` | Remaining days until expiration | | `cert_path` | `string` | Path to the certificate file | | `key_path` | `string` | Path to the private key file | --- #### Get Certificate Details ``` GET /api/certs/ ``` Return details for a single certificate. **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `domain` | `string` | Domain | | `expires_at` | `string` | Expiration date (ISO 8601) | | `days_until_expiry` | `number` | Remaining days | | `cert_path` | `string` | Certificate file path | | `key_path` | `string` | Private key file path | Returns HTTP `404` if no certificate is found for the domain. ### Operations #### Issue Certificate ``` POST /api/certs/issue ``` Request a new certificate for a domain. **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `domain` | `string` | Yes | Domain to issue the certificate for | | `webroot` | `string` | No | Custom webroot path for HTTP-01 validation | **Response:** `data` is `null` on success. Returns HTTP `400` if the domain is missing or the request is malformed. Returns HTTP `500` if the ACME challenge or certificate issuance fails. --- #### Renew Certificate ``` POST /api/certs//renew ``` Force-renew an existing certificate, regardless of its current expiry status. **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/ ``` 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 #### Set ACME Contact Email ``` POST /api/certs/email ``` Set or update the ACME account contact email (used by Let's Encrypt for expiration and security notices). **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | `string` | Yes | Contact email address | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `email` | `string` | Contact email address | --- ## 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 from the response. **Response:** | Field | Type | Description | |-------|------|-------------| | `data` | `object` | Full WireGuard configuration dictionary (`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:** | Field | Type | Description | |-------|------|-------------| | `data` | `object` | Updated configuration (`private_key` omitted) | ### Tunnel Control #### Apply Configuration ``` POST /api/wireguard/apply ``` Write the current configuration to `wg0.conf` on disk and bring the WireGuard tunnel up. **Response:** `data` is `null` on success. Returns HTTP `500` if config write or interface bring-up fails. --- #### Bring Tunnel Down ``` POST /api/wireguard/down ``` Bring down the WireGuard tunnel interface (`wg0`). **Response:** `data` is `null` on success. ### Status #### Tunnel Status ``` GET /api/wireguard/status ``` Return live tunnel state, including 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, etc.) | | `peers` | `[object, ...]` | Per-peer connection stats (handshake time, transfer bytes, endpoint, etc.) | --- #### Initialize ``` POST /api/wireguard/initialize ``` Perform first-time setup: generate a server key pair, write an initial configuration, and prepare for peer enrollment. This endpoint is idempotent — calling it multiple times has no additional effect. **Response:** `data` is `null` on success. ### Peer Management #### Add Peer ``` POST /api/wireguard/add-peer ``` Add a new WireGuard peer. A key pair is auto-generated for the peer. The response includes peer details with the private key stripped. **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 in seconds | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `name` | `string` | Peer name | | `public_key` | `string` | Peer's public key | | `allowed_ips` | `[string, ...]` | Allowed IPs | | `endpoint` | `string` | Allowed endpoint | | `persistent_keepalive` | `number` | Keepalive interval | --- #### Remove Peer ``` DELETE /api/wireguard/remove-peer?name=alice ``` Remove a configured peer. **Query Parameters:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Peer name to remove | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `name` | `string` | Peer name | Returns HTTP `404` if the peer is not found. --- #### List Peers ``` GET /api/wireguard/peers ``` Return all configured peers. Private keys are stripped from the response. **Response:** | Field | Type | Description | |-------|------|-------------| | `data` | `[object, ...]` | Array of peer objects (private keys omitted) | --- #### Peer Connection Status ``` GET /api/wireguard/peer-status ``` Return live per-peer connection status from `wg show`, including last handshake time, transfer bytes, and current endpoint. **Response:** | Field | Type | Description | |-------|------|-------------| | `data` | `[object, ...]` | Array of live peer status objects | ### Client Configuration #### Generate Client Config ``` POST /api/wireguard/generate-client ``` Generate a complete WireGuard client configuration file for provisioning a device. The returned config includes the peer's private key for the client to use. **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Peer name to generate config for | | `server_endpoint` | `string` | Yes | Server public address (`"ip:port"`) for the client's `[Peer]` section | **Response (`data`):** | Field | Type | Description | |-------|------|-------------| | `config` | `string` | Complete WireGuard client config text (`[Interface]` + `[Peer]` block) | The client config includes the generated private key so the client can be provisioned directly. Note that 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.