Files
S905L3A/watchdog/network-watchdog.sh
2026-06-28 20:56:20 +08:00

532 lines
15 KiB
Bash

#!/bin/bash
#
# Network watchdog for the CUPS print server.
# Default behavior: when connectivity checks fail repeatedly, switch the
# configured interface back to DHCP so the box can regain network access.
# If DHCP reveals a different gateway, the script can rebuild the static IP
# with the new network prefix while keeping the old host octet.
#
# Edit /etc/cups-watchdog/network-watchdog.conf to set STATIC_IP,
# STATIC_PREFIX, STATIC_GATEWAY and STATIC_DNS. You can also run:
# /opt/cups-watchdog/network-watchdog.sh static
# /opt/cups-watchdog/network-watchdog.sh dhcp
#
set -u
CONFIG_FILE="${CUPS_NETWORK_WATCHDOG_CONFIG:-/etc/cups-watchdog/network-watchdog.conf}"
STATE_DIR="/run/cups-watchdog"
INTERFACE=""
STATIC_IP=""
STATIC_PREFIX="24"
STATIC_GATEWAY=""
STATIC_DNS="114.114.114.114 223.5.5.5"
STATIC_HOST_OCTET=""
PING_TARGETS="223.5.5.5 114.114.114.114"
FAIL_THRESHOLD=3
DHCP_AFTER_FAILURE=1
REBASE_STATIC_ON_GATEWAY_CHANGE=1
REBASE_WAIT_SECONDS=20
LOG_FILE="/var/log/cups-watchdog/network.log"
MAX_LOG_SIZE_KB=256
if [ -f "$CONFIG_FILE" ]; then
# shellcheck disable=SC1090
. "$CONFIG_FILE"
fi
mkdir -p "$STATE_DIR"
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
acquire_lock() {
local lock_dir="$STATE_DIR/network.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
echo "$(date '+%Y-%m-%d %H:%M:%S') [network-watchdog] another run is active"
exit 0
fi
trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT
}
rotate_log() {
local max_bytes=$((MAX_LOG_SIZE_KB * 1024))
if [ -f "$LOG_FILE" ] && [ "$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0)" -gt "$max_bytes" ]; then
mv "$LOG_FILE" "$LOG_FILE.1" 2>/dev/null || true
: > "$LOG_FILE"
fi
}
log_msg() {
local msg="$1"
local line
rotate_log
line="$(date '+%Y-%m-%d %H:%M:%S') [network-watchdog] $msg"
echo "$line"
echo "$line" >> "$LOG_FILE" 2>/dev/null || true
}
detect_interface() {
if [ -n "${INTERFACE:-}" ] && ip link show "$INTERFACE" >/dev/null 2>&1; then
echo "$INTERFACE"
return 0
fi
ip route show default 2>/dev/null | awk '{print $5; exit}'
}
current_ipv4_for_interface() {
local iface="$1"
ip -4 -o addr show dev "$iface" scope global 2>/dev/null | awk '{split($4, a, "/"); print a[1]; exit}'
}
current_prefix_for_interface() {
local iface="$1"
ip -4 -o addr show dev "$iface" scope global 2>/dev/null | awk '{split($4, a, "/"); print a[2]; exit}'
}
current_gateway_for_interface() {
local iface="$1"
local gateway
gateway="$(ip route show default dev "$iface" 2>/dev/null | awk '{print $3; exit}')"
if [ -z "$gateway" ]; then
gateway="$(ip route show default 2>/dev/null | awk '{print $3; exit}')"
fi
echo "$gateway"
}
last_octet() {
local ip="$1"
case "$ip" in
*.*.*.*) echo "${ip##*.}" ;;
*) echo "" ;;
esac
}
prefix24() {
local ip="$1"
case "$ip" in
*.*.*.*) echo "${ip%.*}" ;;
*) echo "" ;;
esac
}
valid_host_octet() {
local octet="$1"
case "$octet" in
''|*[!0-9]*) return 1 ;;
esac
[ "$octet" -ge 1 ] 2>/dev/null && [ "$octet" -le 254 ] 2>/dev/null
}
nm_connection_for_interface() {
local iface="$1"
nmcli -t -f NAME,DEVICE con show --active 2>/dev/null | awk -F: -v iface="$iface" '$2 == iface {print $1; exit}'
}
configure_dhcp_nmcli() {
local iface="$1"
local conn
conn="$(nm_connection_for_interface "$iface")"
[ -n "$conn" ] || conn="$iface"
nmcli con mod "$conn" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns "" >/dev/null
nmcli con down "$conn" >/dev/null 2>&1 || true
nmcli con up "$conn" >/dev/null
}
configure_dhcp_netplan() {
local iface="$1"
local target_file="/etc/netplan/99-cups-watchdog-dhcp.yaml"
cat > "$target_file" << EOF
# DHCP recovery config generated by cups network watchdog.
network:
version: 2
renderer: networkd
ethernets:
$iface:
dhcp4: yes
EOF
chmod 600 "$target_file"
rm -f /etc/netplan/01-static-ip.yaml /etc/netplan/99-cups-static-ip.yaml /etc/netplan/99-cups-watchdog-static.yaml 2>/dev/null || true
netplan apply
}
configure_dhcp_interfaces() {
local iface="$1"
if [ -f /etc/network/interfaces ]; then
cp /etc/network/interfaces /etc/network/interfaces.cups-watchdog.bak
fi
cat > /etc/network/interfaces << EOF
# DHCP recovery config generated by cups network watchdog.
auto lo
iface lo inet loopback
auto $iface
iface $iface inet dhcp
EOF
systemctl restart networking 2>/dev/null || /etc/init.d/networking restart 2>/dev/null || true
}
configure_dhcp() {
local iface
iface="$(detect_interface)"
if [ -z "$iface" ]; then
log_msg "cannot switch to DHCP: network interface not found"
return 1
fi
log_msg "switching $iface to DHCP"
if command -v nmcli >/dev/null 2>&1 && systemctl is-active --quiet NetworkManager 2>/dev/null; then
configure_dhcp_nmcli "$iface"
elif [ -d /etc/netplan ] && command -v netplan >/dev/null 2>&1; then
configure_dhcp_netplan "$iface"
else
configure_dhcp_interfaces "$iface"
fi
}
set_config_value() {
local key="$1"
local value="$2"
local tmp_file
if [ ! -f "$CONFIG_FILE" ]; then
mkdir -p "$(dirname "$CONFIG_FILE")" 2>/dev/null || true
: > "$CONFIG_FILE" 2>/dev/null || return 1
fi
tmp_file="${CONFIG_FILE}.tmp.$$"
if grep -q "^${key}=" "$CONFIG_FILE" 2>/dev/null; then
awk -v key="$key" -v value="$value" '
BEGIN { q = sprintf("%c", 34) }
$0 ~ "^" key "=" { print key "=" q value q; next }
{ print }
' "$CONFIG_FILE" > "$tmp_file" && mv "$tmp_file" "$CONFIG_FILE"
else
printf '%s="%s"\n' "$key" "$value" >> "$CONFIG_FILE"
fi
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
}
wait_for_dhcp_network() {
local iface="$1"
local i=0
local ip
local gateway
while [ "$i" -lt "${REBASE_WAIT_SECONDS:-20}" ]; do
ip="$(current_ipv4_for_interface "$iface")"
gateway="$(current_gateway_for_interface "$iface")"
if [ -n "$ip" ] && [ -n "$gateway" ]; then
echo "$ip $gateway"
return 0
fi
i=$((i + 1))
sleep 1
done
return 1
}
candidate_ip_in_use() {
local iface="$1"
local candidate_ip="$2"
local current_ip="$3"
[ "$candidate_ip" = "$current_ip" ] && return 1
if command -v arping >/dev/null 2>&1; then
# arping duplicate-address detection returns success when no reply is seen.
arping -D -I "$iface" -c 2 -w 3 "$candidate_ip" >/dev/null 2>&1
[ "$?" != "0" ] && return 0
return 1
fi
if ping -c 1 -W 1 "$candidate_ip" >/dev/null 2>&1; then
return 0
fi
return 1
}
configure_static_nmcli() {
local iface="$1"
local conn
conn="$(nm_connection_for_interface "$iface")"
[ -n "$conn" ] || conn="$iface"
nmcli con mod "$conn" ipv4.method manual ipv4.addresses "$STATIC_IP/$STATIC_PREFIX" ipv4.gateway "$STATIC_GATEWAY" ipv4.dns "$STATIC_DNS" >/dev/null
nmcli con down "$conn" >/dev/null 2>&1 || true
nmcli con up "$conn" >/dev/null
}
configure_static_netplan() {
local iface="$1"
local target_file="/etc/netplan/99-cups-watchdog-static.yaml"
local dns_list=""
local d
for d in $STATIC_DNS; do
dns_list="$dns_list, $d"
done
dns_list="${dns_list#, }"
cat > "$target_file" << EOF
# Static network config generated by cups network watchdog.
network:
version: 2
renderer: networkd
ethernets:
$iface:
dhcp4: no
addresses:
- $STATIC_IP/$STATIC_PREFIX
routes:
- to: default
via: $STATIC_GATEWAY
nameservers:
addresses: [$dns_list]
EOF
chmod 600 "$target_file"
rm -f /etc/netplan/01-dhcp.yaml /etc/netplan/99-cups-dhcp.yaml /etc/netplan/99-cups-watchdog-dhcp.yaml 2>/dev/null || true
netplan apply
}
configure_static_interfaces() {
local iface="$1"
local netmask="255.255.255.0"
case "$STATIC_PREFIX" in
8) netmask="255.0.0.0" ;;
16) netmask="255.255.0.0" ;;
24) netmask="255.255.255.0" ;;
25) netmask="255.255.255.128" ;;
26) netmask="255.255.255.192" ;;
27) netmask="255.255.255.224" ;;
28) netmask="255.255.255.240" ;;
29) netmask="255.255.255.248" ;;
30) netmask="255.255.255.252" ;;
esac
if [ -f /etc/network/interfaces ]; then
cp /etc/network/interfaces /etc/network/interfaces.cups-watchdog.bak
fi
cat > /etc/network/interfaces << EOF
# Static network config generated by cups network watchdog.
auto lo
iface lo inet loopback
auto $iface
iface $iface inet static
address $STATIC_IP
netmask $netmask
gateway $STATIC_GATEWAY
dns-nameservers $STATIC_DNS
EOF
systemctl restart networking 2>/dev/null || /etc/init.d/networking restart 2>/dev/null || true
}
configure_static() {
local iface
iface="$(detect_interface)"
if [ -z "$iface" ]; then
log_msg "cannot switch to static: network interface not found"
return 1
fi
if [ -z "${STATIC_IP:-}" ] || [ -z "${STATIC_GATEWAY:-}" ]; then
log_msg "cannot switch to static: STATIC_IP or STATIC_GATEWAY is empty in $CONFIG_FILE"
return 1
fi
log_msg "switching $iface to static $STATIC_IP/$STATIC_PREFIX"
if command -v nmcli >/dev/null 2>&1 && systemctl is-active --quiet NetworkManager 2>/dev/null; then
configure_static_nmcli "$iface"
elif [ -d /etc/netplan ] && command -v netplan >/dev/null 2>&1; then
configure_static_netplan "$iface"
else
configure_static_interfaces "$iface"
fi
}
rebase_static_after_dhcp() {
local iface="$1"
local dhcp_state
local dhcp_ip
local dhcp_gateway
local dhcp_prefix
local host_octet
local base_prefix
local candidate_ip
local old_static_ip="$STATIC_IP"
local old_static_gateway="$STATIC_GATEWAY"
local old_static_prefix="$STATIC_PREFIX"
local old_static_dns="$STATIC_DNS"
local current_dns
[ "${REBASE_STATIC_ON_GATEWAY_CHANGE:-1}" = "1" ] || return 0
if [ -z "${STATIC_IP:-}" ] || [ -z "${STATIC_GATEWAY:-}" ]; then
log_msg "skip static rebase: STATIC_IP or STATIC_GATEWAY is empty"
return 0
fi
dhcp_state="$(wait_for_dhcp_network "$iface" 2>/dev/null || true)"
if [ -z "$dhcp_state" ]; then
log_msg "skip static rebase: DHCP did not provide an IP and gateway in time"
return 0
fi
dhcp_ip="$(echo "$dhcp_state" | awk '{print $1}')"
dhcp_gateway="$(echo "$dhcp_state" | awk '{print $2}')"
if [ "$dhcp_gateway" = "$STATIC_GATEWAY" ]; then
log_msg "DHCP gateway is still $dhcp_gateway; keep DHCP recovery without rebasing static IP"
return 0
fi
host_octet="${STATIC_HOST_OCTET:-$(last_octet "$STATIC_IP")}"
if ! valid_host_octet "$host_octet"; then
log_msg "skip static rebase: invalid STATIC_HOST_OCTET '$host_octet'"
return 0
fi
base_prefix="$(prefix24 "$dhcp_ip")"
[ -n "$base_prefix" ] || base_prefix="$(prefix24 "$dhcp_gateway")"
if [ -z "$base_prefix" ]; then
log_msg "skip static rebase: cannot derive IPv4 /24 prefix from DHCP state"
return 0
fi
candidate_ip="${base_prefix}.${host_octet}"
if candidate_ip_in_use "$iface" "$candidate_ip" "$dhcp_ip"; then
log_msg "skip static rebase: candidate $candidate_ip appears to be in use; staying on DHCP"
return 0
fi
dhcp_prefix="$(current_prefix_for_interface "$iface")"
STATIC_IP="$candidate_ip"
STATIC_GATEWAY="$dhcp_gateway"
STATIC_PREFIX="${dhcp_prefix:-$STATIC_PREFIX}"
current_dns="$(grep -E '^nameserver ' /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -n2 | tr '\n' ' ')"
STATIC_DNS="${current_dns:-$STATIC_DNS}"
log_msg "gateway changed from $old_static_gateway to $STATIC_GATEWAY; rebasing static IP from $old_static_ip to $STATIC_IP/$STATIC_PREFIX"
configure_static
sleep 3
if connectivity_ok; then
set_config_value "STATIC_IP" "$STATIC_IP" || true
set_config_value "STATIC_GATEWAY" "$STATIC_GATEWAY" || true
set_config_value "STATIC_PREFIX" "$STATIC_PREFIX" || true
set_config_value "STATIC_DNS" "$STATIC_DNS" || true
set_config_value "STATIC_HOST_OCTET" "$host_octet" || true
set_config_value "PING_TARGETS" "$STATIC_GATEWAY 223.5.5.5 114.114.114.114" || true
log_msg "static rebase succeeded: $STATIC_IP via $STATIC_GATEWAY"
return 0
fi
STATIC_IP="$old_static_ip"
STATIC_GATEWAY="$old_static_gateway"
STATIC_PREFIX="$old_static_prefix"
STATIC_DNS="$old_static_dns"
log_msg "static rebase to $candidate_ip failed connectivity check; falling back to DHCP"
configure_dhcp
}
recover_after_failure() {
local iface
local fallback_iface
fallback_iface="$(detect_interface)"
configure_dhcp || return 1
iface="$(detect_interface)"
[ -n "$iface" ] || iface="$fallback_iface"
if [ -z "$iface" ]; then
log_msg "skip static rebase: network interface not found after DHCP recovery"
return 1
fi
rebase_static_after_dhcp "$iface"
}
connectivity_ok() {
local iface
local gateway
local target
iface="$(detect_interface)"
if [ -n "$iface" ] && [ -f "/sys/class/net/$iface/carrier" ]; then
if [ "$(cat "/sys/class/net/$iface/carrier" 2>/dev/null || echo 0)" != "1" ]; then
return 1
fi
fi
gateway="$(ip route show default 2>/dev/null | awk '{print $3; exit}')"
if [ -n "$gateway" ] && ping -c 1 -W 2 "$gateway" >/dev/null 2>&1; then
return 0
fi
for target in $PING_TARGETS; do
[ -n "$target" ] || continue
if ping -c 1 -W 2 "$target" >/dev/null 2>&1; then
return 0
fi
done
return 1
}
check_network() {
local count_file="$STATE_DIR/network.fail_count"
local fail_count=0
if connectivity_ok; then
echo 0 > "$count_file"
log_msg "connectivity ok"
return 0
fi
if [ -f "$count_file" ]; then
fail_count="$(cat "$count_file" 2>/dev/null || echo 0)"
fi
fail_count=$((fail_count + 1))
echo "$fail_count" > "$count_file"
log_msg "connectivity failed ($fail_count/$FAIL_THRESHOLD)"
if [ "$fail_count" -ge "$FAIL_THRESHOLD" ] && [ "$DHCP_AFTER_FAILURE" = "1" ]; then
recover_after_failure
echo 0 > "$count_file"
fi
}
case "${1:-check}" in
check)
acquire_lock
check_network
;;
dhcp)
acquire_lock
configure_dhcp
;;
static)
acquire_lock
configure_static
;;
*)
echo "Usage: $0 [check|dhcp|static]"
exit 2
;;
esac