#!/bin/sh

# Discover active IPv6 clients behind every NDP Proxy downstream interface,
# publish their /128 on the ISP-facing link, and keep proxy state consistent.
#
# Public template: replace both IID placeholders and review every interface and
# PF table before installation. The critical IID is excluded because the
# companion ipv6-ndp-watchdog service owns its deterministic recovery path.

WAN_IF="vtnet0"
DOWNSTREAM_IFS="vtnet1 vtnet2"
CRITICAL_IID="REPLACE_WITH_CRITICAL_CLIENT_INTERFACE_ID"
ONT_IID="REPLACE_WITH_ONT_LAN_INTERFACE_ID"
PF_GLOBAL="ndp_proxy_global"
PF_INTERFACE_TABLES="vtnet1:ndp_proxy_lan"
STATE_DIR="/var/db/ipv6-ndp-watchdog"
STATE_FILE="${STATE_DIR}/clients-seeded"
LOCK_DIR="/var/run/ipv6-ndp-watchdog.lock"
INTERVAL=30
SEED_TTL=3600
BATCH_SIZE=32
TAG="ipv6-ndp-client-watchdog"

locked=0
active_addr=""
active_iface=""
wan_nodad=0

log()
{
    /usr/bin/logger -t "${TAG}" -- "$*"
}

validate_config()
{
    case "${CRITICAL_IID}:${ONT_IID}" in
        *REPLACE_WITH*)
            echo "replace CRITICAL_IID and ONT_IID before starting this service" >&2
            return 1
            ;;
    esac
}

current_prefix()
{
    /sbin/ifconfig "${WAN_IF}" inet6 2>/dev/null | /usr/bin/awk '
        $1 == "inet6" && $2 !~ /^fe80:/ && /autoconf/ &&
        !/deprecated/ && !/detached/ && !/tentative/ && !/duplicated/ {
            split($2, part, ":")
            if (part[1] != "" && part[2] != "" && part[3] != "" && part[4] != "") {
                print part[1] ":" part[2] ":" part[3] ":" part[4]
                exit
            }
        }
    '
}

interface_link_local()
{
    /sbin/ifconfig "$1" inet6 2>/dev/null | /usr/bin/awk '
        $1 == "inet6" && $2 ~ /^fe80:/ { print $2; exit }
    '
}

interface_pf_table()
{
    target_iface="$1"
    for mapping in ${PF_INTERFACE_TABLES}; do
        mapping_iface="${mapping%%:*}"
        mapping_table="${mapping#*:}"
        if [ "${mapping_iface}" = "${target_iface}" ]; then
            printf '%s\n' "${mapping_table}"
            return 0
        fi
    done
    return 1
}

is_downstream_iface()
{
    for iface in ${DOWNSTREAM_IFS}; do
        [ "$1" = "${iface}" ] && return 0
    done
    return 1
}

route_interface()
{
    /usr/bin/netstat -rn -f inet6 | /usr/bin/awk -v addr="$1" '
        $1 == addr { print $NF; exit }
    '
}

route_present_on()
{
    /usr/bin/netstat -rn -f inet6 | /usr/bin/awk -v addr="$1" -v iface="$2" '
        $1 == addr && $NF == iface { found = 1 }
        END { exit(found ? 0 : 1) }
    '
}

table_contains()
{
    /sbin/pfctl -t "$1" -T show 2>/dev/null | /usr/bin/awk -v addr="$2" '
        $1 == addr { found = 1 }
        END { exit(found ? 0 : 1) }
    '
}

ensure_proxy_tables()
{
    addr="$1"
    iface="$2"
    /sbin/pfctl -t "${PF_GLOBAL}" -T add "${addr}" >/dev/null 2>&1 || true
    iface_table="$(interface_pf_table "${iface}" || true)"
    if [ -n "${iface_table}" ]; then
        /sbin/pfctl -t "${iface_table}" -T add "${addr}" >/dev/null 2>&1 || true
    fi
}

addr_is_local()
{
    /sbin/ifconfig -a inet6 2>/dev/null | /usr/bin/awk -v addr="$1" '
        $1 == "inet6" && $2 == addr { found = 1 }
        END { exit(found ? 0 : 1) }
    '
}

client_healthy()
{
    addr="$1"
    iface="$2"
    route_present_on "${addr}" "${iface}" || return 1
    table_contains "${PF_GLOBAL}" "${addr}" || return 1
    iface_table="$(interface_pf_table "${iface}" || true)"
    if [ -n "${iface_table}" ]; then
        table_contains "${iface_table}" "${addr}" || return 1
    fi
    return 0
}

seed_is_fresh()
{
    addr="$1"
    iface="$2"
    now="$(/bin/date +%s)"
    [ -r "${STATE_FILE}" ] || return 1
    /usr/bin/awk -v now="${now}" -v ttl="${SEED_TTL}" -v addr="${addr}" -v iface="${iface}" '
        $2 == addr && $3 == iface && (now - $1) < ttl { fresh = 1 }
        END { exit(fresh ? 0 : 1) }
    ' "${STATE_FILE}"
}

