#!/bin/sh
PATH=/usr/sbin:/usr/bin:/sbin:/bin

IF=eth0
STATE_DIR=/var/run/net_tx_watchdog
STATE_FILE="$STATE_DIR/${IF}.state"
LOCK_FILE="$STATE_DIR/${IF}.lock"

# Tuning
TRIGGER_COUNT=3          # 3 consecutive bad checks
MIN_UPTIME_SECS=180      # ignore first 3 minutes after boot
COOLDOWN_SECS=300        # wait 5 min after a recovery before allowing another
LOGTAG="net-tx-watchdog"

mkdir -p "$STATE_DIR"

log_reset() {
    logger -t "$LOGTAG" "$*"
}

read_stat() {
    cat "/sys/class/net/$IF/statistics/$1" 2>/dev/null
}

read_carrier() {
    cat "/sys/class/net/$IF/carrier" 2>/dev/null
}

read_operstate() {
    cat "/sys/class/net/$IF/operstate" 2>/dev/null
}

has_ipv4() {
    ip -4 addr show dev "$IF" | grep -q 'inet '
}

now_epoch() {
    date +%s
}

boot_time_secs() {
    awk '{print int($1)}' /proc/uptime 2>/dev/null
}

load_state() {
    if [ -f "$STATE_FILE" ]; then
        . "$STATE_FILE"
    else
        PREV_TX=0
        PREV_RX=0
        PREV_TXE=0
        COUNT=0
        LAST_RESET=0
    fi
}

save_state() {
    cat > "$STATE_FILE" <<EOF
PREV_TX=$TX
PREV_RX=$RX
PREV_TXE=$TXE
COUNT=$COUNT
LAST_RESET=$LAST_RESET
EOF
}

recover_interface() {
    log_reset "suspected TX stall on $IF; carrier=$CARRIER oper=$OPER has_ipv4=$HAS_IP tx=$TX prev_tx=$PREV_TX rx=$RX prev_rx=$PREV_RX tx_err=$TXE prev_tx_err=$PREV_TXE"

    if command -v ifdown >/dev/null 2>&1 && command -v ifup >/dev/null 2>&1; then
        ifdown "$IF" >/dev/null 2>&1 || true
        sleep 2
        ifup "$IF" >/dev/null 2>&1 || true
    else
        ip link set dev "$IF" down >/dev/null 2>&1 || true
        sleep 2
        ip link set dev "$IF" up >/dev/null 2>&1 || true
    fi

    LAST_RESET=$(now_epoch)
    COUNT=0
    save_state
    log_reset "recovery attempted on $IF"
}

# prevent overlapping cron runs
if [ -e "$LOCK_FILE" ]; then
    OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null)
    if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
        exit 0
    fi
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT INT TERM

TX=$(read_stat tx_packets)
RX=$(read_stat rx_packets)
TXE=$(read_stat tx_errors)
CARRIER=$(read_carrier)
OPER=$(read_operstate)

[ -z "$TX" ] && exit 0
[ -z "$RX" ] && exit 0
[ -z "$TXE" ] && exit 0
[ -z "$CARRIER" ] && exit 0
[ -z "$OPER" ] && exit 0

HAS_IP=0
has_ipv4 && HAS_IP=1

load_state

NOW=$(now_epoch)
UPTIME=$(boot_time_secs)

# Safeguard 1: ignore early boot period
if [ "$UPTIME" -lt "$MIN_UPTIME_SECS" ]; then
    PREV_TX=$TX
    PREV_RX=$RX
    PREV_TXE=$TXE
    COUNT=0
    save_state
    exit 0
fi

# Safeguard 2: cooldown after a reset
if [ $((NOW - LAST_RESET)) -lt "$COOLDOWN_SECS" ]; then
    PREV_TX=$TX
    PREV_RX=$RX
    PREV_TXE=$TXE
    COUNT=0
    save_state
    exit 0
fi

TX_STALLED=0
RX_MOVING=0
TXE_MOVING=0

[ "$TX" = "$PREV_TX" ] && TX_STALLED=1
[ "$RX" != "$PREV_RX" ] && RX_MOVING=1
[ "$TXE" != "$PREV_TXE" ] && TXE_MOVING=1

STALL_NOW=0

# Only count as a likely stall if:
# - link still exists
# - interface is up/unknown
# - TX is not moving
# - and either RX is still moving, or TX errors increased, or IPv4 disappeared
if [ "$CARRIER" = "1" ] && { [ "$OPER" = "up" ] || [ "$OPER" = "unknown" ]; } && [ "$TX_STALLED" = "1" ]; then
    if [ "$RX_MOVING" = "1" ] || [ "$TXE_MOVING" = "1" ] || [ "$HAS_IP" = "0" ]; then
        STALL_NOW=1
    fi
fi

# avoid false positives when system is just idle
if [ "$TX_STALLED" = "1" ] && [ "$RX_MOVING" = "0" ] && [ "$TXE_MOVING" = "0" ] && [ "$HAS_IP" = "1" ]; then
    STALL_NOW=0
fi

if [ "$STALL_NOW" = "1" ]; then
    COUNT=$((COUNT + 1))
else
    COUNT=0
fi

if [ "$COUNT" -ge "$TRIGGER_COUNT" ]; then
    recover_interface
else
    save_state
fi

exit 0