#!/bin/bash
# node-relay 中转机（代理入口）一键装机 + 自动注册到 api 后台（DESIGN §15）。
#   - Reality/TCP：nginx stream ssl_preread 按 SNI（{主节点IP}.sslip.io）分流到各主节点 :443
#   - hy2/UDP：nftables 按端口 DNAT + masquerade 到各主节点 hy2 端口
#   - 守护进程：node-agent relay（同一二进制的子命令），收 api 下发的目标表后重写上述两处并热加载
#   - 管理端点：127.0.0.1:8081，经 nginx 自签证书 https 反代到 0.0.0.0:10443（api 信任所有证书）
# 中转机本身不装 Xray/hysteria/SQLite，不作为节点注册；后续装节点时在 register-node.sh 里选择本中转机即可。
#
# 用法（在中转机上，以 root 运行）：
#   curl -fsSL https://64.118.149.202.sslip.io/install-relay.sh | bash
#   或下载后：bash install-relay.sh
# 最后一步会交互提示：管理员账号密码（已开谷歌两步验证时再要动态码）→ 中转机名称（默认 relay-<ip>）。
# 可用环境变量预置跳过提示：API_BASE ADMIN_USER ADMIN_PASS ADMIN_TOTP RELAY_NAME RESOURCE_BASE
# 重复执行幂等：复用已有 API_KEY / 证书，不会重复注册（relay.env 里已有 RELAY_ID 则跳过注册）。
# 卸载：curl -fsSL https://64.118.149.202.sslip.io/uninstall-relay.sh | bash
set -e
log(){ echo "[$(date '+%H:%M:%S')] $1"; }

RESOURCE_BASE="${RESOURCE_BASE:-https://64.118.149.202.sslip.io}"
API_BASE="${API_BASE:-https://api.guayangtou.top}"
RELAY_API_PORT="${RELAY_API_PORT:-10443}"     # api → 中转机管理端点的 https 反代端口
RELAY_LISTEN="${RELAY_LISTEN:-127.0.0.1:8081}" # 守护进程本机监听
RELAY_ENV="/etc/node-relay/relay.env"
RELAY_CFG="/etc/node-relay/config.yaml"
NGINX_STREAM_CONF="/etc/nginx/relay-stream.conf"
NFT_CONF="/etc/nftables.d/relay.nft"

[ "$(id -u)" = "0" ] || { echo "ERROR: 请以 root 运行"; exit 1; }

PUBLIC_IP=$(curl -s4 --max-time 10 ifconfig.me || curl -s4 --max-time 10 ip.sb)
[ -z "$PUBLIC_IP" ] && { echo "FATAL: no public ip"; exit 1; }
log "public ip: $PUBLIC_IP   resource: $RESOURCE_BASE   api: $API_BASE"

export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq nginx nftables openssl curl ca-certificates >/dev/null 2>&1
apt-get install -y -qq libnginx-mod-stream >/dev/null 2>&1 || true   # 部分发行版 stream 模块随 nginx 内置，缺包忽略
log "deps installed (nginx/nftables/openssl/curl)"

# ========== 内核：转发开关 + BBR + 缓冲（中转机纯转发，取节点调优的精简子集）==========
log "tune kernel: ip_forward + BBR + 网络缓冲"
modprobe tcp_bbr 2>/dev/null || true
echo tcp_bbr > /etc/modules-load.d/bbr.conf
if grep -qw bbr /proc/sys/net/ipv4/tcp_available_congestion_control 2>/dev/null; then
  BBR_CC="net.ipv4.tcp_congestion_control = bbr"
else
  BBR_CC="# net.ipv4.tcp_congestion_control = bbr   # 内核不支持，已跳过"
fi
cat > /etc/sysctl.d/99-node-relay.conf <<EOF
# node-relay 中转机调优（由 install-relay.sh 生成）
net.ipv4.ip_forward = 1
net.core.default_qdisc = fq
${BBR_CC}
fs.file-max = 1048576
net.core.somaxconn = 32768
net.core.netdev_max_backlog = 32768
net.ipv4.tcp_max_syn_backlog = 32768
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.rmem_default = 2500000
net.core.wmem_default = 2500000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_mtu_probing = 1
EOF
sysctl --system >/dev/null 2>&1 || true
log "sysctl 生效: ip_forward=$(cat /proc/sys/net/ipv4/ip_forward) cc=$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)"