mark_seeded()
{
    addr="$1"
    iface="$2"
    now="$(/bin/date +%s)"
    /bin/mkdir -p "${STATE_DIR}"
    /bin/chmod 700 "${STATE_DIR}"
    tmp="${STATE_FILE}.$$"
    (umask 077
        if [ -r "${STATE_FILE}" ]; then
            /usr/bin/awk -v addr="${addr}" '$2 != addr' "${STATE_FILE}"
        fi
        printf '%s %s %s\n' "${now}" "${addr}" "${iface}"
    ) > "${tmp}"
    /bin/mv "${tmp}" "${STATE_FILE}"
}

candidate_addrs()
{
    prefix="$1"
    {
        /sbin/pfctl -ss 2>/dev/null | /usr/bin/tr ' ' '\n' | /usr/bin/sed -n \
            "s/^\\(${prefix}:[0-9A-Fa-f:]*\\)\\[[0-9][0-9]*\\]$/\\1/p"
        /sbin/pfctl -t "${PF_GLOBAL}" -T show 2>/dev/null
        /usr/bin/netstat -rn -f inet6 2>/dev/null | /usr/bin/awk \
            -v prefix="${prefix}:" -v ifaces=" ${DOWNSTREAM_IFS} " \
            'index($1, prefix) == 1 && index(ifaces, " " $NF " ") { print $1 }'
        /usr/sbin/ndp -an 2>/dev/null | /usr/bin/awk \
            -v prefix="${prefix}:" -v ifaces=" ${DOWNSTREAM_IFS} " \
            'index($1, prefix) == 1 && index(ifaces, " " $3 " ") { print $1 }'
    } | /usr/bin/awk -v prefix="${prefix}:" 'index($1, prefix) == 1 { print $1 }' | \
        /usr/bin/sort -u | /usr/bin/head -n 256
}

locate_client()
{
    addr="$1"
    iface="$(route_interface "${addr}")"
    if is_downstream_iface "${iface}"; then
        printf '%s\n' "${iface}"
        return 0
    fi

    iface="$(/usr/sbin/ndp -an 2>/dev/null | /usr/bin/awk -v addr="${addr}" \
        -v ifaces=" ${DOWNSTREAM_IFS} " '$1 == addr && index(ifaces, " " $3 " ") { print $3; exit }')"
    if is_downstream_iface "${iface}"; then
        printf '%s\n' "${iface}"
        return 0
    fi

    for iface in ${DOWNSTREAM_IFS}; do
        source_ll="$(interface_link_local "${iface}")"
        [ -n "${source_ll}" ] || continue
        /sbin/route delete -inet6 "${addr}" >/dev/null 2>&1 || true
        /sbin/route add -inet6 "${addr}" -iface "${iface}" >/dev/null 2>&1 || continue
        if /sbin/ping6 -c 1 -W 1500 -S "${source_ll}" "${addr}" >/dev/null 2>&1; then
            printf '%s\n' "${iface}"
            return 0
        fi
        /sbin/route delete -inet6 "${addr}" >/dev/null 2>&1 || true
    done
    return 1
}

acquire_lock()
{
    if /bin/mkdir "${LOCK_DIR}" 2>/dev/null; then
        locked=1
        return 0
    fi
    return 1
}

release_lock()
{
    if [ "${locked}" -eq 1 ]; then
        /bin/rmdir "${LOCK_DIR}" >/dev/null 2>&1 || true
        locked=0
    fi
}

restore_client()
{
    if [ -n "${active_addr}" ]; then
        /sbin/ifconfig "${WAN_IF}" inet6 "${active_addr}" delete >/dev/null 2>&1 || true
    fi
    if [ "${wan_nodad}" -eq 1 ]; then
        /sbin/ifconfig "${WAN_IF}" inet6 -no_dad >/dev/null 2>&1 || true
        wan_nodad=0
    fi
    if [ -n "${active_addr}" ] && [ -n "${active_iface}" ]; then
        /sbin/route add -inet6 "${active_addr}" -iface "${active_iface}" >/dev/null 2>&1 || true
        source_ll="$(interface_link_local "${active_iface}")"
        [ -n "${source_ll}" ] && /sbin/ping6 -c 1 -W 1500 -S "${source_ll}" \
            "${active_addr}" >/dev/null 2>&1 || true
        ensure_proxy_tables "${active_addr}" "${active_iface}"
    fi
    active_addr=""
    active_iface=""
}

shutdown_cleanup()
{
    restore_client
    release_lock
    exit 0
}

