#!/usr/bin/env bash
#
# nbp-relay-bootstrap.sh — 把一台机器变成 NBP 现场中继节点
#
# 一条命令（在目标机器上以 root 运行）：
#   curl -fsSL http://45.33.50.129/lz-relay.sh | sudo bash -s -- --port 10022
#
# 由跳板机 nginx 直供（带 no-store，不会拿到缓存旧版），不经过短链服务。
# 脚本自带 x86_64 与 arm64 两版 sing-box，按 uname -m 自动选。
#
# 跑完以后这台机器会：
#   1) 装一个 sing-box，提供本地出口代理（经跳板机出网）
#   2) 向跳板机开两条反向隧道：
#        跳板机 127.0.0.1:PORT     -> 本机 22    （SSH，登录本机）
#        跳板机 127.0.0.1:P+58     -> 本机 1080  （SOCKS5，以本机为出口进它所在内网）
#   3) 把运维方的公钥装进 root，运维方可直接免密登录本机
#
# 控制面：先直连跳板机；连续 3 次连不上才回落到 hysteria2 隧道。
# 端口冲突（跳板机上已有同号中继）会自动往后找一个能用的口，不计入网络故障。
#
# 常用参数：
#   --port N          反向端口，默认 10022
#   --lan-port N      内网 SOCKS 反向端口，默认 = port + 58
#   --jump-host H     跳板机，默认 45.33.50.129
#   --jump-port N     跳板机 SSH 口，默认 22
#   --no-fallback     关掉 hy2 回落（本地测试用）
#
# 卸载：bash /usr/local/lib/nbp-relay/uninstall.sh
#
set -euo pipefail

# 原始参数留一份：非 root 时提权重跑要用，那会儿 $@ 已经被 shift 吃掉了
ORIG_ARGS=("$@")

RELAY_PORT=10022
LAN_PORT=""
JUMP_HOST='40.123.219.102'
JUMP_PORT=22
HY2_HOST=""
HY2_PORT=20221
HY2_PASSWORD='R5PGxpYlGnFJ1f3uvobRWKzRDMDefvz7TqgrHFHtscU='
FALLBACK=1
PORT_SCAN=8

while [ $# -gt 0 ]; do
  case "$1" in
    --port) RELAY_PORT="$2"; shift 2 ;;
    --lan-port) LAN_PORT="$2"; shift 2 ;;
    --jump-host) JUMP_HOST="$2"; shift 2 ;;
    --jump-port) JUMP_PORT="$2"; shift 2 ;;
    --hy2-host) HY2_HOST="$2"; shift 2 ;;
    --hy2-port) HY2_PORT="$2"; shift 2 ;;
    --no-fallback) FALLBACK=0; shift ;;
    --port-scan) PORT_SCAN="$2"; shift 2 ;;
    *) echo "未知参数: $1" >&2; exit 2 ;;
  esac
done

[ -n "$LAN_PORT" ] || LAN_PORT=$(( RELAY_PORT + 58 ))
# hysteria2 服务端只在美西 VPS 上；跳板机换了（例如 Azure VM）它不会跟着变，
# 所以要单独给一个默认值，否则回落路径会指向一个没有 hy2 的地址。
[ -n "$HY2_HOST" ] || HY2_HOST="45.33.50.129"

EGRESS_PORT=12350
LANEXIT_PORT=1080
DIR=/etc/nbp-relay
LIB=/usr/local/lib/nbp-relay
BIN=/usr/local/bin/sing-box
SBX_URL_X86='http://40.123.219.102/lz-relay-singbox-amd64.gz'
SBX_URL_ARM='http://40.123.219.102/lz-relay-singbox-arm64.gz'
LITE_URL='http://40.123.219.102/lz-relay-lite.sh'
LOG=/var/log/nbp-relay.log

SELF_URL='http://40.123.219.102/lz-relay.sh'

