#!/bin/sh
# ============================================================================
# opengfw 拦截诊断（只读，不改设备任何设置）
#
# 用法（在路由上执行）：
#     sh /tmp/ogdiag.sh                 # 面板端口/账号/口令从运行进程的参数里读
#     sh /tmp/ogdiag.sh <面板口令>       # 读不到时手动给
#
# 第 ④ 步采样 20 秒 —— 请在采样期间用客户端连一次 Shadowsocks（开个网页）。
# 跑完把整段输出发回即可。脚本不会输出口令。
# ============================================================================

J=/usr/sbin/jq
[ -x "$J" ] || J=$(command -v jq 2>/dev/null)
PW_ARG="$1"

hr() { echo "──────────────────────────────────────────────"; }

# ── 找插件安装位置与进程 ────────────────────────────────────────────────
APPDIR=""
for d in /etc/log/apps/opengfw-*; do
  [ -d "$d" ] && APPDIR="$d"
done
if [ -z "$APPDIR" ]; then
  echo "❌ 没找到已安装的 opengfw 应用。已安装的应用："
  ls /etc/log/apps/ 2>/dev/null | sed 's/^/     /'
  exit 0
fi

VPID=""
for p in /proc/[0-9]*; do
  [ -r "$p/cmdline" ] || continue
  if tr '\0' ' ' < "$p/cmdline" 2>/dev/null | grep -q 'opengfwd'; then VPID=${p#/proc/}; break; fi
done

# 从运行进程的参数里取 flag 的值（参数是 "-flag 值" 成对出现）
argof() {
  [ -n "$VPID" ] || return
  tr '\0' '\n' < "/proc/$VPID/cmdline" 2>/dev/null | awk -v f="$1" 'p==f{print;exit}{p=$0}'
}
# 应用配置是 KEY=VALUE，按键取值（不 source，避免值里的特殊字符被执行）
envof() { [ -r "$1" ] && sed -n "s/^$2=//p" "$1" | head -1; }
ENVF="$APPDIR/.pkg/app/.env"

LISTEN=$(argof -listen); CFG=$(argof -config); RUL=$(argof -rules)
WUSER=$(argof -web-user); WPASS=$(argof -web-password); TLSARG=$(argof -web-tls)
[ -n "$WUSER" ] || WUSER=$(envof "$ENVF" OPENGFW_WEB_USER)
[ -n "$WPASS" ] || WPASS=$(envof "$ENVF" OPENGFW_WEB_PASSWORD)
[ -n "$TLSARG" ] || TLSARG=$(envof "$ENVF" OPENGFW_WEB_TLS)
[ -n "$CFG" ] || CFG=$(envof "$ENVF" OPENGFW_CONFIG_PATH)
[ -n "$RUL" ] || RUL=$(envof "$ENVF" OPENGFW_RULES_PATH)
[ -n "$PW_ARG" ] && WPASS="$PW_ARG"

HOST="${LISTEN%:*}"; PORT="${LISTEN##*:}"
case "$HOST" in ""|0.0.0.0|"::"|"[::]") HOST=127.0.0.1 ;; esac
[ -n "$PORT" ] || { PORT=$(envof "$ENVF" APP_PORT_WEB); [ -n "$PORT" ] || PORT=9170; }
if [ "$TLSARG" = "off" ]; then SCHEME=http; else SCHEME=https; fi
BASE="$SCHEME://$HOST:$PORT"

api() {
  if [ -n "$WUSER" ]; then curl -sk -m 10 -u "$WUSER:$WPASS" "$BASE$1"
  else curl -sk -m 10 "$BASE$1"; fi
}

echo "opengfw 拦截诊断   $(date '+%Y-%m-%d %H:%M:%S')"
echo "插件位置: $APPDIR"
echo "面板:     $BASE   账号: ${WUSER:-（无鉴权）}   口令: $([ -n "$WPASS" ] && echo 已读到 || echo 未读到)"
# 生效路径优先从接口取：启动参数里的路径可能是老版本写进去的默认值
# （例如 1.11 的 /etc/opengfw），引擎在 1.12 起会自动重定向到持久分区。
EFF_CFG=$(api /api/v1/status | $J -r '.config_path' 2>/dev/null)
EFF_RUL=$(api /api/v1/status | $J -r '.rules_path' 2>/dev/null)
[ -n "$EFF_CFG" ] || EFF_CFG="$CFG"
[ -n "$EFF_RUL" ] || EFF_RUL="$RUL"
echo "生效配置文件: ${EFF_CFG:-（未读到）}"
echo "生效规则文件: ${EFF_RUL:-（未读到）}"
if [ -n "$CFG" ] && [ "$CFG" != "$EFF_CFG" ]; then
  echo "  （启动参数里写的是 $CFG，已自动改用上面的持久分区路径；应用设置是安装时生成的，升级不会更新）"
