feat: add networkd subsystem and fix code review issues

Phase 1-4: Networkd subsystem
- lib/network.py: systemd-networkd config renderer (.network INI files)
  with full schema support: [Match], [Link], [Network], [Address], [Route],
  [DHCPv4], [DHCPv6] sections. One Address/=DNS= line per value per spec.
  Route sections use #N suffix per systemd.syntax(7).
- lib/network.py: generate_network_files() with 50-<name>.network prefix
  and stale file cleanup
- lib/network.py: collect_upstream_dns() filters local/private DNS
- lib/network.py: infer_dhcp_ranges() and infer_zones() helpers
- daemon/handlers/network.py: routes for GET/POST /network/interfaces
  and full apply with DNS upstream sync to dnsmasq
- webui/api/network.py: Flask blueprint for /api/network/* endpoints
- webui/api: interfaces page updated with IP config inline editing
- lib/state.py: networkd collector using parse_networkctl_status()
- system/sudoers.d/vacuum-walld: networkctl + systemd-network rules
- system/systemd/vacuum-walld.service: ReadWritePaths for /etc/systemd/network
- install.sh: ACME email now optional, configured from WebUI
- lib/acme.py: get_email() falls back to declarative config

Phase 5: Code review fixes
- daemon/server.py: path params now win over JSON body and query params
  in request body merge (prevents config save name override)
- daemon/server.py: remove dead 'import re'
- daemon/handlers/network.py: replace Path.mkdir() with sudo mkdir
  for /etc/systemd/network (ProtectSystem=strict compatibility)
- system/sudoers.d/vacuum-walld: pin systemctl to specific commands
  (reload/is-active dnsmasq instead of wildcard)
- system/sudoers.d/vacuum-walld: restore !requiretty and section comment
- lib/network.py: remove unused _MANAGEMENT_PORTS constant
- webui/api/network.py: remove redundant body[\name\] = name in save_interface

Tests: 332 passing (110 new/updated), ruff clean
This commit is contained in:
2026-06-01 03:15:50 +00:00
parent 2f215793e9
commit bc72db903c
26 changed files with 3294 additions and 121 deletions
+6
View File
@@ -40,6 +40,7 @@ vacuum-walld ──→ daemon/handlers/nginx.py ──→ write local .conf file
vacuum-walld ──→ daemon/handlers/dnsmasq.py ──→ render config ──→ sudo tee /etc/dnsmasq.d/vacuum-wall.conf ──→ sudo systemctl reload dnsmasq
vacuum-walld ──→ daemon/handlers/acme.py ──→ acme.sh (subprocess) ──→ deploy hook (daemon API) ──→ ZeroSSL ACME
vacuum-walld ──→ daemon/handlers/wireguard.py ──→ render data/wireguard/wg0.conf ──→ sudo cp to /etc/wireguard/ ──→ sudo wg-quick up wg0
vacuum-walld ──→ daemon/handlers/network.py ──→ render 50-<name>.network ──→ sudo cp to /etc/systemd/network/ ──→ sudo networkctl reload
vacuum-walld ──→ daemon/handlers/logs.py ──→ sudo journalctl ──→ systemd journal
```
@@ -77,6 +78,7 @@ Vacuum Wall uses a declarative configuration model. Persistent user-facing confi
| dnsmasq | `config/dnsmasq/config.json` | `data/dnsmasq/fragments/` | `/etc/dnsmasq.d/vacuum-wall.conf` | The JSON file is the source of truth. The rendered `.conf` file is overwritten on each apply. |
| nginx | `config/nginx/config.json` | `data/nginx/.htpasswd`, `data/nginx/sites-enabled/` | `data/nginx/sites-enabled/<domain>.conf` + `/etc/nginx/conf.d/vacuum-wall.conf` | All proxy and management domain definitions are derived from the JSON config. Generated `.conf` files are overwritten on each apply. |
| WireGuard | `config/wireguard/config.json` | `data/wireguard/` | `/etc/wireguard/wg0.conf` | The JSON file defines the interface and all peers. The rendered WireGuard config is overwritten on each apply. |
| networkd | `config/network/config.json` | `data/networkd/` | `/etc/systemd/network/50-<name>.network` | The JSON file defines per-interface static addresses, routes, DNS, DHCP, and link settings. Each entry renders to a `50-<name>.network` INI file. Stale files are cleaned on apply. Public DNS servers are auto-synced to dnsmasq upstreams. |
| ACME | N/A (`~/.acme.sh/` managed by acme.sh) | `data/acme/` | Certificate and key files | acme.sh manages its own state, renewal scheduling, and account keys. Vacuum Wall triggers issuance and renewal but does not maintain independent ACME state. |
## Directory Structure
@@ -95,6 +97,8 @@ config/
│ └── config.json # Proxy domain definitions, management domain, SSL settings
└── wireguard/
└── config.json # WireGuard interface and peer configuration
├── network/
│ └── config.json # Per-interface static IP, routes, DNS, DHCP settings
```
### Data — Runtime Artifacts
@@ -114,6 +118,7 @@ data/
├── logs/
│ └── vacuum-wall.log # Application log file
└── wireguard/ # WireGuard runtime artifacts
├── networkd/ # Generated 50-<name>.network files
```
Both `config/` and `data/` reside within the project directory. The systemd service unit's `ReadWritePaths` directive grants the Flask process write access to both directories, while keeping the rest of the filesystem read-only. The `INSTALL_DIR` value is templated into the service unit at install time.
@@ -128,6 +133,7 @@ The following file system locations are used for integration with system service
| `/etc/nginx/snippets/vacuum-wall-ssl.conf` | Shared SSL configuration snippet (protocols, ciphers, DH parameters, OCSP). Included by all HTTPS server blocks. | Vacuum Wall (lib/nginx.py) |
| `/etc/dnsmasq.d/vacuum-wall.conf` | Generated dnsmasq configuration file. Written from `config/dnsmasq/config.json`. | Vacuum Wall (lib/dnsmasq.py) |
| `/etc/wireguard/wg0.conf` | Generated WireGuard interface configuration. Written from `config/wireguard/config.json`. | Vacuum Wall (lib/wireguard.py) |
| `/etc/systemd/network/50-<name>.network` | Generated systemd-networkd drop-in files. Written from `config/network/config.json`, one per interface. | Vacuum Wall (lib/network.py) |
| `/etc/sudoers.d/vacuum-walld` | Sudo whitelist for the daemon user. Defines all permitted privilege escalations. | Install script (rendered from Jinja2 template) |
The `/etc/nginx/conf.d/vacuum-wall.conf` include file ensures that all domain-specific configurations in `sites-enabled/` are loaded by nginx without modifying the main `nginx.conf`. The SSL snippet keeps TLS settings consistent across all managed domains and allows global updates from a single location.