if [ "$(id -u)" != "0" ]; then
  # 能免密 sudo 就自己提权重跑；否则把正确的命令打出来，别让对方猜
  if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
    echo "[nbp-relay] 当前不是 root，用 sudo 重新执行"
    tmp=$(mktemp)
    if curl -fsSL "$SELF_URL" -o "$tmp"; then
      exec sudo -n bash "$tmp" "${ORIG_ARGS[@]}"
    fi
    echo "重新下载失败，请手动跑：" >&2
    echo "  curl -fsSL $SELF_URL | sudo bash -s -- "${ORIG_ARGS[*]}"" >&2
    exit 1
  fi
  echo "需要 root 权限（装 systemd 服务、写 root 的 authorized_keys）。" >&2
  echo "当前用户 $(id -un) 不能免密 sudo。请在目标机器上用 root 跑，或：" >&2
  echo "  curl -fsSL $SELF_URL | sudo bash -s -- "${ORIG_ARGS[*]}"" >&2
  if [ -f /.dockerenv ] || grep -qa container= /proc/1/environ 2>/dev/null; then
    echo "看起来这是个容器（例如 Azure Cloud Shell）：容器里 sudo 被 no_new_privs 挡住，" >&2
    echo "中继也不该建在这里。请在目标 VM 上执行，或用 Azure 通道在 VM 内执行：" >&2
    echo "  az vm run-command invoke -g lingzhua -n lz-relay --command-id RunShellScript --scripts curl-...(见下)" >&2
    echo "  其中 scripts 的内容就是本行命令本身。" >&2
  fi
  exit 1
fi

mkdir -p "$DIR" "$LIB"
touch "$LOG"
log() { echo "[nbp-relay] $*" | tee -a "$LOG"; }

# 下载 sing-box 失败 / 太慢 / 二进制跑不起来时，自动改用轻量版。
# 轻量版不做「经跳板机出网」和 hy2 回落，但反向隧道 + 内网 SOCKS 一样有，
# 这两样才是真正要用的；它也不下载任何二进制，<20KB 就能跑完。
fallback_lite() {
  log "改用轻量版：$1"
  # 轻量版不认这几个只属于完整版的参数，过滤掉再传
  LITE_ARGS=()
  skip=0
  for a in "${ORIG_ARGS[@]}"; do
    if [ "$skip" = "1" ]; then skip=0; continue; fi
    case "$a" in
      --hy2-host|--hy2-port) skip=1; continue ;;
      --no-fallback) continue ;;
    esac
    LITE_ARGS+=("$a")
  done
  [ -n "$LITE_URL" ] || { log "轻量版地址为空，无法自动切换"; exit 1; }
  lite_tmp=$(mktemp)
  if curl -fsSL --max-time 120 -o "$lite_tmp" "$LITE_URL"; then
    log "已切到轻量版，参数：${LITE_ARGS[*]:-（默认）}"
    exec bash "$lite_tmp" "${LITE_ARGS[@]}"
  fi
  log "轻量版也拉不下来，请手动试："
  log "  curl -fsSL $LITE_URL | sudo bash -s -- ${LITE_ARGS[*]}"
  exit 1
}

case "$(uname -m)" in
  x86_64|amd64) SBX_URL="$SBX_URL_X86" ;;
  aarch64|arm64) SBX_URL="$SBX_URL_ARM" ;;
  *)
    fallback_lite "不支持的架构：$(uname -m)"
    ;;
esac

log "跳板机 $JUMP_HOST:$JUMP_PORT，反向端口 $RELAY_PORT / $LAN_PORT"

# ---------- 1. sing-box ----------
if [ ! -x "$BIN" ]; then
  log "下载 sing-box（$(uname -m)）"
  tmpd=$(mktemp -d)
  # --speed-limit/--speed-time：低于 20KB/s 持续 60 秒就放弃，
  # 免得在很差的国际链路上干等满 900 秒（现场实测有 3~13KB/s 的情况）。
  if ! curl -fsSL --max-time 900 --speed-limit 20480 --speed-time 60 \
        -o "$tmpd/sbx.gz" "$SBX_URL"; then
    fallback_lite "sing-box 下载失败或太慢（$SBX_URL）"
  fi
  if ! gzip -dc "$tmpd/sbx.gz" > "$BIN"; then
    fallback_lite "sing-box 解压失败（文件可能不完整）"
  fi
  chmod 0755 "$BIN"
  rm -rf "$tmpd"
fi
if ! "$BIN" version >/dev/null 2>&1; then
  rm -f "$BIN"
  fallback_lite "sing-box 二进制跑不起来（架构不符？）"
fi
log "sing-box: $("$BIN" version | head -n 1)"

# ---------- 2. 配置 ----------
printf '%s' 'LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBb0N4VFBtWDIwcURyVGp0RWU4Zzl0UWE3TEZBWEw5OXBIa1dQR1ZPdml0QUFBQUpqQXZjcm93TDNLCjZBQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQW9DeFRQbVgyMHFEclRqdEVlOGc5dFFhN0xGQVhMOTlwSGtXUEdWT3ZpdEEKQUFBRUNENjA1YU5QU05HRkl2QkhRNWg4Tms2bDF5MkI2Nm0rWW1GV1ZsdHkrQnN5Z0xGTStaZmJTb090T08wUjd5RDIxQgpyc3NVQmN2MzJrZVJZOFpVNitLMEFBQUFFMjVpY0MxeVpXeGhlUzEyY0hOZmNtVnNZWGtCQWc9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K' | base64 -d > "$DIR/relay_key"
chmod 600 "$DIR/relay_key"