mkdir -p /etc/node-relay/ssl /var/lib/node-relay /etc/nftables.d /usr/local/bin

# ========== 凭据（幂等：已有则复用；只取 API_KEY/RELAY_ID/RELAY_NAME，不让旧 env 覆盖本次显式传入的 API_BASE 等）==========
if [ -f "$RELAY_ENV" ]; then
  API_KEY="${API_KEY:-$(grep -oP '^API_KEY=\K.*' "$RELAY_ENV" || true)}"
  RELAY_ID="${RELAY_ID:-$(grep -oP '^RELAY_ID=\K.*' "$RELAY_ENV" || true)}"
  RELAY_NAME="${RELAY_NAME:-$(grep -oP '^RELAY_NAME=\K.*' "$RELAY_ENV" || true)}"
fi
API_KEY="${API_KEY:-$(openssl rand -hex 16)}"
cat > "$RELAY_ENV" <<EOF
PUBLIC_IP=${PUBLIC_IP}
API_KEY=${API_KEY}
RELAY_API_PORT=${RELAY_API_PORT}
RESOURCE_BASE=${RESOURCE_BASE}
API_BASE=${API_BASE}
RELAY_ID=${RELAY_ID:-}
RELAY_NAME=${RELAY_NAME:-}
EOF
chmod 600 "$RELAY_ENV"

# ========== 二进制（同节点 agent，relay 子命令；先停服务避免 ETXTBSY）==========
systemctl stop node-relay 2>/dev/null || true
curl -fsSL -o /usr/local/bin/node-agent "${RESOURCE_BASE}/bin/agent" && chmod +x /usr/local/bin/node-agent
log "node-agent binary fetched"

# ========== 自签证书（api 信任所有证书，CN=公网 IP 即可）==========
if [ ! -s /etc/node-relay/ssl/cert.pem ]; then
  openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
    -keyout /etc/node-relay/ssl/key.pem -out /etc/node-relay/ssl/cert.pem -days 3650 \
    -subj "/CN=${PUBLIC_IP}" >/dev/null 2>&1
  log "self-signed cert -> /etc/node-relay/ssl/"
fi

# ========== nginx：stream include（顶层）+ 管理端点 https 反代 ==========
# 占位空 stream 块（守护进程启动后会按目标表重写；为空时不监听 443）
[ -f "$NGINX_STREAM_CONF" ] || printf 'stream {\n}\n' > "$NGINX_STREAM_CONF"
if ! grep -q "relay-stream.conf" /etc/nginx/nginx.conf; then
  printf '\n# node-relay: Reality/TCP 按 SNI 分流（由 node-agent relay 托管）\ninclude %s;\n' "$NGINX_STREAM_CONF" >> /etc/nginx/nginx.conf
fi
cat > /etc/nginx/sites-available/relay-api <<EOF
# api → 中转机管理端点（PUT/GET /relay/targets, GET /relay/health）
server {
    listen 0.0.0.0:${RELAY_API_PORT} ssl;
    server_name _;
    ssl_certificate     /etc/node-relay/ssl/cert.pem;
    ssl_certificate_key /etc/node-relay/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    client_max_body_size 4m;
    location / { proxy_pass http://${RELAY_LISTEN}; proxy_set_header Host \$host; proxy_read_timeout 120s; proxy_send_timeout 120s; }
}
EOF
ln -sf /etc/nginx/sites-available/relay-api /etc/nginx/sites-enabled/relay-api
if ! nginx -t >/dev/null 2>&1; then
  echo "FATAL: nginx -t 失败。多半是缺少 stream/ssl_preread 模块，请安装 libnginx-mod-stream（或 nginx-full）后重跑："
  nginx -t 2>&1 | tail -5
  exit 1
fi
systemctl enable nginx >/dev/null 2>&1 || true
systemctl restart nginx
log "nginx: $(systemctl is-active nginx) (stream include + :${RELAY_API_PORT} https 反代)"

# ========== nftables：规则文件占位 + 开机 include ==========
[ -f "$NFT_CONF" ] || cat > "$NFT_CONF" <<'EOF'
#!/usr/sbin/nft -f
# placeholder, managed by node-agent relay
table ip node_relay
delete table ip node_relay
table ip node_relay {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;
    }
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
    }
}
EOF
nft -f "$NFT_CONF"
[ -f /etc/nftables.conf ] || printf '#!/usr/sbin/nft -f\nflush ruleset\n' > /etc/nftables.conf
if ! grep -q 'nftables.d/relay.nft' /etc/nftables.conf; then
  printf '\n# node-relay: hy2/UDP 端口 DNAT（由 node-agent relay 托管）\ninclude "%s"\n' "$NFT_CONF" >> /etc/nftables.conf
