#!/bin/sh
# Freezer router observer v1. Read-only, fixed command set; safe to send on SSH stdin.
# It deliberately does not report a router as healthy or protected.
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
LC_ALL=C
export LC_ALL

if [ "${1:-}" != status ] || [ "$#" -ne 1 ]; then
    printf '%s\n' 'Usage: freezer-router status' >&2
    exit 64
fi

# One key and one bounded, single-line value per record. Never return raw UCI,
# process arguments, subscription contents, credentials, or firewall rule bodies.
emit() {
    clean_value=$(printf '%s' "$2" | tr '\011\012\015' '   ' | cut -c 1-2048)
    printf '%s\t%s\n' "$1" "$clean_value"
}
field() {
    if command -v jsonfilter >/dev/null 2>&1; then
        printf '%s' "$1" | jsonfilter -e "$2" 2>/dev/null
    fi
}
boolean() {
    case "$1" in 1|true) printf '1';; 0|false) printf '0';; *) printf '?';; esac
}
process_running() {
    if ! command -v pidof >/dev/null 2>&1; then printf '?'; return; fi
    found=0
    uncertain=0
    for process_name in "$@"; do
        pidof "$process_name" >/dev/null 2>&1
        result=$?
        case "$result" in 0) found=1;; 1) :;; *) uncertain=1;; esac
    done
    if [ "$found" = 1 ]; then printf '1'
    elif [ "$uncertain" = 1 ]; then printf '?'
    else printf '0'; fi
}
# Only syntactically numeric IP candidates are emitted. The Mac validates them
# with inet_pton. This excludes DNS URLs and any embedded authentication data.
ip_candidates() {
    printf '%s\n' "$1" | tr ' \011' '\n\n' | awk '
        /^[0-9][0-9.]*$/ { print; next }
        /^[0-9a-fA-F:]+$/ { if (index($0, ":") > 0) print }
    ' | head -n 32
}
# POSIX cksum is often absent from BusyBox. The wire format stays numeric so a
# missing cksum can use awk without changing the Mac parser.
rule_fingerprint() {
    if command -v cksum >/dev/null 2>&1; then
        printf '%s\n' "$1" | cksum | awk '{print $1 ":" $2}'
        return
    fi
    if command -v awk >/dev/null 2>&1; then
        printf '%s\n' "$1" | awk 'BEGIN { sum = 0; bytes = 0 }
            {
                bytes += length($0) + 1
                for (i = 1; i <= length($0); i++) sum = (sum * 131 + index("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -.:/_", substr($0, i, 1)) + 1) % 4294967291
            }
            END { print sum ":" bytes }'
        return
    fi
    return 1
}
firewall_summary() {
    binary=$1
    prefix=$2
    if ! command -v "$binary" >/dev/null 2>&1; then
        emit "${prefix}_state" unavailable
        return
    fi
    # -S excludes packet counters, so ordinary traffic does not change this
    # fingerprint. Collect tables independently and preserve incomplete status.
    complete=1
    for table in filter nat mangle; do
        rules=$("$binary" -t "$table" -S 2>/dev/null)
        result=$?
        if [ "$result" -ne 0 ]; then
            emit "${prefix}_${table}" unavailable
            complete=0
        else
            checksum=$(rule_fingerprint "$rules") || checksum=""
            count=$(printf '%s\n' "$rules" | awk '/^-A / { n++ } END { print n+0 }')
            if [ -n "$checksum" ]; then
                emit "${prefix}_${table}" "cksum:$checksum;rules:$count"
            else
                emit "${prefix}_${table}" unavailable
                complete=0
            fi
        fi
    done
    if [ "$complete" = 1 ]; then emit "${prefix}_state" observed
    else emit "${prefix}_state" partial; fi
}
# Lowest-metric IPv4 default route, not the interface literally named wan.
active_default_device() {
    printf '%s\n' "$1" | awk '
        BEGIN { best = -1 }
        $1 == "default" {
            metric = 0
            device = ""
            for (i = 1; i <= NF; i++) {
                if ($i == "dev" && i < NF) device = $(i + 1)
                if ($i == "metric" && i < NF) metric = $(i + 1) + 0
            }
            if (device != "" && (best < 0 || metric < best)) {
                best = metric
                chosen = device
            }
        }
        END { if (chosen != "") print chosen }
    '
}
emit_uplink() {
    status_json=$1
    call_result=$2
    emit wan_up "$(boolean "$(field "$status_json" '@.up')")"
    emit wan_protocol "$(field "$status_json" '@.proto')"
    emit wan_device "$(field "$status_json" '@.l3_device')"
    emit dns_runtime "$(ip_candidates "$(field "$status_json" '@["dns-server"][*]')")"
    if [ "$call_result" = 0 ]; then emit wan_state observed
    else emit wan_state unavailable; fi
}

