#!/bin/dash
# Recover eth0 from the lockup seen with the stmmac driver, where the hub
# gets no unicast replies while multicast (e.g. IPv6 router advertisements)
# still arrives: either eth0 stops accepting unicast, or it sends corrupted
# frames that the router drops.
#
# Recovery steps are tried from least to most invasive, and the script
# stops at the first one that works, so the log tells which part of the
# NIC state was broken. Output goes to multilog (via fh-network) and to
# syslog, which is included in support log bundles. ethtool is not on all
# hubs, so the fh-network daemon logs the driver counters itself.
#
# Runs as root via sudo. Root only acts on values it reads from the kernel
# itself (default route, sysfs MAC); output from commands run through drop
# could be tampered with by another fh-network process, so it is only ever
# piped into a dropped logger and never parsed as root.
#
# Exit status, used by fh-network to decide when to reboot (sudo itself
# uses 1, so that is avoided):
#   0   connectivity restored
#   10  recovery failed, gateway still unreachable
#   11  gateway reachable, fault is further upstream
#   12  no carrier (cable unplugged, router off)

EXIT_FIXED=0
EXIT_FAILED=10
EXIT_UPSTREAM=11
EXIT_NO_CARRIER=12

IF=eth0
# Only valid for the RK3328 GMAC; reading a physical address that is not
# mapped can hang or crash the hub, so the registers are only read when
# eth0 is that device.
GMAC_BASE=0xff550000
GMAC_DEVICE=ff550000.ethernet

# Some hubs only have devmem as a busybox applet.
if command -v devmem >/dev/null 2>&1; then
	DEVMEM=devmem
elif busybox --list 2>/dev/null | grep -qx devmem; then
	DEVMEM="busybox devmem"
else
	DEVMEM=
fi

drop()
{
	fh-drop fh-network "$@"
}

# -s also writes to stderr, which ends up in the multilog log.
log()
{
	drop logger -s -t fh-network -- "$*"
}

gateway()
{
	ip -4 route show default dev "$IF" 2>/dev/null | awk '{ print $3; exit }'
}

# Needs a unicast reply (ARP + ICMP), which is exactly what fails.
# ping is not always setuid or cap_net_raw, so it runs as root.
check()
{
	target=$(gateway)
	[ -n "$target" ] || target=1.1.1.1
	ping -c 3 -W 2 -q -I "$IF" "$target" >/dev/null 2>&1
}

snapshot()
{
	log "snapshot ($1):"
	{
		echo "--- ip -s link"
		drop ip -s link show dev "$IF"
		echo "--- ip addr"
		drop ip addr show dev "$IF"
		echo "--- ip neigh"
		drop ip neigh show dev "$IF"
		# dwmac1000 MAC address 0 high/low registers; compare with ip link
		if [ -n "$DEVMEM" ] && [ "$(basename "$(readlink -f /sys/class/net/$IF/device)")" = "$GMAC_DEVICE" ]; then
			echo "--- GMAC_ADDR_HIGH(0) $($DEVMEM $((GMAC_BASE + 0x40)) 32)"
			echo "--- GMAC_ADDR_LOW(0)  $($DEVMEM $((GMAC_BASE + 0x44)) 32)"
		fi
		echo "--- dmesg"
		dmesg | drop tail -n 50
	} 2>&1 | drop logger -s -t fh-network
}

fixed()
{
	log "connectivity restored by: $1"
	snapshot "after $1"
	exit $EXIT_FIXED
}

log "network check failed, starting recovery (gateway: $(gateway))"
snapshot "before recovery"

if [ "$(cat /sys/class/net/$IF/carrier 2>/dev/null)" != 1 ]; then
	log "no carrier on $IF, check cable and router; not touching $IF"
	exit $EXIT_NO_CARRIER
fi

if check; then
	log "gateway reachable, fault is further upstream; not touching $IF"
	exit $EXIT_UPSTREAM
fi

# 1. Promiscuous mode bypasses the unicast address filter.
ip link set dev "$IF" promisc on
sleep 2
if check; then
	log "promisc on fixed it: unicast RX filter is broken"
	ip link set dev "$IF" promisc off
	sleep 2
	if check; then
		fixed "promisc toggle"
	fi
	log "broken again after promisc off, continuing"
else
	ip link set dev "$IF" promisc off
fi

# 2. Rewrite the MAC address register only.
ip link set dev "$IF" address "$(cat /sys/class/net/$IF/address)"
sleep 2
if check; then
	fixed "MAC address rewrite"
fi

# 3. TX checksum offload off: the NIC may be sending frames with bad
# checksums, which the router drops. If it is broken again once offload
# is back on, keep it off; the next boot turns it back on.
txcsum()
{
	fh-network-txcsum "$IF" "$1" 2>&1 | drop logger -s -t fh-network
}

# Already off (e.g. turned off by hand to test): toggling cannot help,
# and turning it back on would undo that.
if fh-network-txcsum "$IF" 2>/dev/null | grep -q ' off$'; then
	log "TX checksum offload already off, skipping"
elif txcsum off; sleep 2; check; then
	log "TX checksum offload off fixed it: hub was sending bad checksums"
	txcsum on
	sleep 2
	if check; then
		fixed "TX checksum offload toggle"
	fi
	txcsum off
	sleep 2
	if check; then
		fixed "TX checksum offload off (kept off until reboot)"
	fi
	log "broken again with TX checksum offload kept off, continuing"
else
	txcsum on
fi

# 4. Full link cycle: stmmac close/open resets DMA and reprograms the MAC.
ip link set dev "$IF" down
sleep 2
ip link set dev "$IF" up
if command -v dhcpcd >/dev/null 2>&1; then
	dhcpcd -n "$IF"
elif command -v dhclient >/dev/null 2>&1; then
	dhclient -r "$IF"
	dhclient -nw "$IF"
fi

i=0
while [ $i -lt 30 ]; do
	sleep 2
	if check; then
		fixed "link down/up"
	fi
	i=$((i + 2))
done

log "recovery failed, $IF still unable to reach gateway"
snapshot "after failed recovery"
exit $EXIT_FAILED
