feat: add system config import, refactor install script and nginx auth
- lib/system_import: new module to import system configs into JSON at daemon startup - daemon/server.py: call import_all() during startup for config reconciliation - daemon/handlers/nginx.py: simplify add_domain auth handling, remove duplicate code - scripts/install.sh: replace inline Python setup with curl-based daemon API calls; apply IP forwarding at runtime - hoover: bump internal asset versions to v=8 - pages: bump asset versions to v=9
This commit is contained in:
+65
-99
@@ -277,11 +277,16 @@ render_template "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.service" \
|
||||
install -m 0644 "${PROJECT_DIR}/system/systemd/vacuum-wall-acme.timer" /etc/systemd/system/vacuum-wall-acme.timer
|
||||
systemctl daemon-reload
|
||||
|
||||
# --- 7. Enable IP forwarding (persistent via sysctl.conf) ---
|
||||
# --- 7. Enable IP forwarding (persistent via sysctl.conf + runtime apply) ---
|
||||
log "Enabling IP forwarding..."
|
||||
if ! grep -q "^net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null; then
|
||||
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
|
||||
fi
|
||||
# Apply immediately so NAT works without reboot
|
||||
if [ "$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null)" != "1" ]; then
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 && log "IP forwarding enabled at runtime" || \
|
||||
warn "Could not enable IP forwarding at runtime"
|
||||
fi
|
||||
|
||||
# --- 8. Detect network interfaces ---
|
||||
log "Detecting network interfaces..."
|
||||
@@ -348,108 +353,73 @@ else
|
||||
chown "$USER_DAEMON_NAME:$USER_GROUP" "$_SOCKET" 2>/dev/null || true
|
||||
chmod 0660 "$_SOCKET" 2>/dev/null || true
|
||||
|
||||
# --- 10. Configure subsystems via daemon API ---
|
||||
log "Configuring subsystems via daemon API..."
|
||||
WAN_IFACE="$WAN_IFACE" \
|
||||
LAN_IFACES="$LAN_IFACES" \
|
||||
MGMT_DOMAIN="$DOMAIN" \
|
||||
MGMT_USER="$MGMT_USER" \
|
||||
MGMT_PASS="$MGMT_PASS" \
|
||||
"${PROJECT_DIR}/.venv/bin/python3" -c "
|
||||
import daemon.client as c
|
||||
from daemon.iface import (
|
||||
POST_ACME_SELF_SIGNED, POST_NGINX_DOMAINS_ADD, POST_NGINX_APPLY,
|
||||
POST_FIREWALL_CONFIG, POST_FIREWALL_CONFIG_APPLY, POST_NETWORK_SYSCTL_SET,
|
||||
GET_NETWORK_INFER_DHCP_RANGES,
|
||||
)
|
||||
import sys
|
||||
echo ""
|
||||
echo " Setting up initial management configuration..."
|
||||
|
||||
domain = '${DOMAIN}'
|
||||
mgmt_user = '${MGMT_USER}'
|
||||
mgmt_pass = '${MGMT_PASS}'
|
||||
wan_iface = '${WAN_IFACE}'
|
||||
lan_ifaces = '${LAN_IFACES}'
|
||||
# htpasswd is created by the daemon via /nginx/domains/add (writes to data/.htpasswd)
|
||||
|
||||
# Self-signed cert for management domain
|
||||
try:
|
||||
res = c.post(POST_ACME_SELF_SIGNED, {'domain': domain, 'days': 365})
|
||||
print(f' [cert] Self-signed: {\"generated\" if res.get(\"generated\") else \"exists\"}')
|
||||
except Exception as e:
|
||||
print(f' [cert] Warning: {e}', file=sys.stderr)
|
||||
# Helper: POST JSON to daemon API over Unix socket
|
||||
_daemon_post() {
|
||||
local endpoint="$1"
|
||||
local json="$2"
|
||||
local label="${3:-POST $endpoint}"
|
||||
local resp
|
||||
if resp=$(curl -s -f --unix-socket "$_SOCKET" \
|
||||
"http://localhost${endpoint}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$json" 2>&1); then
|
||||
log "$label"
|
||||
return 0
|
||||
else
|
||||
warn "$label: $resp"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Management proxy domain + htpasswd
|
||||
try:
|
||||
c.post(POST_NGINX_DOMAINS_ADD, {
|
||||
'domain': domain,
|
||||
'paths': {
|
||||
'/': {
|
||||
'backend': {'host': '127.0.0.1', 'port': 9090, 'proto': 'http'},
|
||||
'is_management': True,
|
||||
},
|
||||
'/ws': {
|
||||
'backend': {'host': '127.0.0.1', 'port': 9091, 'proto': 'http'},
|
||||
'is_websocket': True,
|
||||
},
|
||||
# 1A. Self-signed certificate
|
||||
_daemon_post "/acme/self-signed" "{\"domain\":\"$DOMAIN\"}" "Self-signed certificate"
|
||||
|
||||
# 1C. Management proxy domain
|
||||
_daemon_post "/nginx/domains/add" "$(jq -n \
|
||||
--arg domain "$DOMAIN" \
|
||||
--arg user "$MGMT_USER" \
|
||||
--arg pass "$MGMT_PASS" \
|
||||
'{
|
||||
domain: $domain,
|
||||
paths: {
|
||||
"/": {
|
||||
backend: {host: "127.0.0.1", port: 9090, proto: "http"},
|
||||
is_management: true
|
||||
},
|
||||
"/ws": {
|
||||
backend: {host: "127.0.0.1", port: 9091, proto: "http"},
|
||||
is_websocket: true
|
||||
}
|
||||
},
|
||||
'auth_user': mgmt_user,
|
||||
'auth_pass': mgmt_pass,
|
||||
})
|
||||
c.post(POST_NGINX_APPLY)
|
||||
print(f' [proxy] Management proxy configured for {domain}')
|
||||
except Exception as e:
|
||||
print(f' [proxy] Warning: {e}', file=sys.stderr)
|
||||
auth_user: $user,
|
||||
auth_pass: $pass
|
||||
}')" "Management domain configured"
|
||||
|
||||
# Firewall config (interface detection done in bash above)
|
||||
import json as _json
|
||||
zones = {}
|
||||
_daemon_post "/nginx/apply" "{}" "Nginx config applied"
|
||||
|
||||
if wan_iface:
|
||||
zones['public'] = {
|
||||
'target': 'DEFAULT',
|
||||
'interfaces': [i for i in wan_iface.split(',') if i],
|
||||
'services': ['http', 'https', 'ssh'],
|
||||
'masquerade': True,
|
||||
}
|
||||
# Firewall zone assignment
|
||||
if [[ -n "$WAN_IFACE" ]]; then
|
||||
_daemon_post "/firewall/zones/interfaces" \
|
||||
"$(jq -n --arg zone "public" --arg iface "$WAN_IFACE" \
|
||||
'{zone: $zone, interfaces: [$iface]}')" \
|
||||
"WAN interface assigned to public zone"
|
||||
fi
|
||||
|
||||
if lan_ifaces:
|
||||
zones['internal'] = {
|
||||
'target': 'ACCEPT',
|
||||
'interfaces': [i for i in lan_ifaces.split(',') if i],
|
||||
'services': ['dhcp', 'dns', 'ntp'],
|
||||
'masquerade': False,
|
||||
}
|
||||
if [[ -n "$LAN_IFACES" ]]; then
|
||||
# Convert comma-separated list to JSON array
|
||||
LAN_JSON=$(echo "$LAN_IFACES" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | jq -R . | jq -s '.')
|
||||
_daemon_post "/firewall/zones/interfaces" \
|
||||
"$(jq -n --arg zone "internal" --argjson ifaces "$LAN_JSON" \
|
||||
'{zone: $zone, interfaces: $ifaces}')" \
|
||||
"LAN interfaces assigned to internal zone"
|
||||
fi
|
||||
|
||||
# Always create vpn zone skeleton for later WireGuard setup
|
||||
zones['vpn'] = {
|
||||
'target': 'ACCEPT',
|
||||
'interfaces': [],
|
||||
'services': [],
|
||||
'masquerade': False,
|
||||
}
|
||||
|
||||
try:
|
||||
c.post(POST_FIREWALL_CONFIG, {'zones': zones})
|
||||
c.post(POST_FIREWALL_CONFIG_APPLY)
|
||||
print(' [firewall] Zones configured and applied')
|
||||
except Exception as e:
|
||||
print(f' [firewall] Warning: {e}', file=sys.stderr)
|
||||
|
||||
# IP forwarding
|
||||
try:
|
||||
c.post(POST_NETWORK_SYSCTL_SET, {'name': 'net.ipv4.ip_forward', 'value': '1'})
|
||||
print(' [network] IP forwarding enabled')
|
||||
except Exception as e:
|
||||
print(f' [network] Warning: {e}', file=sys.stderr)
|
||||
|
||||
# Infer DHCP ranges (logged for user reference)
|
||||
try:
|
||||
ranges = c.get(GET_NETWORK_INFER_DHCP_RANGES)
|
||||
for iface, rng in ranges.get('ranges', {}).items():
|
||||
print(f' [suggestion] DHCP range for {iface}: {rng.get(\"start\")}-{rng.get(\"end\")}')
|
||||
except Exception:
|
||||
pass
|
||||
"
|
||||
log "Subsystem configuration complete"
|
||||
unset _daemon_post
|
||||
fi
|
||||
|
||||
systemctl start vacuum-wall >/dev/null 2>&1 && log "Started vacuum-wall WebUI" || warn "Could not start vacuum-wall WebUI"
|
||||
@@ -489,7 +459,3 @@ 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"
|
||||
echo " when DNS points to this appliance."
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user