printf '%s\n' 'FREEZER_ROUTER_STATUS 1'
route4=""
result4=1
route6=""
result6=1
if command -v ip >/dev/null 2>&1; then
    route4=$(ip -4 route show default 2>/dev/null)
    result4=$?
    route6=$(ip -6 route show default 2>/dev/null)
    result6=$?
fi
if command -v ubus >/dev/null 2>&1; then
    board=$(ubus call system board 2>/dev/null)
    emit hostname "$(field "$board" '@.hostname')"
    emit model "$(field "$board" '@.model')"
    emit firmware "$(field "$board" '@.release.description')"
    uplink_device=$(active_default_device "$route4")
    matched=0
    wan=$(ubus call network.interface.wan status 2>/dev/null)
    wan_result=$?
    wan_device_now=$(field "$wan" '@.l3_device')
    if [ -n "$uplink_device" ] && [ "$wan_result" = 0 ] && [ "$wan_device_now" = "$uplink_device" ]; then
        emit_uplink "$wan" 0
        matched=1
    elif [ -n "$uplink_device" ]; then
        for iface_name in $(ubus list 2>/dev/null | sed -n 's/^network\.interface\.//p'); do
            iface_status=$(ubus call "network.interface.$iface_name" status 2>/dev/null) || continue
            iface_device=$(field "$iface_status" '@.l3_device')
            if [ "$iface_device" = "$uplink_device" ]; then
                emit_uplink "$iface_status" 0
                matched=1
                break
            fi
        done
    fi
    if [ "$matched" != 1 ]; then
        if [ -z "$wan" ]; then
            wan=$(ubus call network.interface.wan status 2>/dev/null)
            wan_result=$?
        fi
        emit_uplink "$wan" "$wan_result"
    fi
else
    emit wan_state unavailable
fi
if command -v uci >/dev/null 2>&1; then
    emit wan_config_protocol "$(uci -q get network.wan.proto 2>/dev/null)"
    emit wan_peer_dns "$(boolean "$(uci -q get network.wan.peerdns 2>/dev/null)")"
    emit dns_configured "$(ip_candidates "$(uci -q get network.wan.dns 2>/dev/null)")"
    emit dnsmasq_noresolv "$(boolean "$(uci -q get 'dhcp.@dnsmasq[0].noresolv' 2>/dev/null)")"
    emit openclash_enabled "$(boolean "$(uci -q get openclash.config.enable 2>/dev/null)")"
fi
emit proxy_running "$(process_running mihomo clash clash_meta clash_tun)"
emit dns_running "$(process_running dnsmasq)"
emit default_route "$route4"
emit default_route6 "$route6"
if [ "$result4" = 0 ]; then emit route4_state observed
else emit route4_state unavailable; fi
if [ "$result6" = 0 ]; then emit route6_state observed
else emit route6_state unavailable; fi
firewall_summary iptables firewall_v4
firewall_summary ip6tables firewall_v6
printf '%s\n' 'FREEZER_ROUTER_END'
