LibrePortal/scripts/docker/network/network_conflicts.sh
librelad 20f8ca2eb5 feat(network): detect + heal apps stranded off the docker subnet
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>
2026-06-02 16:03:53 +01:00

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"
}