WireGuard access classes, firewall nftables fixes, network sync event refactor

- WireGuard: refactor to multi-interface 'access classes' model; extract config
  generation and helpers into lib/wireguard.py; add per-class up/down endpoints
  and API routes; update UI with class management pages and QR code component
- Firewall: fix zone creation with --new-zone before --set-target; skip
  masquerade on public zone; add masquerade propagation for nftables backend
  so NAT works when internal zones exit via public
- Network: rename sync event subsystem 'network' -> 'networkd'; always stamp
  config hash even when deployment fails (fixes pending-changes detection)
- DHCP: add new API endpoint and update frontend page
- State/Sync: update state collectors and sync buses for new subsystems
- Docs: update API and config documentation for new endpoints and schemas
This commit is contained in:
2026-07-20 03:57:16 +00:00
parent dadabd7954
commit 04417cf05c
19 changed files with 2688 additions and 455 deletions
+40 -21
View File
@@ -38,13 +38,16 @@ export default definePage({
const zoneData = cfg.zones || {};
const sIface = (state.firewall.data?.state || {}).interfaces || [];
const masqZones = new Set(
// With nftables, masquerade is propagated to the public zone at runtime for
// POSTROUTING to work. The config-side masquerade flag indicates which
// zones source NAT traffic (LAN / internal), not where traffic exits (WAN).
const lanZones = new Set(
Object.entries(zoneData)
.filter(([, zcfg]) => !!zcfg.masquerade)
.map(([z]) => z)
);
const wanIface = sIface.filter((i) => i.zone && masqZones.has(i.zone));
const lanIface = sIface.filter((i) => i.zone && !masqZones.has(i.zone));
const wanIface = sIface.filter((i) => i.zone && !lanZones.has(i.zone));
const lanIface = sIface.filter((i) => i.zone && lanZones.has(i.zone));
const ifaceRows = (ifaces) =>
ifaces.map((iface) => html`<tr key=${'ii-' + iface.name}>
@@ -60,24 +63,40 @@ export default definePage({
<td><${Badge} text=${iface.zone || '—'} variant="secondary" /></td>
</tr>`);
// Build set of non-public zones with masquerade — determines if public is auto-propagated
const anyNonPublicMasq = Object.entries(zoneData)
.filter(([zone]) => zone !== "public")
.some(([, zcfg]) => !!zcfg.masquerade);
const masqRows = Object.entries(zoneData)
.filter(([zone]) => zone !== "public")
.map(([zone, zcfg]) => {
const masq = !!zcfg.masquerade;
return html`<tr key=${'m-' + zone}>
<td><strong>${zone}</strong></td>
<td><${Badge} text=${masq ? 'Enabled' : 'Disabled'} variant=${masq ? 'success' : 'info'} /></td>
<td>
<${ActionButton}
url="/api/firewall/masquerade"
cls="btn btn-sm btn-outline"
labelOn="Disable" labelOff="Enable" condition=${masq}
body=${() => ({ zone, enable: !masq })}
successMsg=${'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone}
refresh="firewall" />
</td>
</tr>`;
});
.map(([zone, zcfg]) => {
const masq = !!zcfg.masquerade;
const isPublic = zone === "public";
// Public zone masquerade is auto-propagated when any non-public zone
// has it enabled (nftables backend dispatches POSTROUTING to the
// output interface's zone chain). Show it read-only with a note.
if (isPublic) {
const effective = masq || anyNonPublicMasq;
return html`<tr key=${'m-' + zone}>
<td><strong>${zone}</strong> <span class="text-muted">(auto)</span></td>
<td><${Badge} text=${effective ? 'Enabled' : 'Disabled'} variant=${effective ? 'success' : 'info'} /></td>
<td><span class="text-muted">${anyNonPublicMasq ? 'Propagated from other zones' : 'Not needed'}</span></td>
</tr>`;
}
return html`<tr key=${'m-' + zone}>
<td><strong>${zone}</strong></td>
<td><${Badge} text=${masq ? 'Enabled' : 'Disabled'} variant=${masq ? 'success' : 'info'} /></td>
<td>
<${ActionButton}
url="/api/firewall/masquerade"
cls="btn btn-sm btn-outline"
labelOn="Disable" labelOff="Enable" condition=${masq}
body=${() => ({ zone, enable: !masq })}
successMsg=${'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone}
refresh="firewall" />
</td>
</tr>`;
});
const fwRows = [];
Object.entries(zoneData).forEach(([zone, zcfg]) => {
@@ -109,7 +128,7 @@ export default definePage({
title: 'WAN / External',
columns: ['Interface', 'IPv4', 'IPv6', 'MAC', 'Zone'],
rows: ifaceRows(wanIface),
emptyText: 'No WAN interfaces with masquerade enabled',
emptyText: 'No WAN interfaces',
}),
DataTableSection({
title: 'Internal / LAN',