sync: add cross-subsystem event bus for config consistency

Add EventBus with loop guards to keep firewall, dnsmasq, wireguard,
and network configs consistent. Handlers emit SyncEvent after mutations;
subscribers compute diffs and write JSON without manual cascade loops.
This commit is contained in:
2026-06-29 20:44:36 +00:00
parent 348bbfbca6
commit 9088f34345
15 changed files with 2117 additions and 59 deletions
+28 -5
View File
@@ -86,7 +86,13 @@ POST /api/firewall/config/apply
Apply the declarative config to live firewalld. Applies targets, services, interfaces, masquerade, rich rules, and forward ports.
**Response:** `data` contains `applied_zones` list and backup path.
**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
@@ -470,7 +476,12 @@ 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.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `applied` | `boolean` | Always `true` on success |
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
### Status
@@ -1114,7 +1125,12 @@ POST /api/wireguard/apply
Write the current configuration to `wg0.conf` and bring the tunnel up.
**Response:** `data` is `null` on success.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `applied` | `boolean` | Always `true` on success |
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
---
@@ -1138,7 +1154,12 @@ POST /api/wireguard/down
Bring down the WireGuard tunnel interface (`wg0`).
**Response:** `data` is `null` on success.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `down` | `boolean` | Always `true` on success |
| `synced` | `[string, ...]` | Subsystems auto-synced as a result |
### Status
@@ -1329,7 +1350,8 @@ Save network config for an interface, render the `.network` file, copy it to `/e
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Interface name |
| `applied` | `boolean` | Always `true` on success |
| `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.
@@ -1369,6 +1391,7 @@ Full sync: generate all `.network` files, remove stale files, copy to `/etc/syst
| `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
+40
View File
@@ -105,6 +105,46 @@ Poll intervals are configurable via `VACUUM_WALL_POLL_INTERVALS` env var (`firew
On collector failure during a poll, no broadcast is sent (avoids noisy ticks). State data is set to `None`.
## Cross-Subsystem Sync Event Bus
When a subsystem's configuration changes, related subsystems are automatically
updated to stay consistent. An in-process event bus (`lib/sync.py`) decouples
subsystems — no handler calls into another handler's logic directly.
### How It Works
1. A mutation handler saves its config (e.g., adding a DHCP range).
2. The handler emits a `SyncEvent` on the event bus.
3. Subscribers react by updating related subsystem configs:
- **DnsToFirewallSync**: Adds `dhcp`, `dns` services and `masquerade` to the
firewall zone for each interface serving a DHCP range.
- **WgToFirewallSync**: Creates or updates a `vpn` firewall zone with
WireGuard interface and UDP 51820 rich rule.
- **FirewallToDhcpSync**: Detects stale DHCP ranges for interfaces not in
any zone (logs warnings, does not auto-remove).
- **NetworkToAllSync**: Suggests DHCP ranges and syncs firewall zone
interface assignments when network config changes.
4. The handler refreshes state for the originating subsystem plus all
transitively affected subsystems.
### Guard Rails
- **Idempotency**: Each subscriber reads current state, computes desired state,
writes the diff. Running twice is safe.
- **No loops**: The event bus tracks `(subsystem, action)` per dispatch cycle.
Re-entrant emits for the same key are silently dropped.
- **Firewall-cmd separation**: Sync subscribers only write JSON config. They
do NOT call `firewall-cmd`. The user clicks "Apply" on the firewall page to
push to firewalld.
- **Error handling**: Subscriber exceptions are caught, logged as warnings,
and do NOT abort the originating handler.
### Frontend Impact
Minimal. The sync happens transparently in the backend. The "pending changes"
indicator on the firewall page will show pending when DHCP or WireGuard saves
(since sync writes JSON but does not call firewall-cmd).
## Directory Structure
### Config — Declarative Settings
+17 -1
View File
@@ -478,4 +478,20 @@ When `POST /api/network/apply` is called, the handler automatically collects pub
### 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]`).
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]`).
## Cross-Subsystem Dependencies
Some subsystems depend on each other. When you modify one, related subsystems
are updated automatically through the event bus.
| Trigger Subsystem | Affected Subsystem | What Happens |
|-------------------|-------------------|--------------|
| dnsmasq (DHCP range) | firewall | Zone gains `dhcp`/`dns` services and `masquerade`. Removing the last range removes them. |
| wireguard (peer add/remove) | firewall | `vpn` zone is created or maintained with `wg0` interface, masquerade, and UDP 51820 rule. |
| firewall (zone changes) | dnsmasq | Stale DHCP ranges (for interfaces no longer in any zone) are automatically removed. Zones with dhcp service but no range are logged as warnings. |
| network (interface config) | firewall | Zone interface assignments in firewall config are updated to match. |
| network (interface config) | dnsmasq | Suggested DHCP ranges are logged when an interface has a static IP but no DHCP range. |
Note: The firewall "Apply" button is still needed to push config changes to
firewalld. Sync only updates the declarative JSON.