#!/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" }