seed_client()
{
    active_addr="$1"
    active_iface="$2"
    prefix="$3"
    source_ll="$(interface_link_local "${active_iface}")"
    ont_addr="${prefix}:${ONT_IID}"

    [ -n "${source_ll}" ] || return 1
    /sbin/route delete -inet6 "${active_addr}" >/dev/null 2>&1 || true
    /sbin/route add -inet6 "${active_addr}" -iface "${active_iface}" >/dev/null 2>&1 || return 1
    /usr/sbin/ndp -d "${active_addr}" >/dev/null 2>&1 || true
    if ! /sbin/ping6 -c 1 -W 2000 -S "${source_ll}" "${active_addr}" >/dev/null 2>&1; then
        log "downstream verification failed for ${active_addr} on ${active_iface}"
        return 1
    fi

    if ! /sbin/ifconfig "${WAN_IF}" inet6 no_dad >/dev/null 2>&1; then
        log "could not enable temporary no-DAD mode for ${active_addr}"
        return 1
    fi
    wan_nodad=1
    /sbin/route delete -inet6 "${active_addr}" >/dev/null 2>&1 || true
    if ! /sbin/ifconfig "${WAN_IF}" inet6 "${active_addr}/128" alias >/dev/null 2>&1; then
        log "could not create temporary upstream alias for ${active_addr}"
        restore_client
        return 1
    fi
    /bin/sleep 1

    seed_result=0
    /sbin/ping6 -c 2 -W 3000 -S "${active_addr}" "${ont_addr}" >/dev/null 2>&1 || seed_result=1
    restore_client
    /bin/sleep 1

    if [ "${seed_result}" -ne 0 ]; then
        log "upstream seed failed for ${active_addr} on ${active_iface}"
        return 1
    fi
    if ! client_healthy "$1" "$2"; then
        log "proxy relearn failed for $1 on $2 after upstream seed"
        return 1
    fi
    mark_seeded "$1" "$2"
    log "active client protected: $1 via $2"
    return 0
}

run_check()
{
    validate_config || return 1
    prefix="$(current_prefix)"
    [ -n "${prefix}" ] || return 1
    critical_addr="${prefix}:${CRITICAL_IID}"
    ont_addr="${prefix}:${ONT_IID}"
    processed=0
    initializing=0
    [ -r "${STATE_FILE}" ] || initializing=1

    candidates="$(candidate_addrs "${prefix}")"
    [ -n "${candidates}" ] || return 0

    for addr in ${candidates}; do
        [ "${addr}" != "${critical_addr}" ] || continue
        [ "${addr}" != "${ont_addr}" ] || continue
        addr_is_local "${addr}" && continue

        already_healthy=0
        existing_iface="$(route_interface "${addr}")"
        if is_downstream_iface "${existing_iface}" && client_healthy "${addr}" "${existing_iface}"; then
            already_healthy=1
        fi
        iface="$(locate_client "${addr}")" || continue
        is_downstream_iface "${iface}" || continue
        if [ "${initializing}" -eq 1 ] && [ "${already_healthy}" -eq 1 ]; then
            mark_seeded "${addr}" "${iface}"
            continue
        fi
        if seed_is_fresh "${addr}" "${iface}" && [ "${already_healthy}" -eq 1 ]; then
            continue
        fi

        acquire_lock || return 0
        seed_client "${addr}" "${iface}" "${prefix}" || true
        release_lock
        processed=$((processed + 1))
        [ "${processed}" -lt "${BATCH_SIZE}" ] || break
    done
    return 0
}

show_status()
{
    prefix="$(current_prefix)"
    count=0
    [ -n "${prefix}" ] && [ -r "${STATE_FILE}" ] && \
        count="$(/usr/bin/awk -v prefix="${prefix}:" 'index($2, prefix) == 1 { count++ } END { print count+0 }' "${STATE_FILE}")"
    echo "current_prefix=${prefix:-none}"
    echo "protected_active_clients=${count}"
    echo "downstream_interfaces=${DOWNSTREAM_IFS}"
}

seed_named_client()
{
    validate_config || return 1
    addr="$1"
    prefix="$(current_prefix)"
    [ -n "${prefix}" ] || return 1
    case "${addr}" in
        "${prefix}:"*) ;;
        *) echo "address is outside the current prefix" >&2; return 1 ;;
    esac
    addr_is_local "${addr}" && return 1
    iface="$(locate_client "${addr}")" || {
        log "could not locate ${addr} on a downstream interface"
        return 1
    }
    acquire_lock || return 1
    seed_client "${addr}" "${iface}" "${prefix}"
    result=$?
    release_lock
    return "${result}"
}

trap shutdown_cleanup INT TERM HUP

case "${1:-check}" in
    check|--check)
        run_check
        ;;
    daemon|--daemon)
        while :; do
            run_check || true
            /bin/sleep "${INTERVAL}"
        done
        ;;
    status|--status)
        show_status
        ;;
    seed|--seed)
        [ "$#" -eq 2 ] || { echo "usage: $0 --seed <ipv6-address>" >&2; exit 64; }
        seed_named_client "$2"
        ;;
    *)
        echo "usage: $0 [check|status|daemon|--seed <ipv6-address>]" >&2
        exit 64
        ;;
esac
