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:
+133
@@ -1190,6 +1190,139 @@ Returns HTTP `404` if the peer is not found.
|
||||
|
||||
---
|
||||
|
||||
## 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 `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` | Always `true` on success |
|
||||
|
||||
---
|
||||
|
||||
#### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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"`) |
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
+89
-1
@@ -291,4 +291,92 @@ The `zones` object maps zone names (keys) to zone configurations. Each zone corr
|
||||
|
||||
### Applying Firewall Configuration
|
||||
|
||||
The `config_pending()` function compares the declarative config in `config/firewall/config.json` against the live firewalld state returned by the daemon (via `daemon.handlers.firewall.get_state()`). It returns a diff indicating which zones have pending changes for interfaces, services, target, masquerade, forward ports, and rich rules. Zones that exist live but not in config are reported as `unmanaged_zones`.
|
||||
The `config_pending()` function compares the declarative config in `config/firewall/config.json` against the live firewalld state returned by the daemon (via `daemon.handlers.firewall.get_state()`). It returns a diff indicating which zones have pending changes for interfaces, services, target, masquerade, forward ports, and rich rules. Zones that exist live but not in config are reported as `unmanaged_zones`.
|
||||
|
||||
## Networkd (IP Configuration)
|
||||
|
||||
**File**: `config/network/config.json`
|
||||
|
||||
This file defines static IP configuration for network interfaces managed by systemd-networkd. The application renders each interface entry into a `50-<name>.network` INI file in `data/networkd/`, which the handler copies to `/etc/systemd/network/`.
|
||||
|
||||
```json
|
||||
{
|
||||
"interfaces": {
|
||||
"eth0": {
|
||||
"addresses": ["192.168.1.1/24"],
|
||||
"gateway": "192.168.1.254",
|
||||
"dns": ["8.8.8.8", "1.1.1.1"],
|
||||
"dhcp": "no"
|
||||
},
|
||||
"eth1": {
|
||||
"dhcp": "ipv4",
|
||||
"dns_default_route": true,
|
||||
"dhcp_client": {
|
||||
"hostname": "router",
|
||||
"use_dns": true
|
||||
}
|
||||
},
|
||||
"wg0": {
|
||||
"addresses": [{"address": "10.137.0.1/24"}],
|
||||
"routes": [
|
||||
{
|
||||
"destination": "10.0.0.0/8",
|
||||
"gateway": "10.137.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interface Entry Fields
|
||||
|
||||
Each key in the `interfaces` object is an interface name (e.g., `eth0`, `eth1`, `wg0`). The value is a dict with the following keys:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `addresses` | `array` | IPv4 addresses. Each item is either a bare CIDR string (`"192.168.1.1/24"`) or a dict with `address`, `label`, `scope`, `route_metric`, `duplicate_address_detection`, `manage_temporary_address`, `add_prefix_route`. Renders to `[Address]` sections. |
|
||||
| `ipv6_addresses` | `array` | Same as `addresses`, but for IPv6. |
|
||||
| `gateway` | `string` | Default IPv4 gateway (`[Network] Gateway=`). |
|
||||
| `ipv6_gateway` | `string` | Default IPv6 gateway (`[Network] IPv6Gateway=`). |
|
||||
| `dns` | `array` | IPv4 DNS servers (`[Network] DNS=`, one per line). |
|
||||
| `ipv6_dns` | `array` | IPv6 DNS servers (`[Network] IPv6DNS=`). |
|
||||
| `domains` | `array` | Search domains (`[Network] Domains=`). |
|
||||
| `ipv6_domains` | `array` | IPv6 search domains (`[Network] IPv6Domains=`). |
|
||||
| `dns_default_route` | `boolean` | Whether DNS is the default route for resolution (`[Network] DNSDefaultRoute=`). |
|
||||
| `dhcp` | `string` | DHCP mode: `"yes"`, `"ipv4"`, `"ipv6"`, `"no"`. Controls `[Network] DHCP=` and whether `[DHCPv4]`/`[DHCPv6]` sections are rendered. |
|
||||
| `routes` | `array` | Static routes. Each dict has `destination`, `gateway`, `metric`, `table`, `type`, `scope`, `gateway_on_link`, `ipv6_preference`, `initial_congestion_window`, `initial_advertised_receive_window`, `quick_ack`, `fast_open_no_cookie`, `mtu_bytes`, `protocol`, `next_hop`, `multi_path_route`. Renders to `[Route#N]` sections. |
|
||||
| `link` | `object` | Link settings: `mtu_bytes`, `mac_address`, `arp`, `multicast`, `all_multicast`, `promiscuous`, `unmanaged`, `activation_policy`, `required_for_online`. Renders to `[Link]` section. |
|
||||
| `dhcp_client` | `object` | DHCP client settings. Shared keys for both `[DHCPv4]` and `[DHCPv6]`: `hostname`, `duid_type`, `duid_raw_data`, `iaid`, `client_identifier`, `rapid_commit`, `anonymize`, `use_dns`, `use_ntp`, `use_sip`, `use_captive_portal`, `use_mtu`, `use_hostname`, `use_domains`, `use_routes`, `route_metric`, `send_decline`, `net_label`, `nft_set`, `ip_service_type`, `socket_priority`, `bootp`, `label`, `max_attempts`, `listen_port`, `server_port`, `mud_url`, `boot_filename`, `send_option`, `send_vendor_option`, `user_class`, `vendor_class_identifier`, `request_options`. |
|
||||
| `bind_carrier` | `array` | Carrier interfaces to bind to. |
|
||||
| `ignore_carrier_loss` | `boolean` | Ignore carrier loss events. |
|
||||
| `keep_configuration` | `boolean` | Keep configuration on stop. |
|
||||
| `configure_without_carrier` | `boolean` | Configure even without carrier. |
|
||||
| `link_local_addressing` | `string` | Link-local addressing mode. |
|
||||
| `ipv6_link_local_address_generation_mode` | `string` | IPv6 link-local address generation mode. |
|
||||
| `ipv6_stable_secret_address` | `string` | Stable secret for IPv6 address generation. |
|
||||
| `ipv4_ll_start_address` | `string` | Link-local IPv4 start address. |
|
||||
| `ipv4_ll_route` | `boolean` | Add route to link-local IPv4 address. |
|
||||
| `default_route_on_device` | `boolean` | Always add default route via this device. |
|
||||
| `ipv6_hop_limit` | `int` | IPv6 hop limit. |
|
||||
| `ipv6_retransmission_time_sec` | `string` | IPv6 retransmission timeout. |
|
||||
| `ipv4_duplicate_address_detection_timeout_sec` | `string` | IPv4 DAD timeout. |
|
||||
| `ipv4_reverse_path_filter` | `string` | IPv4 reverse path filtering mode. |
|
||||
| `ipv4_accept_local` | `boolean` | Accept packets to local addresses as non-local. |
|
||||
| `ipv4_route_localnet` | `boolean` | Route local network traffic. |
|
||||
| `ipv4_proxy_arp` | `boolean` | Enable proxy ARP. |
|
||||
| `ipv4_proxy_arp_private_vlan` | `boolean` | Private VLAN proxy ARP. |
|
||||
| `ipv6_proxy_ndp` | `boolean` | Enable IPv6 proxy NDP. |
|
||||
| `ipv6_proxy_ndp_address` | `string` | IPv6 proxy NDP address. |
|
||||
| `ipv6_send_ra` | `boolean` | Send IPv6 Router Advertisements. |
|
||||
| `m_pls_routing` | `boolean` | Enable MPLS routing. |
|
||||
| `keep_master` | `boolean` | Keep master on stop. |
|
||||
| `ip_family` | `string` | IP family to use. |
|
||||
|
||||
### DNS Upstream Sync
|
||||
|
||||
When `POST /api/network/apply` is called, the handler automatically collects public DNS servers from all networkd interface configs (via `collect_upstream_dns()`), filters out local/private-range addresses, and syncs the deduplicated list to dnsmasq's upstream DNS configuration. This keeps dnsmasq's upstream resolvers in sync with whatever DNS the WAN interface receives (whether statically configured or via DHCP).
|
||||
|
||||
### Generated Files
|
||||
|
||||
Each interface config entry produces a `50-<name>.network` file in `data/networkd/`. During apply, these are copied to `/etc/systemd/network/` and stale files (not matching any config entry) are removed. File generation uses the `systemd.syntax(7)` naming convention: first section is bare (`[Address]`, `[Route]`), subsequent sections use `#` suffix (`[Address#1]`, `[Route#2]`).
|
||||
+14
-3
@@ -2,11 +2,11 @@
|
||||
|
||||
## What is Vacuum Wall?
|
||||
|
||||
Vacuum Wall is a zone-based firewall appliance with a built-in SSL reverse proxy, providing a unified platform for network security and traffic management. It combines firewalld policy control, DHCP/DNS services, WireGuard VPN tunnels, and automated certificate provisioning into a single device. A single web UI controls everything, making enterprise-grade network infrastructure manageable from one place.
|
||||
Vacuum Wall is a zone-based firewall appliance with a built-in SSL reverse proxy, providing a unified platform for network security and traffic management. It combines firewalld policy control, DHCP/DNS services, systemd-networkd for static IP management, WireGuard VPN tunnels, and automated certificate provisioning into a single device. A single web UI controls everything, making enterprise-grade network infrastructure manageable from one place.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Vacuum Wall is built around four integrated subsystems managed through a two-layer architecture: a non-privileged Flask web UI and a privileged background daemon (`vacuum-walld`). The web UI communicates with the daemon via a Unix socket. The daemon handles all privileged operations (sudo) for the subsystems: the traffic plane uses firewalld with its nftables backend (zone-based policies, source NAT, destination NAT); the DNS/DHCP plane serves private subnets via dnsmasq; the proxy plane runs nginx with automatic ACME certificates through acme.sh; and the VPN plane uses WireGuard (wg-quick) for encrypted tunnel management. All subsystems are configured and monitored through the Flask web UI, which is itself proxied through nginx with basic HTTP authentication.
|
||||
Vacuum Wall is built around five integrated subsystems managed through a two-layer architecture: a non-privileged Flask web UI and a privileged background daemon (`vacuum-walld`). The web UI communicates with the daemon via a Unix socket. The daemon handles all privileged operations (sudo) for the subsystems: the traffic plane uses firewalld with its nftables backend (zone-based policies, source NAT, destination NAT); the DNS/DHCP plane serves private subnets via dnsmasq; the network plane uses systemd-networkd for static IP management; the proxy plane runs nginx with automatic ACME certificates through acme.sh; and the VPN plane uses WireGuard (wg-quick) for encrypted tunnel management. All subsystems are configured and monitored through the Flask web UI, which is itself proxied through nginx with basic HTTP authentication.
|
||||
|
||||
## Subsystems
|
||||
|
||||
@@ -22,6 +22,10 @@ dnsmasq serves as both the DHCP server and local DNS resolver. It is configured
|
||||
|
||||
The nginx reverse proxy handles HTTPS termination for user-defined domains, with certificates automatically provisioned and renewed via acme.sh and an ACME provider (ZeroSSL by default). Each proxy domain is configured with an HTTP-to-HTTPS redirect, modern TLS settings, and a configurable backend target. New proxy domains are added through the web UI, and the configuration is applied without manual intervention.
|
||||
|
||||
### Network (systemd-networkd)
|
||||
|
||||
The networkd subsystem manages static IP configuration for network interfaces via systemd-networkd. It renders declarative JSON configuration into per-interface `.network` INI files (`50-<name>.network`), supporting static addresses, routes, DNS, DHCP clients, link settings, and all `[Address]`, `[Route]`, `[DHCPv4]`, `[DHCPv6]`, and `[Link]` section keys. When the full apply runs, public DNS servers from networkd configs are auto-synced to dnsmasq's upstream resolvers. Helper endpoints can infer candidate DHCP ranges from static IPs and suggest firewalld zone assignments based on interface role.
|
||||
|
||||
### WireGuard
|
||||
|
||||
WireGuard support provides server-side VPN tunnel management. Peers are added through the web UI, with the system generating client configuration files that can be downloaded and applied on remote devices. The dashboard displays active connections and transfer statistics for each peer, allowing operators to monitor tunnel health and usage.
|
||||
@@ -31,6 +35,7 @@ WireGuard support provides server-side VPN tunnel management. Peers are added th
|
||||
- Debian 13 (trixie) target platform
|
||||
- Python 3.13+, Flask 3.x for web management
|
||||
- firewalld (nftables backend)
|
||||
- systemd-networkd (ip-lladdr, networkctl)
|
||||
- nginx 1.26+
|
||||
- dnsmasq
|
||||
- WireGuard tools (wireguard-tools)
|
||||
@@ -60,6 +65,7 @@ After installation, access the management interface at `https://<hostname>.local
|
||||
├── .venv/ # Python virtual environment
|
||||
├── config/ # Declarative JSON configuration (source of truth)
|
||||
│ ├── dnsmasq/ # DHCP/DNS config
|
||||
│ ├── network/ # systemd-networkd per-interface config
|
||||
│ ├── firewall/ # Firewall zone & rule config
|
||||
│ ├── nginx/ # Proxy domain & SSL config
|
||||
│ └── wireguard/ # VPN interface & peer config
|
||||
@@ -69,11 +75,13 @@ After installation, access the management interface at `https://<hostname>.local
|
||||
│ ├── acme/ # ACME certificates
|
||||
│ ├── firewall/ # Firewall rule backup
|
||||
│ ├── logs/ # Application logs
|
||||
│ ├── networkd/ # Generated 50-<name>.network files
|
||||
│ └── wireguard/ # Generated WireGuard configs
|
||||
├── daemon/ # Privileged background daemon
|
||||
│ ├── server.py # aiohttp server, cache, batch routing, handler registry
|
||||
│ ├── client.py # Sync HTTP client over Unix socket
|
||||
│ └── handlers/ # Privileged operation handlers (all sudo calls)
|
||||
│ ├── handlers/ # Privileged operation handlers (all sudo calls)
|
||||
│ │ └── network.py # networkd handler (generate + apply)
|
||||
├── system/ # System file templates (all Jinja2)
|
||||
│ ├── systemd/ # Service and timer unit files
|
||||
│ │ ├── vacuum-wall.service # Web UI service (rendered at install)
|
||||
@@ -87,8 +95,10 @@ After installation, access the management interface at `https://<hostname>.local
|
||||
│ ├── common.py # Shared utilities (run, run_proc, load_json, save_json, deep_merge, ensure_dirs)
|
||||
│ ├── logging.py # Logging setup
|
||||
│ ├── firewall.py # firewalld bindings
|
||||
│ ├── network.py # systemd-networkd rendering & parsing
|
||||
│ ├── dnsmasq.py # DHCP/DNS configuration
|
||||
│ ├── nginx.py # Reverse proxy configuration
|
||||
│ ├── state.py # State collector (uses lib.network.parse_networkctl_status)
|
||||
│ ├── acme.py # Certificate management
|
||||
│ └── wireguard.py # VPN tunnel management
|
||||
├── webui/ # Flask web application
|
||||
@@ -100,6 +110,7 @@ After installation, access the management interface at `https://<hostname>.local
|
||||
│ │ ├── proxy.py # Nginx proxy API
|
||||
│ │ ├── certs.py # Certificate API
|
||||
│ │ ├── wireguard.py # WireGuard API
|
||||
│ │ ├── network.py # Networkd API
|
||||
│ │ └── logs.py # Logs API
|
||||
│ ├── templates/ # Jinja2/HTMX templates
|
||||
│ └── static/ # CSS and client-side JS
|
||||
|
||||
@@ -40,6 +40,9 @@ The file `/etc/sudoers.d/vacuum-walld` grants the daemon user (`vacuum-walld`) p
|
||||
| Certificates | (none) | acme.sh runs as the non-root service user directly; no sudo escalation is needed (webroot validation is used) |
|
||||
| Network queries | `ip -o link show` | List network interfaces |
|
||||
| Network queries | `ip -o addr show` | List IP addresses on interfaces |
|
||||
| Networkd | `networkctl status *` | Query interface status from networkd |
|
||||
| Networkd | `networkctl reload *` | Reload networkd for a specific interface |
|
||||
| Networkd | `networkctl reload` | Reload networkd for all interfaces |
|
||||
| Logs | `journalctl --unit=* -n *` | Query systemd journal for managed services |
|
||||
| Logs | `cat /var/log/nginx/*` | Read nginx access and error logs |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user