docs: update all documentation and AGENTS.md

This commit is contained in:
2026-06-16 03:35:22 +00:00
parent bc72db903c
commit b8c2fa2f24
7 changed files with 73 additions and 17 deletions
+1 -8
View File
@@ -36,13 +36,6 @@ vacuum-walld ──→ daemon/handlers/*.py ──→ sudo <cmd> ──→ syste
Project uses `.venv`. Install deps with `pip install -e .` (from `pyproject.toml`). `__init__.py` files in `webui/` and `lib/` are intentionally empty — no `sys.path` boilerplate needed. Project uses `.venv`. Install deps with `pip install -e .` (from `pyproject.toml`). `__init__.py` files in `webui/` and `lib/` are intentionally empty — no `sys.path` boilerplate needed.
**No CDN packages.** All frontend libraries (JS and CSS) must be vendored in `webui/static/`. Never reference `unpkg.com`, `cdn.jsdelivr.net`, or similar. To add/update a library, edit the version in `scripts/update-vendor.sh` and run it.
| Library | Version | Vendor file | Symlink (active) | CDN source |
| ------- | ------- | ------------------------------------|--------------------------------| ---------- |
| htmx | 2.0.4 | `vendor/htmx-2.0.4.min.js` | `webui/static/htmx.min.js` | `npm:htmx.org@2.0.4` |
| htmx-ext-json-enc | 2.0.0 | `vendor/json-enc-2.0.0.js` | `webui/static/json-enc.js` | `npm:htmx-ext-json-enc@2.0.0` |
## Deployment ## Deployment
`install.sh` installs only system components and configures them; the project serves from the repo root by default. All options can be set via env vars or CLI flags (CLI takes precedence). Set `INSTALL_DIR` or `--path` to override install directory. Use `--dev` to auto-detect repo owner as service user (non-dev mode requires `--user`). `install.sh` installs only system components and configures them; the project serves from the repo root by default. All options can be set via env vars or CLI flags (CLI takes precedence). Set `INSTALL_DIR` or `--path` to override install directory. Use `--dev` to auto-detect repo owner as service user (non-dev mode requires `--user`).
@@ -109,7 +102,7 @@ Install dev tooling with `pip install -e ".[dev]"`.
## Docs ## Docs
`docs/` contains the authoritative reference for each subsystem. `docs/` contains the authoritative reference for each subsystem. **Before reasoning about any subsystem**, read the relevant doc(s) below to ground your understanding in the project's documented behavior rather than inference from code alone.
| Doc | Contents | | Doc | Contents |
|-----|----------| |-----|----------|
-1
View File
@@ -8,7 +8,6 @@ A zone-based firewall appliance with a built-in SSL reverse proxy. Combines fire
- Python 3.13+, Flask 3.x web UI - Python 3.13+, Flask 3.x web UI
- firewalld (nftables backend), dnsmasq, nginx, WireGuard - firewalld (nftables backend), dnsmasq, nginx, WireGuard
- acme.sh for ACME certificates (ZeroSSL) - acme.sh for ACME certificates (ZeroSSL)
- HTMX + Jinja2 templates
--- ---
+55 -3
View File
@@ -965,7 +965,29 @@ Set or update the ACME account contact email.
**Response (`data`):** Returns the set `email` field. **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 ## WireGuard API
@@ -1229,7 +1251,7 @@ Return config and runtime state for a specific interface.
| `config` | `object` | Full networkd config entry for this interface | | `config` | `object` | Full networkd config entry for this interface |
| `runtime` | `object` | Runtime state from `networkctl` (addresses, gateway, DNS, state) | | `runtime` | `object` | Runtime state from `networkctl` (addresses, gateway, DNS, state) |
Returns HTTP `404` if the interface is not found in config. 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.
--- ---
@@ -1250,6 +1272,8 @@ Save network config for an interface, render the `.network` file, copy it to `/e
| `name` | `string` | Interface name | | `name` | `string` | Interface name |
| `applied` | `boolean` | Always `true` on success | | `applied` | `boolean` | Always `true` on success |
Returns HTTP `400` if the interface name is invalid.
--- ---
#### Reload Interface #### Reload Interface
@@ -1267,6 +1291,8 @@ Reload networkd for a single interface (runs `networkctl reload <name>`).
| `name` | `string` | Interface name | | `name` | `string` | Interface name |
| `reloaded` | `boolean` | Always `true` on success | | `reloaded` | `boolean` | Always `true` on success |
Returns HTTP `400` if the interface name is invalid.
### Full Sync ### Full Sync
#### Apply All Interfaces #### Apply All Interfaces
@@ -1321,11 +1347,37 @@ Suggest firewalld zone assignments for configured interfaces based on heuristics
|-------|------|-------------| |-------|------|-------------|
| `data.zones` | `object` | Map of interface name to suggested zone (`"lan"`, `"wan"`, `"management"`) | | `data.zones` | `object` | Map of interface name to suggested zone (`"lan"`, `"wan"`, `"management"`) |
### Sysctl
#### Set Kernel Parameter
```
POST /api/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 ## Logs API
Endpoints prefixed with `/api/logs/...`. Serve rendered HTML log line fragments for HTMX consumption. These endpoints **do not** follow the standard JSON `{"ok": true, "data": ...}` response contract — they return HTML `<div>` elements directly. Errors are rendered inline as `(error reading ...)` text rather than returning JSON error responses. Endpoints prefixed with `/api/logs/...`. These endpoints **do not** follow the standard JSON `{"ok": true, "data": ...}` response contract — they return HTML `<div>` elements directly. Errors are rendered inline as `(error reading ...)` text rather than returning JSON error responses.
### System Journal ### System Journal
+1 -1
View File
@@ -34,7 +34,7 @@ The following diagram summarizes how the Flask WebUI communicates with each mana
``` ```
External Client ──→ nginx (SSL termination) ──→ Flask WebUI (127.0.0.1:9090) External Client ──→ nginx (SSL termination) ──→ Flask WebUI (127.0.0.1:9090)
Flask WebUI ──→ daemon/client.py (Unix socket) ──→ vacuum-walld (aiohttp server) Flask WebUI ──→ daemon/client.py (path resolution, Unix socket) ──→ vacuum-walld (aiohttp server)
vacuum-walld ──→ daemon/handlers/firewall.py ──→ sudo firewall-cmd ──→ firewalld / D-Bus ──→ nftables vacuum-walld ──→ daemon/handlers/firewall.py ──→ sudo firewall-cmd ──→ firewalld / D-Bus ──→ nftables
vacuum-walld ──→ daemon/handlers/nginx.py ──→ write local .conf files ──→ sudo cp to /etc/nginx/ ──→ sudo nginx -t && sudo nginx -s reload vacuum-walld ──→ daemon/handlers/nginx.py ──→ write local .conf files ──→ sudo cp to /etc/nginx/ ──→ sudo nginx -t && sudo nginx -s reload
vacuum-walld ──→ daemon/handlers/dnsmasq.py ──→ render config ──→ sudo tee /etc/dnsmasq.d/vacuum-wall.conf ──→ sudo systemctl reload dnsmasq vacuum-walld ──→ daemon/handlers/dnsmasq.py ──→ render config ──→ sudo tee /etc/dnsmasq.d/vacuum-wall.conf ──→ sudo systemctl reload dnsmasq
+2
View File
@@ -373,6 +373,8 @@ Each key in the `interfaces` object is an interface name (e.g., `eth0`, `eth1`,
| `keep_master` | `boolean` | Keep master on stop. | | `keep_master` | `boolean` | Keep master on stop. |
| `ip_family` | `string` | IP family to use. | | `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 ### 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). 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).
+1 -4
View File
@@ -40,8 +40,6 @@ WireGuard support provides server-side VPN tunnel management. Peers are added th
- dnsmasq - dnsmasq
- WireGuard tools (wireguard-tools) - WireGuard tools (wireguard-tools)
- acme.sh for ACME certificate management (ZeroSSL by default) - acme.sh for ACME certificate management (ZeroSSL by default)
- HTMX for dynamic UI updates
- Jinja2 for server-side templating
## Quick Start ## Quick Start
@@ -112,8 +110,7 @@ After installation, access the management interface at `https://<hostname>.local
│ │ ├── wireguard.py # WireGuard API │ │ ├── wireguard.py # WireGuard API
│ │ ├── network.py # Networkd API │ │ ├── network.py # Networkd API
│ │ └── logs.py # Logs API │ │ └── logs.py # Logs API
── templates/ # Jinja2/HTMX templates ── static/ # SPA (index.html, app.js, reactive-dom.js, style.css)
│ └── static/ # CSS and client-side JS
├── docs/ # Documentation ├── docs/ # Documentation
│ ├── overview.md # This file │ ├── overview.md # This file
│ ├── deployment.md │ ├── deployment.md
+13
View File
@@ -53,6 +53,10 @@ Key safety properties:
- `NOPASSWD` is used so the application never prompts for a password. `Defaults:<user>` restricts the secure path and disables TTY requirement. - `NOPASSWD` is used so the application never prompts for a password. `Defaults:<user>` restricts the secure path and disables TTY requirement.
- The sudoers file is rendered from a Jinja2 template at install time, substituting the configured user name. - The sudoers file is rendered from a Jinja2 template at install time, substituting the configured user name.
## Daemon Client Path Resolution
The `daemon/client.py` module resolves `<param>` placeholders in URL paths before sending requests over the Unix socket. For example, a request to `/network/interfaces/<name>` with a body containing `{"name": "eth0"}` is rewritten to `/network/interfaces/eth0` before transmission. Parameter values are URL-encoded to handle special characters safely. This eliminates the need for the API layer to construct literal paths and ensures the daemon always receives concrete paths for routing.
## Web Security ## Web Security
### Management Interface ### Management Interface
@@ -135,6 +139,15 @@ The `lib/firewall` module is a generic firewalld parser with no hardcoded zone d
IP forwarding (`net.ipv4.ip_forward = 1`) is enabled system-wide to allow routing between zones (LAN to Internet, VPN to LAN). However, actual traffic flow is controlled by firewalld rules. Masquerade is enabled on the `internal` zone so that LAN clients get NAT translation when accessing the Internet through the Vacuum Wall router. IP forwarding (`net.ipv4.ip_forward = 1`) is enabled system-wide to allow routing between zones (LAN to Internet, VPN to LAN). However, actual traffic flow is controlled by firewalld rules. Masquerade is enabled on the `internal` zone so that LAN clients get NAT translation when accessing the Internet through the Vacuum Wall router.
## Input Validation
Interface names provided via the API are validated at two layers before any file system access or subprocess invocation:
- **API layer** (`webui/api/network.py`): The Flask route calls `validate_interface_name()` from `lib/common.py`, rejecting any name that doesn't match `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`. Names containing `/`, `..`, spaces, or other disallowed characters return HTTP `400`.
- **Daemon handler layer** (`daemon/handlers/network.py`): Each handler re-validates the name from the request body using the same function. An invalid name raises `ValueError`, which the daemon converts to an error response before any `sudo` call.
This defense-in-depth approach ensures that even if a request bypasses the API layer, the daemon will still reject malicious interface names.
## Certificate Security ## Certificate Security
### acme.sh Integration ### acme.sh Integration