Closes the gap behind the vpn-recreate bug: when the shared network is recreated with a different /24, every app's stored static IP is left outside it and adoptDockerSubnet only realigns CFG, not the apps. - networkScanConflicts (network_conflicts.sh): read-only scan diffing each active network_resources IP against docker's real subnet (via ipInSubnet). Per-service routing-aware — skips gateway-routed services whose ipv4 is commented out in the deployed compose, so gluetun apps don't false-positive. Distinguishes 'daemon down' (benign) from 'network missing' (real). - webuiSystemNetworkCheck (webui_system_network.sh): self-throttled generator that writes frontend/data/system/network_status.json (modelled on verify_status.json). Wired into webuiSystemUpdate AND run unconditionally every ~60s from the task-processor poll (regen webui is mtime-gated and would never fire on drift, which touches no source file). - networkHealConflicts (network_heal.sh) + 'libreportal system network check|heal [app]': the heal adopts docker's subnet in-process, then re-IPs stranded apps with reset_network=ip (ports preserved), gluetun first. Mutating path runs only through the task system (dual-mode, like update apply); read-only check runs inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
4.0 KiB
Bash
89 lines
4.0 KiB
Bash
#!/bin/bash
|
|
|
|
# Network-drift heal — the mutating half of the detector. Runs ONLY through the
|
|
# task system (see cli_system_commands.sh `network heal`, which enqueues unless
|
|
# LIBREPORTAL_TASK_EXEC=1), never a direct API.
|
|
#
|
|
# It (1) realigns CFG to docker's real subnet IN-PROCESS so re-IP draws from the
|
|
# corrected /24, then (2) re-IPs each stranded app with ports PRESERVED
|
|
# (reset_network="ip"), healing a gateway provider (gluetun) first so recreating
|
|
# it doesn't orphan the apps routed through it. Re-IP runs sequentially, and a
|
|
# fresh scan afterwards rewrites network_status.json so the WebUI badge clears
|
|
# (or stays, if anything failed to heal).
|
|
#
|
|
# networkHealConflicts [app] # heal one app, or all detected conflicts
|
|
networkHealConflicts() {
|
|
local target_app="$1"
|
|
isHeader "Healing network drift"
|
|
|
|
# 1) Realign CFG to docker's real subnet in this process (ipFindAvailable
|
|
# reads CFG_NETWORK_SUBNET, so this must happen before any re-IP).
|
|
local docker_subnet
|
|
docker_subnet=$(dockerCommandRun "docker network inspect $CFG_NETWORK_NAME --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'" 2>/dev/null | tr -d '[:space:]')
|
|
if [[ -z "$docker_subnet" ]]; then
|
|
isNotice "Network '$CFG_NETWORK_NAME' not present — (re)creating it."
|
|
DOCKER_NETWORK_SETUP_NEEDED="true"
|
|
declare -f installDockerNetwork >/dev/null 2>&1 && installDockerNetwork
|
|
docker_subnet=$(dockerCommandRun "docker network inspect $CFG_NETWORK_NAME --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'" 2>/dev/null | tr -d '[:space:]')
|
|
fi
|
|
if [[ -n "$docker_subnet" && "$docker_subnet" != "$CFG_NETWORK_SUBNET" ]]; then
|
|
declare -f adoptDockerSubnet >/dev/null 2>&1 && adoptDockerSubnet "$docker_subnet"
|
|
fi
|
|
|
|
# 2) Build the app set (unique). Either the requested app, or a fresh scan.
|
|
local -a apps=() a
|
|
if [[ -n "$target_app" ]]; then
|
|
apps=("$target_app")
|
|
else
|
|
networkScanConflicts # populates NET_CONFLICTS (call direct, not $(...))
|
|
local row seen=""
|
|
for row in "${NET_CONFLICTS[@]}"; do
|
|
a="${row%%|*}"
|
|
[[ -n "$a" ]] || continue
|
|
# de-dupe app names (an app can have several drifted services)
|
|
[[ " $seen " == *" $a "* ]] && continue
|
|
seen+=" $a"
|
|
apps+=("$a")
|
|
done
|
|
fi
|
|
|
|
if [[ ${#apps[@]} -eq 0 ]]; then
|
|
isSuccessful "No network conflicts to heal."
|
|
declare -f webuiSystemNetworkCheck >/dev/null 2>&1 && webuiSystemNetworkCheck "force" >/dev/null 2>&1
|
|
return 0
|
|
fi
|
|
|
|
# 3) Heal a gateway PROVIDER first — recreating gluetun re-attaches every app
|
|
# routed through it, so it must settle before (or be reconciled after) them.
|
|
local -a ordered=()
|
|
for a in "${apps[@]}"; do [[ "$a" == "gluetun" ]] && ordered+=("$a"); done
|
|
for a in "${apps[@]}"; do [[ "$a" != "gluetun" ]] && ordered+=("$a"); done
|
|
|
|
# 4) Re-IP each (IP-only — ports preserved), sequentially.
|
|
local attempted=0
|
|
for a in "${ordered[@]}"; do
|
|
if [[ ! "$a" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
|
|
isError "Skipping invalid app slug: $a"; continue
|
|
fi
|
|
isNotice "Re-IPing '$a' into ${CFG_NETWORK_SUBNET} (ports preserved)…"
|
|
dockerInstallApp "$a" "" ip
|
|
((attempted++))
|
|
done
|
|
|
|
# 5) Reconcile gateway-routed apps onto the (possibly recreated) provider.
|
|
declare -f appGluetunRecreateRouted >/dev/null 2>&1 && appGluetunRecreateRouted >/dev/null 2>&1 || true
|
|
|
|
# 6) Fresh scan rewrites the status file; report what (if anything) remains.
|
|
if declare -f webuiSystemNetworkCheck >/dev/null 2>&1; then
|
|
webuiSystemNetworkCheck "force" >/dev/null 2>&1
|
|
fi
|
|
networkScanConflicts
|
|
local remaining=${#NET_CONFLICTS[@]}
|
|
|
|
if (( remaining > 0 )); then
|
|
isError "Network heal attempted ${attempted} app(s); ${remaining} conflict(s) still detected — re-run or inspect manually."
|
|
return 1
|
|
fi
|
|
isSuccessful "Network heal complete — re-IP'd ${attempted} app(s); no conflicts remain."
|
|
}
|