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