#!/bin/sh
# panah-diag — one diagnostic snapshot of everything that decides whether the
# Mac's traffic really rides the Panah tunnel. Written for the case
# "connected, but the traffic does not go through the VPN".
#
#   ./panah-diag [tag]          snapshot (root sees pf + tunnel counters too)
#   ./panah diag                same, via the CLI (sudo for the full picture)
#
# `panah up` runs it automatically (as root, through the helper) after every
# connect attempt — success or failure — and the watchdog runs it when a
# session dies. Output: <package>/logs/diag-<time>-<tag>.txt (newest 30 kept)
# plus logs/diag-latest.txt. Every snapshot ends with a VERDICT block: one
# PASS / FAIL / WARN line per check, so the cause is readable at a glance.
#
# Secrets never enter a snapshot: private keys are reduced to a short
# fingerprint (enough to spot a stale device config), passwords are skipped.
DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
TAG=${1:-manual}
LOG_DIR="$DIR/logs"
RUN_DIR="/var/run/panah"
RUNTIME_CFG="/etc/panah/panah-daita-runtime.json"
DAITA_CFG="$DIR/daita/panah-daita.json"
LW_CFG="/etc/panah/lightway-client.yaml"
[ -f "$LW_CFG" ] || LW_CFG="$DIR/lightway/client_config.yaml"
SERVER_IP="2.31.21.198"        # underlay of both protocols
EXIT_IP="2.31.73.228"          # what the panel reports through a tunnel
TCP_PORT=5001                  # DAITA over TCP (panah-tcp2udp.service)
PANEL="https://blog.najvaa.xyz:8443/api/ip"
ROOT=0; [ "$(id -u)" = 0 ] && ROOT=1

mkdir -p "$LOG_DIR" 2>/dev/null
STAMP=$(date '+%Y%m%d-%H%M%S')
OUT="$LOG_DIR/diag-$STAMP-$TAG.txt"
VERDICT=$(mktemp "${TMPDIR:-/tmp}/panah-verdict.XXXXXX")
trap 'rm -f "$VERDICT"' EXIT

sec()  { printf '\n===== %s =====\n' "$1"; }
run()  { printf '$ %s\n' "$*"; "$@" 2>&1; }
pass() { echo "PASS  $1" >> "$VERDICT"; }
fail() { echo "FAIL  $1" >> "$VERDICT"; }
warn() { echo "WARN  $1" >> "$VERDICT"; }
info() { echo "INFO  $1" >> "$VERDICT"; }
# interface a destination is routed through right now
iface_for() { route -n get "$1" 2>/dev/null | awk '/interface:/{print $2; exit}'; }
# short, irreversible fingerprint of a base64 key (never the key itself)
key_id() { printf '%s' "$1" | shasum -a 256 | cut -c1-12; }
json_field() { sed -n "s/.*\"$2\" *: *\"\\([^\"]*\\)\".*/\\1/p" "$1" 2>/dev/null | head -1 | tr -d '\\'; }
# curl wrapper: one line "code=.. tcp=.. tls=.. err=.."
probe() {
    curl -sk -o /dev/null --max-time "${PROBE_T:-6}" "$@" \
        -w 'code=%{http_code} tcp=%{time_connect}s tls=%{time_appconnect}s ip=%{remote_ip} err=%{errormsg}' 2>&1
}