fi
hr

# ── ① 进程与版本 ────────────────────────────────────────────────────────
echo "① 进程与版本"
if [ -n "$VPID" ]; then
  echo "  引擎进程: pid=$VPID"
  echo "  包内版本: $($J -r .version "$APPDIR/.pkg/manifest.json" 2>/dev/null)"
else
  echo "  ❌ 引擎进程不在跑 —— 面板上点「启动核心」。"
fi
hr

# ── ② 三层开关（最容易搞混的地方）───────────────────────────────────────
echo "② 开关状态"
ST=$(api /api/status)
case "$ST" in
  *Unauthorized*|"")
    echo "  ❌ 面板没响应或鉴权失败（$BASE，账号 ${WUSER:-无}）。"
    echo "     面板口令若是自己改过的，请用:  sh $0 <面板口令>"
    ;;
  *)
    echo "$ST" | $J -r '"  引擎运行: " + (.engine.running|tostring)
      + "   拦截(接管): " + (.fw_active|tostring)
      + "   识别范围: " + .settings.scope
      + "   流量筛选: " + .settings.queue_only
      + "   日志等级: " + .settings.log_level' 2>/dev/null || echo "  $ST" | head -c 300
    HOOKS=0
    for C in INPUT OUTPUT FORWARD; do
      HOOKS=$((HOOKS + $(iptables -w -S "$C" 2>/dev/null | grep -c 'queue-num')))
    done
    echo "  内核 NFQUEUE 钩子: $HOOKS 条"
    [ "$HOOKS" = "0" ] && {
      echo "  ❌ 拦截没真正开启：面板上「启动核心」只让引擎跑起来（只看不拦），"
      echo "     要点「开启接管」才会装内核钩子（按钮会变成「停止接管」）。"
    }
    QO=$(echo "$ST" | $J -r '.settings.queue_only' 2>/dev/null)
    if [ -n "$QO" ] && [ "$QO" != "all" ]; then
      echo "  ❌ 流量筛选是「$QO」：只有该类型端口的报文会进引擎。"
      echo "     你的 SS 走 54454/TCP，不会被送进来 —— 这是「开了拦截没用」最常见的原因，改成「全部」。"
    fi
    ;;
esac
hr

# ── ③ 协议屏蔽开关与规则 ────────────────────────────────────────────────
echo "③ 深度协议防护（勾了才会生成规则）"
SH=$(api /api/rules | $J -c '.data.shield' 2>/dev/null)
if [ -n "$SH" ]; then
  echo "  $SH"
  SSM=$(api /api/rules | $J -r '.data.shield.shadowsocks_mode' 2>/dev/null)
  case "$SSM" in
    block) echo "  ✅ Shadowsocks 屏蔽已启用（生成 fet 判定规则）" ;;
    *)     echo "  ❌ Shadowsocks 屏蔽未启用（当前：${SSM:-空}）—— 没有规则，开了拦截也拦不住 SS" ;;
  esac
else
  echo "  （读不到，见 ② 的鉴权提示）"
fi
RP="$EFF_RUL"
[ -f "$RP" ] || RP="/etc/log/opengfw/rules.yaml"
[ -f "$RP" ] || RP="${RUL:-/etc/log/opengfw/rules.yaml}"
if [ -f "$RP" ]; then
  echo "  规则文件: $RP"
  echo "  面板生成的规则: $(grep -c '^- name: panel:' "$RP") 条"
  grep -E '^- name: panel:' "$RP" | head -6 | sed 's/^/     /'
  echo "  第一条规则: $(head -1 "$RP" | cut -c1-70)"
  echo "  （面板规则必须在文件最前面：引擎先匹配先生效，排在默认放行规则后面就永不生效）"
else
  echo "  ⚠️ 找不到 $RP"
fi
hr

