So what is the contents of /jffs/scripts/ipv6-watchdog.sh ?
# #!/bin/sh
# IPv6 Watchdog Script for Asus GT-AXE16000 / Merlin / Telus FTTH
# Monitors odhcp6c and IPv6 default route, restarts if either is dead
# Logs to /tmp/ipv6_watchdog.log
LOGFILE="/tmp/ipv6_watchdog.log"
PIDFILE="/var/run/odhcp6c.eth0.pid"
MAX_LOG_LINES=200
# The exact command Merlin uses to start odhcp6c - captured from ps output
# -d = daemonize (run in background)
# -f = don't send Client FQDN option
# -R = don't request any options except those specified with -r
# -s = script to call when state changes (/tmp/dhcp6c = symlink to /sbin/rc)
# -N try = request a new address but don't fail if server won't assign one
# -c = client DUID (your router's unique identifier to Telus)
# -P 56:1b558 = request a /56 prefix delegation, interface ID 1b558
# -r23 = request option 23 (DNS servers)
# -r24 = request option 24 (domain list)
# -k = don't send RELEASE when stopping
ODHCP6C_CMD="odhcp6c -df -R -s /tmp/dhcp6c -N try -c 00030001a036bc71b554 -P 56:1b558 -r23 -r24 -k eth0"
# Simple logging function - adds timestamp to every log line
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOGFILE"
}
# Keep log file from growing forever - trim to last MAX_LOG_LINES lines
trim_log() {
if [ -f "$LOGFILE" ]; then
lines=$(wc -l < "$LOGFILE")
if [ "$lines" -gt "$MAX_LOG_LINES" ]; then
tail -n "$MAX_LOG_LINES" "$LOGFILE" > "${LOGFILE}.tmp"
mv "${LOGFILE}.tmp" "$LOGFILE"
fi
fi
}
# Check if odhcp6c process is actually running
# Reads PID from pidfile, then checks if that PID exists in /proc/
is_odhcp6c_running() {
if [ ! -f "$PIDFILE" ]; then
return 1 # no pidfile = not running
fi
pid=$(cat "$PIDFILE" 2>/dev/null)
if [ -z "$pid" ]; then
return 1 # empty pidfile = not running
fi
if [ -f "/proc/${pid}/cmdline" ]; then
return 0 # PID exists in /proc/ = running
fi
return 1 # PID not in /proc/ = dead
}
# Check if a valid IPv6 default route exists via eth0
# This is what actually routes traffic to the internet
has_ipv6_default_route() {
ip -6 route show | grep -q "^default.*dev eth0"
}
# Check if br0 has a global (non-link-local) IPv6 address
# Link-local starts with fe80, global starts with 2001 etc
has_ipv6_global_addr() {
ip -6 addr show br0 | grep -q "scope global"
}
# Main restart function - kills any stale odhcp6c, cleans up, starts fresh
restart_ipv6() {
log "ACTION: Restarting odhcp6c..."
# Kill any existing odhcp6c processes (stale or zombie)
killall odhcp6c 2>/dev/null
sleep 2
# Clean up stale pidfile if it exists
rm -f "$PIDFILE"
# Start odhcp6c with the exact Merlin command
$ODHCP6C_CMD
# Wait a moment for it to start and write its pidfile
sleep 3
# Confirm it actually started
if is_odhcp6c_running; then
log "ACTION: odhcp6c restarted successfully (PID: $(cat $PIDFILE))"
else
log "ERROR: odhcp6c failed to start - manual intervention may be needed"
fi
}
# ---- MAIN LOGIC ----
trim_log
# Check odhcp6c process status
if ! is_odhcp6c_running; then
log "ALERT: odhcp6c is not running"
restart_ipv6
else
# odhcp6c is running, but also verify IPv6 is actually working
if ! has_ipv6_default_route; then
log "ALERT: odhcp6c running but no IPv6 default route - restarting"
restart_ipv6
elif ! has_ipv6_global_addr; then
log "ALERT: odhcp6c running but br0 has no global IPv6 address - restarting"
restart_ipv6
else
log "OK: IPv6 healthy - odhcp6c PID $(cat $PIDFILE), route and address present"
fi
fi