snapshot() {
    PROTO=$(cat "$DIR/.protocol" 2>/dev/null || echo none)
    sec "META"
    echo "time      : $(date '+%Y-%m-%d %H:%M:%S %Z') (utc $(date -u '+%H:%M:%S'))"
    echo "tag       : $TAG   root=$ROOT"
    echo "package   : $(cat "$DIR/bin/.prebuilt" 2>/dev/null || echo source)  app=$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$DIR/Panah.app/Contents/Info.plist" 2>/dev/null)"
    echo "macOS     : $(sw_vers -productVersion 2>/dev/null) ($(uname -m))"
    echo "protocol  : $PROTO"
    echo "settings  : $(tr '\n' ' ' < /etc/panah/settings 2>/dev/null)"
    echo "routed    : $(cat "$RUN_DIR/routed" 2>/dev/null || echo 'no (the CLI has not proven routing)')"

    sec "PROCESSES"
    for p in gotatun lightway-client udp2tcp panah-daita-client panah-watchdog mullvad-daemon; do
        printf '%-20s %s\n' "$p" "$(pgrep -f "$p" | tr '\n' ' ')"
    done
    pgrep -x gotatun >/dev/null && T_DAITA=1 || T_DAITA=0
    pgrep -x lightway-client >/dev/null && T_LW=1 || T_LW=0

    # ---- other VPNs: the #1 Mac-only reason traffic avoids the tunnel ----
    sec "OTHER VPNs / NETWORK EXTENSIONS"
    run scutil --nc list
    run systemextensionsctl list
    MV="/Applications/Mullvad VPN.app/Contents/Resources/mullvad"
    if [ -x "$MV" ]; then
        run "$MV" status
        run "$MV" split-tunnel get
        MV_STATE=$("$MV" status 2>/dev/null | head -1)
        case "$MV_STATE" in
        Connected*|Connecting*) fail "Mullvad is $MV_STATE — its firewall and routes override Panah (disconnect AND quit Mullvad)";;
        *) warn "Mullvad installed ($MV_STATE) — its split-tunnel utun / local DNS (127.x on lo0) can still capture traffic; quit it fully to rule it out";;
        esac
    fi
    scutil --nc list 2>/dev/null | grep -q '(Connected)' && fail "a macOS VPN profile is Connected (scutil --nc list) — two VPNs fight over routes"

    sec "INTERFACES (utun + physical)"
    ifconfig -a 2>/dev/null | awk '/^[a-z]/{i=$1} /^(utun|en)[0-9]/{print} /inet /{print "    " i " " $0}'
    PHYS=$(netstat -rn -f inet 2>/dev/null | awk '$1=="default" && $NF ~ /^en/ {print $NF; exit}')
    GW=$(netstat -rn -f inet 2>/dev/null | awk '$1=="default" && $NF ~ /^en/ {print $2; exit}')
    echo "physical  : ${PHYS:-?} via ${GW:-?}"

    # which utun is OURS
    TUN=""
    if [ "$PROTO" = daita ]; then
        TUN=$(json_field "$RUNTIME_CFG" interface); [ -n "$TUN" ] || TUN=$(json_field "$DAITA_CFG" interface)
        ifconfig "$TUN" >/dev/null 2>&1 || TUN=""
    else
        TUN=$(ifconfig -a 2>/dev/null | awk '/^[a-z]/{i=$1; sub(":$","",i)} /inet (10\.125\.|100\.64\.0\.|169\.254\.10\.)/{print i; exit}')
    fi
    echo "panah tun : ${TUN:-none}"
    for u in $(ifconfig -l); do
        case "$u" in utun*) ;; *) continue;; esac
        [ "$u" = "$TUN" ] && continue
        a=$(ifconfig "$u" 2>/dev/null | awk '/inet /{print $2; exit}')
        [ -n "$a" ] && warn "foreign tunnel $u ($a) is up — another VPN/app owns it"
    done

    sec "ROUTES (inet)"
    run netstat -rn -f inet
    sec "ROUTES (inet6, defaults only)"
    netstat -rn -f inet6 2>/dev/null | awk 'NR<=4 || $1=="default" || $1=="::/0" || $1 ~ /^(::|8000::)\/1$/'

    sec "ROUTE DECISIONS"
    for d in 1.1.1.1 8.8.8.8 142.250.185.78 "$SERVER_IP" "$EXIT_IP" 10.64.0.1 100.64.0.1; do
        printf '%-16s -> %s\n' "$d" "$(iface_for "$d")"
    done
    printf '%-16s -> %s\n' "2606:4700::1111" "$(route -n get -inet6 2606:4700::1111 2>/dev/null | awk '/interface:/{print $2; exit}')"

    if [ -n "$TUN" ]; then
        [ "$(iface_for 1.1.1.1)" = "$TUN" ] && [ "$(iface_for 142.250.185.78)" = "$TUN" ] \
            && pass "IPv4 default traffic is routed into $TUN" \
            || fail "IPv4 default traffic goes via $(iface_for 1.1.1.1), not the tunnel $TUN (the /1 hijack is missing or overridden)"
        S_IF=$(iface_for "$SERVER_IP")
        [ "$S_IF" != "$TUN" ] && pass "server underlay $SERVER_IP pinned outside the tunnel ($S_IF)" \
            || fail "server underlay $SERVER_IP routed INTO the tunnel — the session loops into itself"
        V6=$(route -n get -inet6 2606:4700::1111 2>/dev/null | awk '/interface:/{print $2; exit}')
        case "$V6" in ""|"$TUN"|lo0) pass "IPv6 cannot leak (${V6:-no route})";;
        *) fail "IPv6 leaks via $V6 — v6 traffic bypasses the VPN";; esac
        # a utun of someone else holding a MORE specific route also wins
        netstat -rn -f inet 2>/dev/null | awk -v t="$TUN" '$NF ~ /^utun/ && $NF != t && ($1 ~ /\/(1|2|3|4)$/ || $1=="default") {print}' \
            | while read -r l; do echo "WARN  competing route on another utun: $l" >> "$VERDICT"; done
        netstat -rn -f inet 2>/dev/null | awk '$1=="default" && $3 ~ /I/' | grep -q . \
            && info "interface-scoped defaults exist (flag I) — apps that bind to en0 directly can bypass any VPN"
    else
        if [ $T_DAITA = 1 ] || [ $T_LW = 1 ]; then
            fail "a tunnel client runs but its utun is not up/identifiable — the session never came online"
        else
            info "no Panah tunnel running"
        fi
    fi

    sec "DNS"
    scutil --dns 2>/dev/null | awk '/^resolver/{r=$0} /nameserver|if_index|flags|domain/{print r ": " $0}' | head -40
    networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | grep -v '^\*' | while IFS= read -r s; do
        printf '%-28s %s\n' "$s" "$(networksetup -getdnsservers "$s" 2>/dev/null | tr '\n' ' ')"
    done
    NS=$(scutil --dns 2>/dev/null | awk '/nameserver\[0\]/{print $3; exit}')
    case "$NS" in
    10.64.0.*|100.64.0.*) pass "system DNS uses the Panah resolver ($NS)";;
    127.*) [ -n "$TUN" ] && fail "system DNS is $NS — a LOCAL resolver of another app (Mullvad?) answers, not Panah" || info "system DNS $NS (local resolver of another app)";;
    *) [ -n "$TUN" ] && fail "system DNS is $NS — not the tunnel resolver (DNS leak / wrong answers)" || info "system DNS $NS";;
    esac

    sec "PF (kill switch / other firewalls)"
    if [ $ROOT = 1 ]; then
        run pfctl -s info | head -3
        run pfctl -s Anchors
        run pfctl -a panah_ks -sr
        pfctl -s Anchors 2>/dev/null | grep -qi mullvad && fail "Mullvad pf anchor is loaded — it blocks non-Mullvad traffic even with Panah up"
    else
        echo "(not root — run with sudo for the pf rules)"
    fi

    sec "DAITA"
    if [ -f "$DAITA_CFG" ]; then
        echo "config    : $DAITA_CFG ($(stat -f '%Sm' "$DAITA_CFG"))"
        echo "address   : $(json_field "$DAITA_CFG" tunnel_address)  endpoint=$(json_field "$DAITA_CFG" server_endpoint)  iface=$(json_field "$DAITA_CFG" interface)"
        K_CFG=$(key_id "$(json_field "$DAITA_CFG" parent_private_key)")
        echo "key id    : $K_CFG (sha256 prefix, not the key)"
        # the GUI's own device record: a different key = stale package config
        CU=$(stat -f '%Su' /dev/console 2>/dev/null)
        DEV="/Users/$CU/Library/Application Support/PanahVPN/daita-device.json"
        if [ -f "$DEV" ]; then
            K_DEV=$(key_id "$(sed -n 's/.*"parent_private_key" *: *"\([^"]*\)".*/\1/p' "$DEV" | head -1 | tr -d '\\')")
            A_DEV=$(sed -n 's/.*"tunnel_address" *: *"\([^"]*\)".*/\1/p' "$DEV" | head -1); A_DEV=$(printf "%s" "$A_DEV" | tr -d "\\\\")
            echo "device    : $A_DEV key id $K_DEV ($(stat -f '%Sm' "$DEV"))"
            [ "$K_DEV" = "$K_CFG" ] && pass "DAITA config matches the app's current device ($A_DEV)" \
                || fail "DAITA config is STALE: package key $K_CFG ($(json_field "$DAITA_CFG" tunnel_address)) != device $K_DEV ($A_DEV) — the server drops this handshake"
        fi
    fi
    if [ -f "$RUNTIME_CFG" ] && [ $ROOT = 1 ]; then
        echo "runtime   : endpoint=$(json_field "$RUNTIME_CFG" server_endpoint) iface=$(json_field "$RUNTIME_CFG" interface)"
    fi
    echo "transport : $(grep -m1 '^dtr=' /etc/panah/settings 2>/dev/null | cut -d= -f2) shim=$(cat "$RUN_DIR/udp2tcp.pid" 2>/dev/null || echo none) pin=$(cat "$RUN_DIR/udp2tcp.pin" 2>/dev/null || echo none)"
    [ -f "$RUN_DIR/udp2tcp.log" ] && { echo "--- udp2tcp.log (tail)"; tail -15 "$RUN_DIR/udp2tcp.log"; }
    if [ $T_DAITA = 1 ] && [ -n "$TUN" ] && [ $ROOT = 1 ] && [ -S "/var/run/wireguard/$TUN.sock" ]; then
        echo "--- gotatun peers (keys stripped)"
        UAPI=$(python3 -I -c '
import socket,sys
s=socket.socket(socket.AF_UNIX); s.settimeout(3); s.connect(sys.argv[1]); s.sendall(b"get=1\n\n")
b=b""
while not b.endswith(b"\n\n"):
    c=s.recv(65536)
    if not c: break
    b+=c
for l in b.decode().splitlines():
    k=l.split("=",1)[0]
    if k in ("endpoint","last_handshake_time_sec","rx_bytes","tx_bytes","allowed_ip","persistent_keepalive_interval","errno"):
        print(l)' "/var/run/wireguard/$TUN.sock" 2>&1)
        echo "$UAPI"
        HS=$(echo "$UAPI" | awk -F= '/^last_handshake_time_sec/{v=$2} END{print v+0}')
        RX=$(echo "$UAPI" | awk -F= '/^rx_bytes/{s+=$2} END{print s+0}')
        AGE=$(( $(date +%s) - HS ))
        if [ "$HS" -gt 0 ] && [ $AGE -lt 180 ]; then pass "WireGuard handshake ${AGE}s ago, rx $RX bytes"
        else fail "no recent WireGuard handshake (last=$HS) — server not answering (stale key, blocked transport or shim down)"; fi
    fi
    [ -f "$RUN_DIR/panah-utun9.log" ] && { echo "--- gotatun log (tail, errors/warnings)"; grep -E 'WARN|ERROR|error' "$RUN_DIR/panah-utun9.log" | tail -8; }

    sec "LIGHTWAY"
    echo "server    : $(awk -F': *' '/^server:/{print $2}' "$LW_CFG" 2>/dev/null)  mode=$(awk -F': *' '/^mode:/{print $2}' "$LW_CFG" 2>/dev/null)"
    echo "pid       : $(cat "$RUN_DIR/lightway.pid" 2>/dev/null || echo none)"
    [ $T_LW = 1 ] && [ -z "$TUN" ] && fail "lightway-client runs but never came Online (no 10.125.x/100.64.x utun)"

    # ---- the underlay: can this network reach the server at all? --------
    sec "UNDERLAY REACHABILITY (outside the tunnel, pinned path)"
    PING=$(ping -c 2 -t 3 "$SERVER_IP" 2>&1 | tail -1)
    echo "ping $SERVER_IP : $PING"
    for p in 22 443 8443 $TCP_PORT; do
        if nc -z -G 4 "$SERVER_IP" $p >/dev/null 2>&1; then echo "tcp $SERVER_IP:$p : open"; else echo "tcp $SERVER_IP:$p : CLOSED/filtered"; fi
    done
    TLS=$(PROBE_T=6 probe "https://$SERVER_IP:443/")
    echo "tls $SERVER_IP:443  : $TLS"
    case "$TLS" in
    *"tls=0.000000s"*) case "$TLS" in *"tcp=0.000000s"*) fail "TCP to $SERVER_IP:443 fails from this Mac";;
                       *) fail "TLS to $SERVER_IP:443 never completes (TCP opens, handshake dropped) — Lightway cannot get online on this path";; esac;;
    *) pass "TLS handshake to $SERVER_IP:443 completes — Lightway's transport works";;
    esac
    nc -z -G 4 "$SERVER_IP" $TCP_PORT >/dev/null 2>&1 && pass "TCP $TCP_PORT open — DAITA over TCP transport reachable" \
        || fail "TCP $TCP_PORT to the server is blocked — DAITA over TCP cannot connect"
    echo "--- control: a domestic and a foreign site"
    echo "digikala  : $(PROBE_T=5 probe https://www.digikala.com/)"
    echo "1.1.1.1   : $(PROBE_T=5 probe https://1.1.1.1/)"

    # ---- the real question: where does traffic exit? -------------------
    sec "EXIT PATH (default routing, as an app sees it)"
    BODY=$(curl -s --max-time 8 "$PANEL" 2>/dev/null)
    echo "panel /api/ip : ${BODY:-no answer}"
    IP=$(printf '%s' "$BODY" | sed -n 's/.*"ip":"\([^"]*\)".*/\1/p')
    if [ -n "$TUN" ]; then
        case "$IP" in
        "$EXIT_IP"|10.64.*|100.64.*|10.125.*) pass "apps exit through the Panah server (panel sees $IP)";;
        "") fail "the panel is unreachable over the default route — the tunnel carries no traffic";;
        *) fail "apps exit as $IP — NOT through Panah (traffic bypasses the tunnel)";;
        esac
        TB=$(curl -s --max-time 8 --interface "$TUN" "$PANEL" 2>/dev/null | sed -n 's/.*"ip":"\([^"]*\)".*/\1/p')
        echo "forced via $TUN : ${TB:-no answer}"
        [ -n "$TB" ] && pass "the tunnel itself carries traffic (forced via $TUN: $TB)" \
            || fail "even traffic forced into $TUN gets no answer — the tunnel is dead inside"
    else
        echo "(no tunnel: exit ip = ${IP:-?})"
    fi
    sec "APP LOGS (tail)"
    CU=$(stat -f '%Su' /dev/console 2>/dev/null)
    LIVE="/Users/$CU/Library/Application Support/PanahVPN/live"
    echo "--- gui.log"; tail -25 "$LIVE/gui.log" 2>/dev/null
    echo "--- elev.log (helper ops + CLI markers)"
    grep -E 'op: |\[panah\]|Error|state=Online|no WireGuard' "$LIVE/elev.log" 2>/dev/null | grep -viE 'password|private' | tail -40
    echo "--- watchdog.log"; tail -10 "$RUN_DIR/watchdog.log" 2>/dev/null
    echo "--- public check (third party): $(curl -s --max-time 6 https://am.i.mullvad.net/ip 2>/dev/null || echo no-answer)"
}