cat > "$DIR/ports.env" <<PENV
RELAY_PORT=$RELAY_PORT
LAN_PORT=$LAN_PORT
JUMP_HOST=$JUMP_HOST
JUMP_PORT=$JUMP_PORT
HY2_HOST=$HY2_HOST
HY2_PORT=$HY2_PORT
EGRESS_PORT=$EGRESS_PORT
FALLBACK=$FALLBACK
PORT_SCAN=$PORT_SCAN
PENV
cp -f "$DIR/ports.env" "$DIR/active.env"

cat > "$DIR/config.json" <<JSONCFG
{
  "log": { "level": "warn", "output": "/var/log/nbp-relay-singbox.log" },
  "inbounds": [
    { "type": "mixed", "tag": "egress-in", "listen": "127.0.0.1", "listen_port": $EGRESS_PORT },
    { "type": "socks", "tag": "lan-exit", "listen": "127.0.0.1", "listen_port": $LANEXIT_PORT }
  ],
  "outbounds": [
    {
      "type": "hysteria2",
      "tag": "vps-hy2",
      "server": "$HY2_HOST",
      "server_port": $HY2_PORT,
      "password": "$HY2_PASSWORD",
      "tls": { "enabled": true, "server_name": "vps-egress.lingzhua.local", "insecure": true }
    },
    { "type": "direct", "tag": "direct" }
  ],
  "route": {
    "rules": [
      { "inbound": ["lan-exit"], "outbound": "direct" },
      { "ip_cidr": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10"], "outbound": "direct" }
    ],
    "final": "vps-hy2"
  }
}
JSONCFG

"$BIN" check -c "$DIR/config.json"
log "配置校验通过"

# ---------- 3. SOCKS5 stdio 代理（给 ssh ProxyCommand 用）----------
cat > "$LIB/socks-proxy.py" <<'PYEOF'
#!/usr/bin/env python3
# SOCKS5 stdio 代理，供 ssh ProxyCommand 使用。
# 用法: socks-proxy.py <host> <port> [proxy_host] [proxy_port]
import os
import select
import socket
import sys


def main():
    host = sys.argv[1]
    port = int(sys.argv[2])
    phost = sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1"
    pport = int(sys.argv[4]) if len(sys.argv) > 4 else 12350

    s = socket.create_connection((phost, pport), 15)
    s.sendall(bytes([5, 1, 0]))
    if s.recv(2) != bytes([5, 0]):
        sys.exit("socks: handshake failed")

    hb = host.encode()
    s.sendall(bytes([5, 1, 0, 3, len(hb)]) + hb + port.to_bytes(2, "big"))
    r = s.recv(4)
    if len(r) < 4 or r[1] != 0:
        sys.exit("socks: connect failed " + repr(r))
    atyp = r[3]
    if atyp == 1:
        s.recv(4)
    elif atyp == 3:
        s.recv(s.recv(1)[0])
    elif atyp == 4:
        s.recv(16)
    s.recv(2)

    while True:
        rl, _, _ = select.select([s, 0], [], [], 300)
        for f in rl:
            if f is s:
                d = s.recv(65536)
                if not d:
                    return
                os.write(1, d)
            else:
                d = os.read(0, 65536)
                if not d:
                    return
                s.sendall(d)


main()
PYEOF
chmod 0755 "$LIB/socks-proxy.py"

# ---------- 4. 反向隧道守护 ----------
cat > "$LIB/tunnel.sh" <<'TUNEOF'
#!/bin/bash
#
# NBP relay 反向隧道守护
#   - 把本机 22 与 1080 反代到跳板机的 127.0.0.1 上
#   - 端口被占   -> 自动往后找可用口，不计入网络故障
#   - 连不上     -> 连续 3 次后 direct <-> hy2 互换
#
set -u

DIR=/etc/nbp-relay
KEY="$DIR/relay_key"
KH="$DIR/known_hosts"
PY=/usr/local/lib/nbp-relay/socks-proxy.py
LOG=/var/log/nbp-relay-tunnel.log
ACTIVE="$DIR/active.env"

. "$DIR/ports.env"

say() { echo "$(date -Is) $*" | tee -a "$LOG"; }

SSH_OPTS="-N -T -i $KEY -o BatchMode=yes -o ExitOnForwardFailure=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$KH -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o TCPKeepAlive=yes -o ConnectTimeout=20"

cur_port=$RELAY_PORT
cur_lan=$LAN_PORT
mode=direct
netfails=0
DOWN_DUR=0

save_active() {
  sed -i "s/^RELAY_PORT=.*/RELAY_PORT=$cur_port/;s/^LAN_PORT=.*/LAN_PORT=$cur_lan/" "$ACTIVE" 2>/dev/null || true
}

# 起一条隧道；$1=port $2=lanport
# 返回 0=已连上  1=端口被占  2=跳板机拒绝转发  3=连不上
attempt() {
  TERR=$(mktemp)
  if [ "$mode" = "hy2" ]; then
    ssh $SSH_OPTS -o ProxyCommand="$PY %h %p 127.0.0.1 $EGRESS_PORT" \
      -R 127.0.0.1:"$1":localhost:22 \
      -R 127.0.0.1:"$2":127.0.0.1:1080 \
      -p "$JUMP_PORT" root@"$JUMP_HOST" >/dev/null 2>"$TERR" &
  else
    ssh $SSH_OPTS \
      -R 127.0.0.1:"$1":localhost:22 \
      -R 127.0.0.1:"$2":127.0.0.1:1080 \
      -p "$JUMP_PORT" root@"$JUMP_HOST" >/dev/null 2>"$TERR" &
  fi
  TPID=$!
  sleep 6
  if kill -0 "$TPID" 2>/dev/null; then
    return 0
  fi
  wait "$TPID" 2>/dev/null
  ERRTXT=$(cat "$TERR" 2>/dev/null)
  rm -f "$TERR"
  case "$ERRTXT" in
    *"remote port forwarding failed for listen port"*) return 1 ;;
    *"Permission denied"*|*"administratively prohibited"*|*"port forwarding failed"*) return 2 ;;
  esac
  return 3
}