fi
# ufw 激活时不 enable nftables.service：其 `flush ruleset` 开机会清掉 ufw 规则；改由守护进程启动时重放规则。
UFW_ACTIVE=""
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then UFW_ACTIVE=1; fi
if [ -z "$UFW_ACTIVE" ]; then
  systemctl enable nftables >/dev/null 2>&1 || true
  log "nftables.service enabled（开机加载 /etc/nftables.conf 含 relay.nft）"
else
  log "ufw 处于激活状态：跳过 enable nftables.service，规则由 node-relay 启动时重放"
fi

# ========== 防火墙（best-effort）：转发放行 + 管理/入口端口 ==========
if command -v ufw >/dev/null 2>&1; then
  sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw 2>/dev/null || true
  ufw allow 443/tcp                >/dev/null 2>&1 || true
  ufw allow ${RELAY_API_PORT}/tcp  >/dev/null 2>&1 || true
  ufw allow 22/tcp                 >/dev/null 2>&1 || true
  [ -n "$UFW_ACTIVE" ] && ufw reload >/dev/null 2>&1 || true
fi

# ========== 守护进程配置 + systemd ==========
cat > "$RELAY_CFG" <<EOF
listen: "${RELAY_LISTEN}"
api_key: "${API_KEY}"
targets_path: "/var/lib/node-relay/targets.json"
nginx:
  conf_path: "${NGINX_STREAM_CONF}"
  test_cmd: "nginx -t"
  reload_cmd: "systemctl reload nginx"
nft:
  conf_path: "${NFT_CONF}"
  apply_cmd: "nft -f ${NFT_CONF}"
EOF
chmod 600 "$RELAY_CFG"
cat > /etc/systemd/system/node-relay.service <<'EOF'
[Unit]
Description=node-agent relay (proxy entry: SNI/port forwarding manager)
After=network-online.target nginx.service nftables.service
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/node-agent relay -c /etc/node-relay/config.yaml
Restart=on-failure
RestartSec=3
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now node-relay >/dev/null 2>&1
systemctl restart node-relay
sleep 2
HEALTH=$(curl -s --max-time 5 -H "Hiddify-API-Key: ${API_KEY}" "http://${RELAY_LISTEN}/relay/health" || true)
echo "$HEALTH" | grep -q '"status":200' && log "node-relay: $(systemctl is-active node-relay) health=$HEALTH" \
  || { echo "FATAL: node-relay 健康检查失败：${HEALTH:-<无响应>}"; journalctl -u node-relay --no-pager -n 20; exit 1; }
HEALTH_EXT=$(curl -sk --max-time 5 -H "Hiddify-API-Key: ${API_KEY}" "https://127.0.0.1:${RELAY_API_PORT}/relay/health" || true)
echo "$HEALTH_EXT" | grep -q '"status":200' && log "https 反代 :${RELAY_API_PORT} OK" || log "WARN: https 反代 :${RELAY_API_PORT} 探活失败（api 将无法下发目标表），请检查 nginx"

# ========== 注册到 api 后台（交互；已注册则跳过）==========
if [ -n "${RELAY_ID:-}" ]; then
  log "已注册过（RELAY_ID=${RELAY_ID}，名称 ${RELAY_NAME}），跳过注册。如需重新注册请先清空 ${RELAY_ENV} 里的 RELAY_ID"