{
    snapshot
    sec "VERDICT"
    if [ -s "$VERDICT" ]; then
        grep '^FAIL' "$VERDICT"; grep '^WARN' "$VERDICT"; grep -E '^(PASS|INFO)' "$VERDICT"
    else
        echo "(no checks ran)"
    fi
} > "$OUT" 2>&1

chmod 644 "$OUT" 2>/dev/null
cp -f "$OUT" "$LOG_DIR/diag-latest.txt" 2>/dev/null && chmod 644 "$LOG_DIR/diag-latest.txt" 2>/dev/null
# the console user owns the package — keep the logs readable/removable by them
if [ $ROOT = 1 ]; then
    CU=$(stat -f '%Su' "$DIR" 2>/dev/null)
    [ -n "$CU" ] && chown "$CU" "$LOG_DIR" "$OUT" "$LOG_DIR/diag-latest.txt" 2>/dev/null
fi
# keep the newest 30 snapshots
ls -1t "$LOG_DIR"/diag-2*.txt 2>/dev/null | tail -n +31 | while IFS= read -r f; do rm -f "$f"; done

# one-line summary for the caller's log (the GUI log / terminal)
NF=$(grep -c '^FAIL' "$OUT"); NW=$(grep -c '^WARN' "$OUT")
echo "[panah] diag ($TAG): $NF fail, $NW warn — $OUT"
grep '^FAIL' "$OUT" | head -5 | sed 's/^/[panah]   /'
exit 0