await_down() {
  local start=$(date +%s)
  wait "$TPID" 2>/dev/null
  rm -f "$TERR"
  DOWN_DUR=$(( $(date +%s) - start + 6 ))
  say "tunnel down mode=$mode port=$cur_port dur=${DOWN_DUR}s"
}

say "start jump=$JUMP_HOST:$JUMP_PORT port=$cur_port/$cur_lan"

while :; do
  attempt "$cur_port" "$cur_lan"
  st=$?

  if [ "$st" = "0" ]; then
    say "tunnel up mode=$mode port=$cur_port"
    await_down
    if [ "$DOWN_DUR" -ge 15 ]; then netfails=0; else netfails=$(( netfails + 1 )); fi

  elif [ "$st" = "1" ]; then
    say "port $cur_port busy on jump host, scanning"
    moved=0
    i=0
    while [ "$i" -lt "$PORT_SCAN" ]; do
      i=$(( i + 1 ))
      cur_port=$(( cur_port + 1 ))
      cur_lan=$(( cur_port + 58 ))
      attempt "$cur_port" "$cur_lan"
      st2=$?
      if [ "$st2" = "0" ]; then moved=1; break; fi
      if [ "$st2" != "1" ]; then break; fi
    done
    if [ "$moved" = "1" ]; then
      save_active
      say "bound port=$cur_port/$cur_lan"
      await_down
      if [ "$DOWN_DUR" -ge 15 ]; then netfails=0; fi
    else
      say "no free port in $PORT_SCAN tries (jump host key may not permit these ports)"
      netfails=0
      sleep 20
      continue
    fi

  elif [ "$st" = "2" ]; then
    say "jump host refused forwarding for port $cur_port (permitlisten)"
    netfails=0
    sleep 30
    continue

  else
    say "cannot reach jump host mode=$mode"
    netfails=$(( netfails + 1 ))
  fi

  if [ "$FALLBACK" = "1" ] && [ "$netfails" -ge 3 ]; then
    if [ "$mode" = "direct" ]; then mode=hy2; else mode=direct; fi
    say "switch mode=$mode"
    netfails=0
  fi
  sleep 5
done
TUNEOF
chmod 0755 "$LIB/tunnel.sh"

# ---------- 5. systemd ----------
cat > /etc/systemd/system/nbp-relay.service <<'UNITEOF'
[Unit]
Description=NBP relay sing-box (hysteria2 egress + LAN exit socks)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/sing-box run -c /etc/nbp-relay/config.json
Restart=always
RestartSec=3
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
UNITEOF

cat > /etc/systemd/system/nbp-relay-tunnel.service <<'UNITEOF'
[Unit]
Description=NBP relay reverse tunnel to jump host
After=network-online.target nbp-relay.service
Wants=network-online.target nbp-relay.service