# ── ④ 报文到底有没有进引擎 ──────────────────────────────────────────────
echo "④ 队列计数（判「报文有没有进引擎」只看这个；钩子之后的规则计数恒为 0，别用）"
qcnt() {
  for C in INPUT OUTPUT FORWARD; do
    iptables -w -L "$C" -v -n -x 2>/dev/null | grep NFQUEUE
  done | awk '{s+=$1} END {print s+0}'
}
A=$(qcnt)
echo "  当前: $A 包。"
echo "  采样 20 秒 —— 现在请用客户端连一次 SS（开个网页）……"
sleep 20
B=$(qcnt)
echo "  20 秒后: $B 包   增量: $((B - A))"
if [ "$((B - A))" -eq 0 ]; then
  echo "  ❌ 没有报文进引擎。按概率排："
  echo "     a) 拦截没开 / 没点「开启接管」（看 ②）"
  echo "     b) 流量筛选不是「全部」，SS 的端口不在范围内（看 ②）"
  echo "     c) 客户端根本没走这台路由出去（分流/直连/策略路由）"
  echo "     d) 别的程序在更前面就把流量截走了（看 ⑤）"
else
  echo "  ✅ 报文进了引擎。若仍没被拦，看 ⑥ 里引擎对这些流的判定。"
fi
hr

# ── ⑤ 有没有别的东西先截走流量（不预设是谁）──────────────────────────────
echo "⑤ 更前面的劫持点（TPROXY/REDIRECT 都在 FORWARD 之前生效）"
found=""
for T in mangle nat; do
  OUT=$(iptables -t "$T" -S 2>/dev/null | grep -iE 'TPROXY|REDIRECT|--to-ports' | head -6)
  [ -n "$OUT" ] && { found=1; echo "  [$T 表]"; echo "$OUT" | sed 's/^/     /'; }
done
if [ -z "$found" ]; then
  echo "  未发现 TPROXY/REDIRECT 类规则（这台设备上没有抢先劫持的迹象）"
else
  echo "  --- 上面这些规则的实际命中数（非 0 = 确实在截流量，且比插件早）---"
  for T in mangle nat; do
    iptables -t "$T" -L -v -n -x 2>/dev/null | grep -iE 'TPROXY|REDIRECT|dpt:' |
      awk '$1>0 {print "     ["t"] "$1" 包  "$0}' t="$T" | head -6
  done
fi
echo "  其它已装应用（也可能在处理流量）:"
for d in /etc/log/apps/*/; do
  n=$(basename "$d")
  case "$n" in opengfw-*) continue;; esac
  echo "     $n"
done
if command -v nft >/dev/null 2>&1; then
  echo "  nftables 里 TPROXY/REDIRECT 规则: $(nft list ruleset 2>/dev/null | grep -icE 'tproxy|redirect') 条"
fi
hr

# ── ⑥ 引擎怎么看这些流 ─────────────────────────────────────────────────
echo "⑥ 引擎日志（判定结果与命中规则）"
LOG="$APPDIR/.pkg/log/engine.log"
if [ -s "$LOG" ]; then
  echo "  $LOG  ($(wc -c < "$LOG") 字节)"
  echo "  --- 全加密(fet)判定与 SS 屏蔽命中 ---"
  grep -aE 'fet|panel:shield:shadowsocks' "$LOG" 2>/dev/null | tail -8 | cut -c1-200 | sed 's/^/     /'
  echo "  --- 最近判定（看你的 SS 服务器 IP 有没有出现）---"
  grep -a '命中规则' "$LOG" 2>/dev/null | tail -10 | cut -c1-200 | sed 's/^/     /'
else
  echo "  ⚠️ 日志为空（0 字节）：引擎从未处理过任何流（或刚重启过）。"
fi
echo "  运行日志尾部:"
tail -4 "$APPDIR/.pkg/log/run.log" 2>/dev/null | sed 's/^/     /'
hr

# ── ⑦ 自身可用性 ───────────────────────────────────────────────────────
echo "⑦ 设备自身出网与钩子残留"
echo "  出网: $(ping -c 2 -W 3 -q 223.5.5.5 >/dev/null 2>&1 && echo ok || echo 失败)"
echo "  NFQUEUE 钩子: $(for c in INPUT OUTPUT FORWARD; do iptables -w -S "$c" 2>/dev/null; done | grep -c queue-num) 条（开着拦截时应有 1）"
hr
echo "诊断结束。把上面整段输出发回即可。"
