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
+34 -31
View File
@@ -45,22 +45,22 @@ while [[ $# -gt 0 ]]; do
" --dev Dev mode: auto-detect repo owner, skip safety warning" \
" --mgmt-pass PASS WebUI basic auth password (required)" \
" --mgmt-user USER WebUI basic auth username (default: admin)" \
" --mgmt-domain DOMAIN Management domain (auto-detected)" \
" --acme-email EMAIL ACME registration email (required)" \
" --wan-iface IFACE WAN interface name (auto-detected)" \
" --mgmt-domain DOMAIN Management domain (auto-detected)" \
" --acme-email EMAIL ACME contact email (optional, deprecated — use WebUI)" \
" --wan-iface IFACE WAN interface name (auto-detected)" \
" --lan-ifaces IFC,... LAN interface names, comma-separated (auto-detected)" \
" -h, --help Show this help" \
"" \
"All options also have environment variable equivalents:" \
" USER_NAME, INSTALL_DIR, MGMT_PASS, MGMT_USER," \
" MGMT_DOMAIN, ACME_EMAIL, WAN_IFACE, LAN_IFACES." \
" USER_NAME, INSTALL_DIR, MGMT_PASS, MGMT_USER," \
" MGMT_DOMAIN, WAN_IFACE, LAN_IFACES." \
" CLI flags take precedence over env vars." \
"" \
"Example (dev):" \
" ./install.sh --dev --mgmt-pass pass --acme-email me@example.com" \
"Example (dev):" \
" ./install.sh --dev --mgmt-pass pass" \
"" \
"Example (prod):" \
" MGMT_PASS=pass ACME_EMAIL=me@example.com ./install.sh --user vacuum-wall"
"Example (prod):" \
" MGMT_PASS=pass ./install.sh --user vacuum-wall"
exit 0
;;
*)
@@ -74,6 +74,7 @@ REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
# Required settings (no defaults — must be provided)
MGMT_PASS="${_cli_mgmt_pass:-${MGMT_PASS:-}}"
# ACME_EMAIL is optional — will be configured from the WebUI
ACME_EMAIL="${_cli_acme_email:-${ACME_EMAIL:-}}"
# Optional settings with defaults
@@ -110,18 +111,16 @@ LAN_IFACES="${_cli_lan_ifaces:-${LAN_IFACES:-}}"
# --- Validate required settings ---
missing=()
[[ -z "$MGMT_PASS" ]] && missing+=("MGMT_PASS (--mgmt-pass)")
[[ -z "$ACME_EMAIL" ]] && missing+=("ACME_EMAIL (--acme-email)")
[[ -z "$MGMT_PASS" ]] && missing+=("MGMT_PASS (--mgmt-pass)")
if (( ${#missing[@]} )); then
echo -e "${RED}[!!]${NC} Missing required settings:"
for v in "${missing[@]}"; do
case "$v" in
"MGMT_PASS (--mgmt-pass)") echo ' export MGMT_PASS="your-password" # or --mgmt-pass';;
"ACME_EMAIL (--acme-email)") echo " export ACME_EMAIL=\"you@example.com\" # or --acme-email";;
"MGMT_PASS (--mgmt-pass)") echo ' export MGMT_PASS="your-password" # or --mgmt-pass';;
esac
done
printf '\nTo run: MGMT_PASS=pass ACME_EMAIL=you@example.com ./install.sh\n'
printf '\nTo run: MGMT_PASS=pass ./install.sh\n'
exit 1
fi
ACME_HOME="$PROJECT_DIR/data/acme"
@@ -616,20 +615,23 @@ nginx -t 2>/dev/null && nginx -s reload 2>/dev/null && log "Reloaded nginx" || \
systemctl restart nginx >/dev/null 2>&1 && log "Restarted nginx" || \
warn "Could not restart nginx (check config)"
# --- 14. Configure acme.sh default email ---
if [[ -f "$ACME_HOME/account.conf" ]] && grep -q '^ACME_LEEMAIL=' "$ACME_HOME/account.conf" 2>/dev/null; then
log "acme.sh account already registered, skipping."
# --- 14. Write initial ACME config (skip if user has customized it) ---
ACME_CFG="${PROJECT_DIR}/config/acme/config.json"
if [[ -f "$ACME_CFG" ]]; then
log "ACME config already exists, skipping."
else
# acme.sh must never run as root — always as the service user via sudo -u.
# This prevents acme.sh from running any command as root and limits its
# ability to modify system files.
log "Registering acme.sh account with email $ACME_EMAIL..."
mkdir -p "$ACME_HOME/www"
chown "$USER_DAEMON_NAME:$USER_GROUP" "$ACME_HOME/www"
sudo -u "$USER_DAEMON_NAME" env ACME_HOME="$ACME_HOME" HOME="$PROJECT_DIR" \
"$ACME_HOME/acme.sh" --home "$ACME_HOME" --config-home "$ACME_HOME" \
--register-account -m "$ACME_EMAIL" 2>/dev/null || \
warn "Could not register acme.sh account (will be done from WebUI)"
log "Writing initial ACME configuration..."
mkdir -p "${PROJECT_DIR}/config/acme"
ACME_EMAIL="$ACME_EMAIL" \
ACME_CFG="$ACME_CFG" \
"${PROJECT_DIR}/.venv/bin/python3" -c "
import json, os
cfg = {'email': os.environ.get('ACME_EMAIL', '') or ''}
with open(os.environ['ACME_CFG'], 'w') as f:
json.dump(cfg, f, indent=4)
f.write('\n')
"
log "ACME config written (register via WebUI to activate)"
fi
# --- Done ---
@@ -657,10 +659,11 @@ else
fi
echo ""
echo " Next steps:"
echo " 1. Verify zone assignments at https://$DOMAIN/interfaces"
echo " 2. Configure DHCP ranges for your LAN"
echo " 3. Add proxy domains with ACME certificates"
echo " 4. Set up WireGuard tunnel (optional)"
echo " 1. Set ACME contact email at https://$DOMAIN/certs/settings"
echo " 2. Verify zone assignments at https://$DOMAIN/interfaces"
echo " 3. Configure DHCP ranges for your LAN"
echo " 4. Add proxy domains with ACME certificates"
echo " 5. Set up WireGuard tunnel (optional)"
echo ""
echo " NOTE: A self-signed certificate was generated."
echo " From the WebUI, issue a real certificate for $DOMAIN"