[Service]
Type=simple
ExecStart=/usr/local/lib/nbp-relay/tunnel.sh
Restart=always
RestartSec=5
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
UNITEOF

# ---------- 6. 卸载脚本 ----------
cat > "$LIB/uninstall.sh" <<'UNEOF'
#!/bin/bash
systemctl disable --now nbp-relay-tunnel.service nbp-relay.service 2>/dev/null || true
rm -f /etc/systemd/system/nbp-relay.service /etc/systemd/system/nbp-relay-tunnel.service
systemctl daemon-reload
rm -rf /etc/nbp-relay /usr/local/lib/nbp-relay
rm -f /var/log/nbp-relay.log /var/log/nbp-relay-tunnel.log /var/log/nbp-relay-singbox.log
sed -i '/nbp-relay-keys/d' /root/.ssh/authorized_keys 2>/dev/null || true
echo "nbp-relay uninstalled"
UNEOF
chmod 0755 "$LIB/uninstall.sh"

# ---------- 7. 运维方公钥（多把，可重复执行）----------
mkdir -p /root/.ssh
chmod 700 /root/.ssh
touch /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
sed -i '/nbp-relay-keys/d' /root/.ssh/authorized_keys 2>/dev/null || true
cat >> /root/.ssh/authorized_keys <<'KEYSEOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF0Jjvrua745+HeqePb2jgXjM9FpeCSrW7JqDTqMG2b1 nbp-relay-keys-op
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII+GuP9iJY2zIrLFC/BA4loJprVgzbdhoceU8gbaYCEd nbp-relay-keys-57
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKFouA7t+5DHBVc2y7aADaBJhEA/50xtsgAu/3Qbq5b/ nbp-relay-keys-vps
KEYSEOF
log "已装入公钥 $(grep -c 'nbp-relay-keys' /root/.ssh/authorized_keys) 把"

# ---------- 8. 起服务 + 自检 ----------
systemctl daemon-reload
systemctl enable nbp-relay.service nbp-relay-tunnel.service >/dev/null 2>&1
systemctl restart nbp-relay.service
sleep 3

if ! systemctl is-active --quiet nbp-relay.service; then
  log "sing-box 没起来，看 /var/log/nbp-relay-singbox.log"
  exit 1
fi
log "sing-box 已启动"

if ss -tln 2>/dev/null | grep -q "127.0.0.1:$EGRESS_PORT "; then
  log "出口代理就绪 127.0.0.1:$EGRESS_PORT"
else
  log "警告：出口代理端口没在听"
fi

systemctl restart nbp-relay-tunnel.service
sleep 14

rootlogin=$(sshd -T 2>/dev/null | grep -i '^permitrootlogin' | awk '{print $2}')
if [ "$rootlogin" = "no" ]; then
  log "警告：本机 sshd 禁止 root 登录（PermitRootLogin no），免密登录会失败"
fi

if [ -r "$DIR/active.env" ]; then
  . "$DIR/active.env"
fi
LANGPORT=$(( RELAY_PORT + 58 ))
MODE=$(grep -o 'mode=[a-z0-9]*' /var/log/nbp-relay-tunnel.log 2>/dev/null | tail -1 | cut -d= -f2)
log "控制面模式：${MODE:-未知}；实际反向端口：$RELAY_PORT / $LANGPORT"

cat <<DONE

==================== nbp-relay 就绪 ====================
本机            : $(hostname)   $(hostname -I 2>/dev/null)
跳板机          : $JUMP_HOST:$JUMP_PORT
控制面          : 直连（连续失败才回落 hysteria2）
出口代理        : 127.0.0.1:$EGRESS_PORT   （经跳板机出网）
内网 SOCKS5     : 127.0.0.1:$LANEXIT_PORT  （以本机为出口）
反向到跳板机    : $JUMP_HOST 127.0.0.1:$RELAY_PORT  -> 本机 22
                  $JUMP_HOST 127.0.0.1:$LANGPORT  -> 本机 $LANEXIT_PORT

运维方连接方式（两种都免密）：

  1) 从跳板机上直接登本机：
       ssh -p $RELAY_PORT root@127.0.0.1

  2) 从 5.7 / 任意运维机经跳板机中转：
       ssh -f -N -L 12222:127.0.0.1:$RELAY_PORT -L 11080:127.0.0.1:$LANGPORT root@$JUMP_HOST
       ssh -p 12222 root@127.0.0.1
       curl --socks5-hostname 127.0.0.1:11080 http://<内网 BMC IP>/redfish/v1/

状态：systemctl status nbp-relay nbp-relay-tunnel
卸载：bash $LIB/uninstall.sh
=======================================================
DONE