else
  echo
  echo "=== 注册中转机到 api 后台：$API_BASE ==="
  echo "本中转机 host = ${PUBLIC_IP}:${RELAY_API_PORT}"
  ADMIN_USER="${ADMIN_USER:-}"; ADMIN_PASS="${ADMIN_PASS:-}"; ADMIN_TOTP="${ADMIN_TOTP:-}"
  [ -z "$ADMIN_USER" ] && read -rp "① 管理员账号: " ADMIN_USER </dev/tty
  [ -z "$ADMIN_PASS" ] && { read -rsp "① 管理员密码: " ADMIN_PASS </dev/tty; echo; }
  LOGIN=$(curl -s --max-time 20 -X POST "$API_BASE/api/auth/admin/login" \
    -H 'Content-Type: application/json' \
    -d "{\"username\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASS}\"}")
  TOKEN=""
  # 已开两步验证的管理员，密码正确但未传动态码时 api 返回 requireTotp=true（不发 token），需带 totpCode 重登
  if echo "$LOGIN" | grep -q '"requireTotp":true'; then
    for i in 1 2 3; do
      [ -z "$ADMIN_TOTP" ] && read -rp "① 谷歌验证码: " ADMIN_TOTP </dev/tty
      LOGIN=$(curl -s --max-time 20 -X POST "$API_BASE/api/auth/admin/login" \
        -H 'Content-Type: application/json' \
        -d "{\"username\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASS}\",\"totpCode\":\"${ADMIN_TOTP}\"}")
      TOKEN=$(echo "$LOGIN" | grep -oE '"token":"[^"]*"' | head -1 | sed 's/.*"token":"//;s/"$//')
      [ -n "$TOKEN" ] && break
      echo "  ✗ 动态码错误或已过期（${i}/3）"
      ADMIN_TOTP=""
    done
  else
    TOKEN=$(echo "$LOGIN" | grep -oE '"token":"[^"]*"' | head -1 | sed 's/.*"token":"//;s/"$//')
  fi
  [ -z "$TOKEN" ] && { echo "ERROR: 登录失败：$LOGIN"; exit 1; }
  echo "  ✓ 登录成功"

  RELAY_NAME="${RELAY_NAME:-}"
  if [ -z "$RELAY_NAME" ]; then
    read -rp "② 中转机名称 [relay-${PUBLIC_IP}]: " RELAY_NAME </dev/tty
    RELAY_NAME="${RELAY_NAME:-relay-${PUBLIC_IP}}"
  fi
  REG=$(curl -s -w $'\n%{http_code}' --max-time 60 -X POST "$API_BASE/api/admin/relays" -H "Authorization: Bearer $TOKEN" \
    -H 'Content-Type: application/json' \
    -d "{\"name\":\"${RELAY_NAME}\",\"host\":\"${PUBLIC_IP}:${RELAY_API_PORT}\",\"apiKey\":\"${API_KEY}\"}")
  RCODE=$(printf '%s' "$REG" | tail -n1)
  RBODY=$(printf '%s' "$REG" | sed '$d')
  # 未绑定谷歌身份验证器的管理员会被 AdminTotpEnforcementFilter 拦 403
  if [ "$RCODE" = "403" ] && printf '%s' "$RBODY" | grep -q '谷歌身份验证器'; then
    echo "ERROR: 该管理员尚未绑定谷歌身份验证器，管理端接口已被限制。请先在管理后台 Web 页完成两步验证绑定，再重新运行本脚本。"
    exit 1
  fi
  if printf '%s' "$RBODY" | grep -q '"code":200'; then
    RELAY_ID=$(printf '%s' "$RBODY" | grep -oE '"id":[0-9]+' | head -1 | sed 's/"id"://')
    sed -i "s|^RELAY_ID=.*|RELAY_ID=${RELAY_ID}|; s|^RELAY_NAME=.*|RELAY_NAME=${RELAY_NAME}|" "$RELAY_ENV"
    echo "✓ 注册成功：「${RELAY_NAME}」 host=${PUBLIC_IP}:${RELAY_API_PORT} id=${RELAY_ID:-?}"
  else
    echo "ERROR: 注册失败（HTTP ${RCODE}）：${RBODY:-<无响应>}"
    echo "       本机已装好，可稍后重跑本脚本仅做注册（会复用现有 API_KEY）。"
    exit 1
  fi
fi

echo "==== RESULT ===="
grep -v '^API_KEY=' "$RELAY_ENV"
echo "nginx:      $(systemctl is-active nginx)"
echo "node-relay: $(systemctl is-active node-relay)"
echo "--- listen ---"
ss -tlnp 2>/dev/null | grep -E ":(443|${RELAY_API_PORT}) " || echo "(443 尚未监听：目标表为空时不监听，属正常)"
echo "==== DONE ===="
echo "后续：在主节点上运行 install.sh，注册步骤选择本中转机即可；api 会自动把目标表下发到这里。"
