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>
68 lines
3.1 KiB
Bash
68 lines
3.1 KiB
Bash
#!/bin/bash
|
|
|
|
# Read-only network-drift scan — the shared detection used by both the WebUI
|
|
# status generator (webuiSystemNetworkCheck) and the heal verb (network heal),
|
|
# so the two never diverge.
|
|
#
|
|
# networkScanConflicts sets these globals (call it DIRECTLY, never in $(...) —
|
|
# a subshell would drop the globals):
|
|
# NET_DAEMON_OK "true"/"false" — docker daemon reachable
|
|
# NET_PRESENT "true"/"false" — the shared network ($CFG_NETWORK_NAME) exists
|
|
# NET_DOCKER_SUBNET — its real subnet CIDR (e.g. 10.123.154.0/24)
|
|
# NET_SCAN_ERROR — human note when the daemon/network is off (else "")
|
|
# NET_CONFLICTS (array) — one "app|service|ip" entry per active IP that
|
|
# no longer falls inside the docker network's
|
|
# real subnet (the "network recreated with a
|
|
# different /24, app stranded" drift).
|
|
# Gateway-routed services (no live shared-net ipv4 in their deployed compose,
|
|
# e.g. gluetun-routed service-1) are skipped, so they don't false-positive.
|
|
#
|
|
# Nothing here mutates state.
|
|
|
|
# Is this app/service NOT live on the shared network? Routed via a gateway, or
|
|
# its ipv4 simply isn't present uncommented in the deployed compose -> skip it.
|
|
# We key on the IP (unique per service): a routed service has its whole
|
|
# `ipv4_address:` block commented out (GLUETUN_OFF region), so an uncommented
|
|
# assignment carrying this exact IP means it IS live on the shared net.
|
|
_netServiceIsRouted() {
|
|
local app="$1" ip="$2"
|
|
local compose="${containers_dir}${app}/docker-compose.yml"
|
|
[[ -f "$compose" ]] || return 1 # no compose to consult -> don't skip
|
|
local esc_ip="${ip//./\\.}"
|
|
grep -Eq "^[[:space:]]*ipv4_address:[[:space:]]*${esc_ip}([[:space:]]|#|$)" "$compose" && return 1
|
|
return 0
|
|
}
|
|
|
|
networkScanConflicts() {
|
|
NET_DAEMON_OK="false"; NET_PRESENT="false"; NET_DOCKER_SUBNET=""; NET_SCAN_ERROR=""
|
|
NET_CONFLICTS=()
|
|
|
|
# Distinguish "daemon down" (transient/benign — never alarm on what we can't
|
|
# verify) from "daemon up but our network is gone" (a real conflict).
|
|
if ! dockerCommandRun "docker info" >/dev/null 2>&1; then
|
|
NET_SCAN_ERROR="docker daemon unreachable"
|
|
return 0
|
|
fi
|
|
NET_DAEMON_OK="true"
|
|
|
|
NET_DOCKER_SUBNET=$(dockerCommandRun "docker network inspect $CFG_NETWORK_NAME --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'" 2>/dev/null | tr -d '[:space:]')
|
|
if [[ -z "$NET_DOCKER_SUBNET" ]]; then
|
|
NET_SCAN_ERROR="network '$CFG_NETWORK_NAME' not found"
|
|
return 0
|
|
fi
|
|
NET_PRESENT="true"
|
|
|
|
local rows app service ip
|
|
rows=$(runInstallOp sqlite3 "$docker_dir/$db_file" \
|
|
"SELECT app_name, service_name, resource_value FROM network_resources WHERE resource_type='ip' AND status='active';" 2>/dev/null)
|
|
[[ -z "$rows" ]] && return 0
|
|
|
|
while IFS='|' read -r app service ip; do
|
|
[[ -z "$app" || -z "$ip" ]] && continue
|
|
_netServiceIsRouted "$app" "$ip" && continue
|
|
if ! ipInSubnet "$ip" "$NET_DOCKER_SUBNET"; then
|
|
NET_CONFLICTS+=("${app}|${service}|${ip}")
|
|
fi
|
|
done <<< "$rows"
|
|
}
|