#!/bin/bash
# vless-client.sh - 代理客户端管理脚本 v2.0.0
# 支持协议: VLESS+Reality (Xray), Hysteria2 (Sing-box)
# 代理模式: SOCKS5 / 全局(iptables) / TPROXY(内核透明代理)
# 配置目录: /etc/vless-client

set -uo pipefail

#═══════════════════════════════════════════════════════════════════════════════
# 全局常量
#═══════════════════════════════════════════════════════════════════════════════
readonly VERSION="2.0.0"
readonly CFG="/etc/vless-client"
readonly NODES_DIR="$CFG/nodes"
readonly CURRENT_NODE_FILE="$CFG/current_node"
readonly MODE_FILE="$CFG/mode"
readonly PAUSED_FILE="$CFG/paused"

# 客户端端口
readonly SOCKS_PORT="10808"
readonly HTTP_PORT="10809"
readonly REDIR_PORT="10810"   # TCP 透明代理 (REDIRECT)
readonly TPROXY_PORT="10811"  # UDP 透明代理 (TPROXY)

# iptables 标记
# mark=100: 策略路由标记（本机出站 UDP 回环）
# mark=255: Xray/sing-box 出站标记（跳过重定向，防死循环）
readonly FWMARK_TPROXY="100"
readonly FWMARK_PROXY="255"
readonly ROUTE_TABLE="100"

# 服务名
readonly SVC_XRAY="vless-client-xray"
readonly SVC_SINGBOX="vless-client-singbox"
readonly SVC_TPROXY="vless-client-tproxy"
readonly SVC_GLOBAL="vless-client-global"
readonly SVC_WATCHDOG="vless-client-watchdog"

#═══════════════════════════════════════════════════════════════════════════════
# 颜色 & UI
#═══════════════════════════════════════════════════════════════════════════════
readonly R='\033[0;31m' G='\033[0;32m' Y='\033[0;33m' B='\033[0;34m'
readonly C='\033[0;36m' W='\033[1;37m' D='\033[0;90m' NC='\033[0m'

_header() {
    clear
    echo -e "${C}╔══════════════════════════════════════╗${NC}"
    echo -e "${C}║${NC}  ${W}VLESS Client${NC} ${D}v${VERSION}${NC}"
    echo -e "${C}╚══════════════════════════════════════╝${NC}"
}

_line()  { echo -e "  ${D}──────────────────────────────────────${NC}"; }
_dline() { echo -e "  ${C}══════════════════════════════════════${NC}"; }
_ok()    { echo -e "  ${G}✓${NC} $*"; }
_err()   { echo -e "  ${R}✗${NC} $*" >&2; }
_warn()  { echo -e "  ${Y}!${NC} $*"; }
_info()  { echo -e "  ${C}→${NC} $*"; }
_item()  { echo -e "  ${G}$1${NC}) $2"; }
_pause() { echo ""; read -rp "  按 Enter 继续..." _; }

#═══════════════════════════════════════════════════════════════════════════════
# 系统检测
#═══════════════════════════════════════════════════════════════════════════════
check_root() {
    [[ $EUID -ne 0 ]] && { _err "需要 root 权限"; exit 1; }
}

detect_distro() {
    if [[ -f /etc/alpine-release ]]; then
        DISTRO="alpine"
    elif [[ -f /etc/os-release ]] && grep -qi "ubuntu" /etc/os-release; then
        DISTRO="ubuntu"
    elif [[ -f /etc/os-release ]] && grep -qi "centos\|rhel\|fedora" /etc/os-release; then
        DISTRO="centos"
    else
        DISTRO="debian"
    fi
    export DISTRO
}

detect_distro

#═══════════════════════════════════════════════════════════════════════════════
# 服务管理
#═══════════════════════════════════════════════════════════════════════════════
svc() {
    local action="$1" name="$2"
    if [[ "$DISTRO" == "alpine" ]]; then
        case "$action" in
            start|restart|stop) rc-service "$name" "$action" >/dev/null 2>&1; return $? ;;
            enable)  rc-update add "$name" default >/dev/null 2>&1 ;;
            disable) rc-update del "$name" default >/dev/null 2>&1 ;;
            status)  rc-service "$name" status 2>/dev/null | grep -q "started" ;;
        esac
    else
        case "$action" in
            start|restart|stop) systemctl "$action" "$name" >/dev/null 2>&1; return $? ;;
            enable)  systemctl enable "$name" >/dev/null 2>&1 ;;
            disable) systemctl disable "$name" >/dev/null 2>&1 ;;
            status)
                local state; state=$(systemctl is-active "$name" 2>/dev/null)
                [[ "$state" == "active" || "$state" == "activating" ]]
                ;;
        esac
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 状态查询
#═══════════════════════════════════════════════════════════════════════════════
is_installed() {
    [[ -f "$CURRENT_NODE_FILE" && -f "$CFG/protocol" ]]
}

get_mode() {
    cat "$MODE_FILE" 2>/dev/null || echo "socks"
}

get_protocol() {
    cat "$CFG/protocol" 2>/dev/null || echo "vless"
}

get_mode_name() {
    case "$1" in
        tproxy) echo "TPROXY 透明代理 (TCP+UDP)" ;;
        global) echo "全局代理 (iptables TCP)" ;;
        socks)  echo "SOCKS5/HTTP 代理" ;;
        *)      echo "$1" ;;
    esac
}

get_protocol_name() {
    case "$1" in
        vless) echo "VLESS+Reality (Xray)" ;;
        hy2)   echo "Hysteria2 (Sing-box)" ;;
        *)     echo "$1" ;;
    esac
}

is_paused() {
    [[ -f "$PAUSED_FILE" ]]
}

#═══════════════════════════════════════════════════════════════════════════════
# 工具函数
#═══════════════════════════════════════════════════════════════════════════════
check_cmd() {
    command -v "$1" >/dev/null 2>&1
}

check_tproxy_support() {
    # 动态加载所需的内核模块
    modprobe xt_TPROXY 2>/dev/null || true
    modprobe xt_socket 2>/dev/null || true
    modprobe nf_tproxy_ipv4 2>/dev/null || true

    check_cmd iptables || return 1
    
    iptables -t mangle -N TEST_TPROXY 2>/dev/null || iptables -t mangle -F TEST_TPROXY 2>/dev/null
    
    if iptables -t mangle -A TEST_TPROXY -p udp -j TPROXY --on-port 1234 --tproxy-mark 1 2>/dev/null; then
        iptables -t mangle -D TEST_TPROXY -p udp -j TPROXY --on-port 1234 --tproxy-mark 1 2>/dev/null || true
        iptables -t mangle -X TEST_TPROXY 2>/dev/null || true
        return 0
    else
        iptables -t mangle -X TEST_TPROXY 2>/dev/null || true
        return 1
    fi
}

# 去除 IPv6 方括号，返回纯 IP
strip_brackets() {
    local ip="$1"
    ip="${ip#[}"
    ip="${ip%]}"
    echo "$ip"
}

sync_time() {
    _info "同步系统时间..."
    if check_cmd chronyc; then
        chronyc makestep >/dev/null 2>&1 || true
    elif check_cmd timedatectl; then
        timedatectl set-ntp true 2>/dev/null || true
    elif check_cmd ntpdate; then
        ntpdate -u pool.ntp.org >/dev/null 2>&1 || true
    fi
}

# TCP 延迟测试（对 UDP 协议跳过）
test_latency() {
    local host="$1" port="$2" proto="${3:-tcp}"
    if [[ "$proto" == "udp" ]]; then
        echo "UDP"
        return
    fi
    local start end ms
    start=$(date +%s%3N)
    if timeout 3 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null; then
        end=$(date +%s%3N)
        ms=$((end - start))
        echo "${ms}ms"
    else
        echo "超时"
    fi
}

get_outbound_ip() {
    local mode="$1"
    if [[ "$mode" == "socks" ]]; then
        curl -s --socks5-hostname "127.0.0.1:${SOCKS_PORT}" \
            --connect-timeout 8 ip.sb 2>/dev/null
    else
        curl -s --connect-timeout 8 ip.sb 2>/dev/null
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 安装依赖
#═══════════════════════════════════════════════════════════════════════════════
install_deps() {
    _info "安装基础依赖..."
    if [[ "$DISTRO" == "alpine" ]]; then
        apk add -q curl wget bash jq iptables ip6tables iproute2 \
            ca-certificates unzip 2>/dev/null || true
    elif [[ "$DISTRO" == "centos" ]]; then
        yum install -y -q curl wget jq iptables iproute \
            ca-certificates unzip 2>/dev/null || true
    else
        apt-get update -qq 2>/dev/null || true
        DEBIAN_FRONTEND=noninteractive apt-get install -y -q \
            curl wget jq iptables ip6tables iproute2 \
            ca-certificates unzip 2>/dev/null || true
    fi

    # 确认 iptables 模块已加载
    modprobe xt_TPROXY 2>/dev/null || true
    modprobe xt_socket 2>/dev/null || true
    modprobe nf_tproxy_ipv4 2>/dev/null || true
}

install_xray() {
    if check_cmd xray; then
        local ver; ver=$(xray version 2>/dev/null | head -1 | grep -oP '\d+\.\d+\.\d+' | head -1)
        _ok "Xray 已安装: v${ver:-未知}"
        return 0
    fi
    _info "安装 Xray..."
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64)  arch="64" ;;
        aarch64) arch="arm64-v8a" ;;
        armv7l)  arch="arm32-v7a" ;;
        *)       _err "不支持的架构: $arch"; return 1 ;;
    esac

    local tmp_dir; tmp_dir=$(mktemp -d)
    local url="https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-${arch}.zip"

    if ! curl -sL --retry 3 --retry-delay 2 "$url" -o "$tmp_dir/xray.zip"; then
        _err "Xray 下载失败"; rm -rf "$tmp_dir"; return 1
    fi
    if ! unzip -q "$tmp_dir/xray.zip" -d "$tmp_dir" 2>/dev/null; then
        _err "Xray 解压失败"; rm -rf "$tmp_dir"; return 1
    fi
    install -m 755 "$tmp_dir/xray" /usr/local/bin/xray
    rm -rf "$tmp_dir"
    _ok "Xray 安装完成: $(xray version 2>/dev/null | head -1)"
}

install_singbox() {
    if check_cmd sing-box; then
        local ver; ver=$(sing-box version 2>/dev/null | head -1 | grep -oP '\d+\.\d+\.\d+' | head -1)
        _ok "Sing-box 已安装: v${ver:-未知}"
        return 0
    fi
    _info "安装 Sing-box..."
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64)  arch="amd64" ;;
        aarch64) arch="arm64" ;;
        armv7l)  arch="armv7" ;;
        *)       _err "不支持的架构: $arch"; return 1 ;;
    esac

    # 获取最新版本号并校验
    local api_resp ver
    api_resp=$(curl -s --connect-timeout 10 \
        "https://api.github.com/repos/SagerNet/sing-box/releases/latest")
    ver=$(echo "$api_resp" | grep '"tag_name"' | head -1 | \
          sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\?\([^"]*\)".*/\1/')
    if [[ -z "$ver" || "$ver" =~ "message" ]]; then
        _err "获取 Sing-box 版本失败（API 限速？）"; return 1
    fi

    local tmp_dir; tmp_dir=$(mktemp -d)
    local url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/sing-box-${ver}-linux-${arch}.tar.gz"

    if ! curl -sL --retry 3 --retry-delay 2 "$url" -o "$tmp_dir/sb.tar.gz"; then
        _err "Sing-box 下载失败"; rm -rf "$tmp_dir"; return 1
    fi
    if ! tar -xzf "$tmp_dir/sb.tar.gz" -C "$tmp_dir" 2>/dev/null; then
        _err "Sing-box 解压失败"; rm -rf "$tmp_dir"; return 1
    fi
    local bin; bin=$(find "$tmp_dir" -name "sing-box" -type f | head -1)
    [[ -z "$bin" ]] && { _err "找不到 sing-box 二进制"; rm -rf "$tmp_dir"; return 1; }
    install -m 755 "$bin" /usr/local/bin/sing-box
    rm -rf "$tmp_dir"
    _ok "Sing-box 安装完成: v${ver}"
}

#═══════════════════════════════════════════════════════════════════════════════
# 生成客户端配置
#═══════════════════════════════════════════════════════════════════════════════

gen_vless_client_config() {
    local server_ip="$1" port="$2" uuid="$3" pubkey="$4" sid="$5" sni="$6"
    local clean_ip; clean_ip=$(strip_brackets "$server_ip")

    echo "$clean_ip" > "$CFG/server_ip"
    echo "$port"     > "$CFG/server_port"

    cat > "$CFG/config.json" <<EOF
{
  "log": { "loglevel": "warning" },
  "dns": {
    "servers": [
      {
        "address": "https://1.1.1.1/dns-query",
        "domains": ["geosite:geolocation-!cn"],
        "queryStrategy": "UseIPv4"
      },
      {
        "address": "114.114.114.114",
        "domains": ["geosite:cn", "geosite:private"],
        "queryStrategy": "UseIPv4"
      },
      { "address": "localhost" }
    ]
  },
  "inbounds": [
    {
      "tag": "socks",
      "port": ${SOCKS_PORT},
      "listen": "127.0.0.1",
      "protocol": "socks",
      "settings": { "udp": true }
    },
    {
      "tag": "http",
      "port": ${HTTP_PORT},
      "listen": "127.0.0.1",
      "protocol": "http"
    },
    {
      "tag": "tproxy-tcp",
      "port": ${REDIR_PORT},
      "listen": "0.0.0.0",
      "protocol": "dokodemo-door",
      "settings": { "network": "tcp", "followRedirect": true },
      "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] }
    },
    {
      "tag": "tproxy-udp",
      "port": ${TPROXY_PORT},
      "listen": "0.0.0.0",
      "protocol": "dokodemo-door",
      "settings": { "network": "udp", "followRedirect": true },
      "streamSettings": {
        "sockopt": { "tproxy": "tproxy", "mark": ${FWMARK_PROXY} }
      }
    }
  ],
  "outbounds": [
    {
      "tag": "proxy",
      "protocol": "vless",
      "settings": {
        "vnext": [{
          "address": "${clean_ip}",
          "port": ${port},
          "users": [{ "id": "${uuid}", "encryption": "none", "flow": "xtls-rprx-vision" }]
        }]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "serverName": "${sni}",
          "fingerprint": "chrome",
          "publicKey": "${pubkey}",
          "shortId": "${sid}"
        },
        "sockopt": { "mark": ${FWMARK_PROXY} }
      }
    },
    {
      "tag": "direct",
      "protocol": "freedom",
      "streamSettings": { "sockopt": { "mark": ${FWMARK_PROXY} } }
    },
    { "tag": "block", "protocol": "blackhole" },
    {
      "tag": "dns-out",
      "protocol": "dns",
      "streamSettings": { "sockopt": { "mark": ${FWMARK_PROXY} } }
    }
  ],
  "routing": {
    "domainStrategy": "IPIfNonMatch",
    "rules": [
      { "type": "field", "inboundTag": ["tproxy-udp"], "port": 53, "outboundTag": "proxy" },
      { "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" },
      { "type": "field", "ip": ["geoip:cn"], "outboundTag": "direct" },
      { "type": "field", "domain": ["geosite:cn", "geosite:private"], "outboundTag": "direct" },
      { "type": "field", "network": "tcp,udp", "outboundTag": "proxy" }
    ]
  }
}
EOF
}

gen_hy2_client_config() {
    local server_ip="$1" port="$2" password="$3" sni="$4"
    local clean_ip; clean_ip=$(strip_brackets "$server_ip")

    echo "$clean_ip" > "$CFG/server_ip"
    echo "$port"     > "$CFG/server_port"

    cat > "$CFG/singbox.json" <<EOF
{
  "log": { "level": "warn" },
  "dns": {
    "servers": [
      {
        "tag": "remote",
        "address": "https://1.1.1.1/dns-query",
        "detour": "proxy"
      },
      {
        "tag": "local",
        "address": "114.114.114.114",
        "detour": "direct"
      }
    ],
    "rules": [
      { "geosite": ["cn", "private"], "server": "local" }
    ],
    "final": "remote"
  },
  "inbounds": [
    {
      "type": "socks",
      "tag": "socks-in",
      "listen": "127.0.0.1",
      "listen_port": ${SOCKS_PORT},
      "udp": true
    },
    {
      "type": "http",
      "tag": "http-in",
      "listen": "127.0.0.1",
      "listen_port": ${HTTP_PORT}
    },
    {
      "type": "redirect",
      "tag": "tproxy-tcp",
      "listen": "0.0.0.0",
      "listen_port": ${REDIR_PORT}
    },
    {
      "type": "tproxy",
      "tag": "tproxy-udp",
      "listen": "0.0.0.0",
      "listen_port": ${TPROXY_PORT},
      "udp": true
    }
  ],
  "outbounds": [
    {
      "type": "hysteria2",
      "tag": "proxy",
      "server": "${clean_ip}",
      "server_port": ${port},
      "password": "${password}",
      "routing_mark": ${FWMARK_PROXY},
      "tls": {
        "enabled": true,
        "server_name": "${sni}",
        "insecure": true
      }
    },
    {
      "type": "direct",
      "tag": "direct",
      "routing_mark": ${FWMARK_PROXY}
    },
    { "type": "block", "tag": "block" }
  ],
  "route": {
    "rules": [
      { "geoip": ["private"], "outbound": "direct" },
      { "geosite": ["private"], "outbound": "direct" },
      { "geoip": ["cn"], "outbound": "direct" },
      { "geosite": ["cn"], "outbound": "direct" }
    ],
    "final": "proxy"
  }
}
EOF
}

#═══════════════════════════════════════════════════════════════════════════════
# 节点管理（安全解析，不使用 source）
#═══════════════════════════════════════════════════════════════════════════════
save_node() {
    local name="$1" protocol="$2"
    shift 2
    mkdir -p "$NODES_DIR"
    local node_file="$NODES_DIR/$name"

    case "$protocol" in
        vless)
            printf 'protocol=%s\nserver_ip=%s\nport=%s\nuuid=%s\npubkey=%s\nsid=%s\nsni=%s\n' \
                "vless" "$1" "$2" "$3" "$4" "$5" "$6" > "$node_file"
            ;;
        hy2)
            printf 'protocol=%s\nserver_ip=%s\nport=%s\npassword=%s\nsni=%s\n' \
                "hy2" "$1" "$2" "$3" "$4" > "$node_file"
            ;;
    esac
    chmod 600 "$node_file"
}

# 安全解析节点文件（逐行读取，不 source）
load_node() {
    local node_file="$1"
    [[ ! -f "$node_file" ]] && { _err "节点文件不存在: $node_file"; return 1; }

    # 重置所有节点变量
    protocol="" server_ip="" port="" uuid="" pubkey="" sid="" sni="" password=""

    local key val
    while IFS='=' read -r key val; do
        [[ -z "$key" || "$key" == \#* ]] && continue
        key="${key// /}"  # trim spaces
        case "$key" in
            protocol)  protocol="$val" ;;
            server_ip) server_ip="$val" ;;
            port)      port="$val" ;;
            uuid)      uuid="$val" ;;
            pubkey)    pubkey="$val" ;;
            sid)       sid="$val" ;;
            sni)       sni="$val" ;;
            password)  password="$val" ;;
        esac
    done < "$node_file"

    [[ -z "$protocol" || -z "$server_ip" || -z "$port" ]] && {
        _err "节点文件格式错误: $node_file"; return 1
    }
}

list_nodes() {
    [[ ! -d "$NODES_DIR" ]] && return 1
    local current; current=$(cat "$CURRENT_NODE_FILE" 2>/dev/null)
    local i=1 found=false

    for node_file in "$NODES_DIR"/*; do
        [[ ! -f "$node_file" ]] && continue
        found=true
        load_node "$node_file" 2>/dev/null || continue
        local name; name=$(basename "$node_file")
        local mark=""
        [[ "$name" == "$current" ]] && mark=" ${G}◀ 当前${NC}"

        local lat_str
        local clean; clean=$(strip_brackets "$server_ip")
        local lat; lat=$(test_latency "$clean" "$port" "${protocol:-vless}")
        if [[ "$lat" == "UDP" ]]; then
            lat_str="${C}UDP${NC}"
        elif [[ "$lat" == "超时" ]]; then
            lat_str="${R}超时${NC}"
        else
            lat_str="${G}${lat}${NC}"
        fi

        printf "  ${G}%2d${NC}) %-24s ${D}[%s]${NC}  ${D}%s:%s${NC}  %b%b\n" \
            "$i" "$name" "$(get_protocol_name "${protocol:-vless}")" \
            "$server_ip" "$port" "$lat_str" "$mark"
        ((i++))
    done

    [[ "$found" == "false" ]] && return 1
    return 0
}

select_node() {
    local prompt="$1"
    SELECTED_NODE=""
    list_nodes || { _warn "没有保存的节点"; return 1; }
    _line
    local max; max=$(ls "$NODES_DIR" 2>/dev/null | wc -l)
    read -rp "  $prompt [1-$max]: " choice
    [[ ! "$choice" =~ ^[0-9]+$ ]] && { _err "无效选择"; return 1; }
    local file; file=$(ls "$NODES_DIR" | sed -n "${choice}p")
    [[ -z "$file" ]] && { _err "节点不存在"; return 1; }
    SELECTED_NODE="$NODES_DIR/$file"
}

apply_node_config() {
    local node_file="$1"
    load_node "$node_file" || return 1
    case "${protocol:-vless}" in
        vless) gen_vless_client_config "$server_ip" "$port" "$uuid" "$pubkey" "$sid" "$sni" ;;
        hy2)   gen_hy2_client_config   "$server_ip" "$port" "$password" "$sni" ;;
        *)     _err "不支持的协议: $protocol"; return 1 ;;
    esac
    echo "${protocol:-vless}" > "$CFG/protocol"
}

#═══════════════════════════════════════════════════════════════════════════════
# TPROXY & Global iptables 脚本生成
#
# TPROXY 正确原理（Linux 内核文档）:
#   本机 OUTPUT UDP:
#     OUTPUT → mangle PROXY_LOCAL（排除特殊流量）→ MARK $FWMARK_TPROXY
#     → 策略路由 (ip rule fwmark $FWMARK_TPROXY lookup $ROUTE_TABLE)
#     → ip route local 0.0.0.0/0 dev lo table $ROUTE_TABLE
#     → 包重新进入 PREROUTING（以本地包形式）
#     → mangle PREROUTING PROXY_TPROXY → TPROXY → tproxy-udp inbound
#
#   转发 PREROUTING UDP（路由器模式，可选）:
#     PREROUTING → mangle PROXY_TPROXY → TPROXY → tproxy-udp inbound
#
#   本机 OUTPUT TCP:
#     OUTPUT → nat PROXY_TCP → REDIRECT → tproxy-tcp inbound
#═══════════════════════════════════════════════════════════════════════════════
create_proxy_scripts() {
    mkdir -p "$CFG"
    local srv_ip; srv_ip=$(cat "$CFG/server_ip" 2>/dev/null || echo "")

    # ─────────────────────────────────────────────────────────────────────────
    # tproxy-up.sh
    # ─────────────────────────────────────────────────────────────────────────
    cat > "$CFG/tproxy-up.sh" <<TPROXY_UP
#!/bin/bash
# 注意：不使用 set -e，iptables 命令返回非零是正常情况（规则已存在等）

FWMARK="${FWMARK_TPROXY}"
PROXY_MARK="${FWMARK_PROXY}"
ROUTE_TABLE="${ROUTE_TABLE}"
REDIR_PORT="${REDIR_PORT}"
TPROXY_PORT="${TPROXY_PORT}"
SERVER_IP="${srv_ip}"

echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null || true

BYPASS_NETS=(
    "0.0.0.0/8"
    "10.0.0.0/8"
    "100.64.0.0/10"
    "127.0.0.0/8"
    "169.254.0.0/16"
    "172.16.0.0/12"
    "192.0.0.0/24"
    "192.168.0.0/16"
    "198.18.0.0/15"
    "224.0.0.0/3"
    "240.0.0.0/4"
)

# ── 彻底清理旧规则（幂等，不依赖 set -e）─────────────────
ipt_del_ref() {
    # 安全删除父链引用：先 -C 检查存在再 -D 删除，两步均忽略错误
    iptables -t "\$1" -C "\$2" -j "\$3" 2>/dev/null && \
        iptables -t "\$1" -D "\$2" -j "\$3" 2>/dev/null || true
}

cleanup_rules() {
    ipt_del_ref nat    OUTPUT     PROXY_TCP
    ipt_del_ref mangle OUTPUT     PROXY_LOCAL
    ipt_del_ref mangle PREROUTING PROXY_TPROXY

    for t_c in "nat:PROXY_TCP" "mangle:PROXY_LOCAL" "mangle:PROXY_TPROXY"; do
        tbl="\${t_c%%:*}" chain="\${t_c##*:}"
        iptables -t "\$tbl" -F "\$chain" 2>/dev/null || true
        iptables -t "\$tbl" -X "\$chain" 2>/dev/null || true
    done

    ip rule del fwmark "\$FWMARK" lookup "\$ROUTE_TABLE" 2>/dev/null || true
    ip route flush table "\$ROUTE_TABLE" 2>/dev/null || true
}

cleanup_rules

# ── 策略路由 ─────────────────────────────────────────────
ip route add local 0.0.0.0/0 dev lo table "\$ROUTE_TABLE" 2>/dev/null || \
    ip route replace local 0.0.0.0/0 dev lo table "\$ROUTE_TABLE" 2>/dev/null || true
ip rule add fwmark "\$FWMARK" lookup "\$ROUTE_TABLE" 2>/dev/null || true

# ══════════════════════════════════════════════════════════
# 1. TCP 透明代理（OUTPUT → nat REDIRECT）
# ══════════════════════════════════════════════════════════
iptables -t nat -N PROXY_TCP 2>/dev/null || { iptables -t nat -F PROXY_TCP; }
iptables -t nat -A PROXY_TCP -m mark --mark "\$PROXY_MARK" -j RETURN
[ -n "\$SERVER_IP" ] && iptables -t nat -A PROXY_TCP -d "\$SERVER_IP" -j RETURN
iptables -t nat -A PROXY_TCP -p tcp --dport 22 -j RETURN
iptables -t nat -A PROXY_TCP -p tcp --sport 22 -j RETURN
for net in "\${BYPASS_NETS[@]}"; do
    iptables -t nat -A PROXY_TCP -d "\$net" -j RETURN
done
iptables -t nat -A PROXY_TCP -p tcp -j REDIRECT --to-ports "\$REDIR_PORT"
iptables -t nat -A OUTPUT -j PROXY_TCP

# ══════════════════════════════════════════════════════════
# 2. UDP 透明代理（OUTPUT 打标 → lo 回环 → PREROUTING TPROXY）
# ══════════════════════════════════════════════════════════

# 阶段 A：OUTPUT 打标
iptables -t mangle -N PROXY_LOCAL 2>/dev/null || { iptables -t mangle -F PROXY_LOCAL; }
iptables -t mangle -A PROXY_LOCAL -m mark --mark "\$PROXY_MARK" -j RETURN
[ -n "\$SERVER_IP" ] && iptables -t mangle -A PROXY_LOCAL -d "\$SERVER_IP" -j RETURN
for net in "\${BYPASS_NETS[@]}"; do
    iptables -t mangle -A PROXY_LOCAL -d "\$net" -j RETURN
done
iptables -t mangle -A PROXY_LOCAL -p udp -j MARK --set-mark "\$FWMARK"
iptables -t mangle -A OUTPUT -j PROXY_LOCAL

# 阶段 B：PREROUTING TPROXY 接收
iptables -t mangle -N PROXY_TPROXY 2>/dev/null || { iptables -t mangle -F PROXY_TPROXY; }
iptables -t mangle -A PROXY_TPROXY -m mark --mark "\$PROXY_MARK" -j RETURN
[ -n "\$SERVER_IP" ] && iptables -t mangle -A PROXY_TPROXY -d "\$SERVER_IP" -j RETURN
for net in "\${BYPASS_NETS[@]}"; do
    iptables -t mangle -A PROXY_TPROXY -d "\$net" -j RETURN
done
iptables -t mangle -A PROXY_TPROXY -p udp \
    -j TPROXY --on-port "\$TPROXY_PORT" --tproxy-mark "\$FWMARK"
iptables -t mangle -A PREROUTING -p udp -m mark --mark "\$FWMARK" -j PROXY_TPROXY

echo "TPROXY rules applied."
TPROXY_UP
    chmod +x "$CFG/tproxy-up.sh"

    # ─────────────────────────────────────────────────────────────────────────
    # tproxy-down.sh
    # ─────────────────────────────────────────────────────────────────────────
    cat > "$CFG/tproxy-down.sh" <<TPROXY_DOWN
#!/bin/bash
FWMARK="${FWMARK_TPROXY}"
ROUTE_TABLE="${ROUTE_TABLE}"

iptables -t nat    -C OUTPUT     -j PROXY_TCP    2>/dev/null && \
    iptables -t nat    -D OUTPUT     -j PROXY_TCP
iptables -t mangle -C OUTPUT     -j PROXY_LOCAL  2>/dev/null && \
    iptables -t mangle -D OUTPUT     -j PROXY_LOCAL
iptables -t mangle -C PREROUTING -j PROXY_TPROXY 2>/dev/null && \
    iptables -t mangle -D PREROUTING -j PROXY_TPROXY

for t_c in "nat:PROXY_TCP" "mangle:PROXY_LOCAL" "mangle:PROXY_TPROXY"; do
    tbl="\${t_c%%:*}" chain="\${t_c##*:}"
    iptables -t "\$tbl" -F "\$chain" 2>/dev/null || true
    iptables -t "\$tbl" -X "\$chain" 2>/dev/null || true
done

ip rule del fwmark "\$FWMARK" lookup "\$ROUTE_TABLE" 2>/dev/null || true
ip route flush table "\$ROUTE_TABLE" 2>/dev/null || true
echo "TPROXY rules cleared."
TPROXY_DOWN
    chmod +x "$CFG/tproxy-down.sh"

    # ─────────────────────────────────────────────────────────────────────────
    # global-up.sh（仅 TCP，更安全的全局模式）
    # ─────────────────────────────────────────────────────────────────────────
    cat > "$CFG/global-up.sh" <<GLOBAL_UP
#!/bin/bash
# 注意：不使用 set -e，iptables 命令返回非零是正常情况

REDIR_PORT="${REDIR_PORT}"
PROXY_MARK="${FWMARK_PROXY}"
SERVER_IP="${srv_ip}"

echo 1 > /proc/sys/net/ipv4/ip_forward 2>/dev/null || true

BYPASS_NETS=(
    "0.0.0.0/8"
    "10.0.0.0/8"
    "100.64.0.0/10"
    "127.0.0.0/8"
    "169.254.0.0/16"
    "172.16.0.0/12"
    "192.0.0.0/24"
    "192.168.0.0/16"
    "198.18.0.0/15"
    "224.0.0.0/3"
    "240.0.0.0/4"
)

# 清理旧规则（幂等）
iptables -t nat -C OUTPUT -j PROXY_GLOBAL 2>/dev/null && \
    iptables -t nat -D OUTPUT -j PROXY_GLOBAL 2>/dev/null || true
iptables -t nat -F PROXY_GLOBAL 2>/dev/null || true
iptables -t nat -X PROXY_GLOBAL 2>/dev/null || true

iptables -t nat -N PROXY_GLOBAL 2>/dev/null || { iptables -t nat -F PROXY_GLOBAL; }

# 跳过代理自身出站
iptables -t nat -A PROXY_GLOBAL -m mark --mark "\$PROXY_MARK" -j RETURN
# 跳过代理服务器 IP
[ -n "\$SERVER_IP" ] && iptables -t nat -A PROXY_GLOBAL -d "\$SERVER_IP" -j RETURN
# 跳过 SSH
iptables -t nat -A PROXY_GLOBAL -p tcp --sport 22 -j RETURN
iptables -t nat -A PROXY_GLOBAL -p tcp --dport 22 -j RETURN
# 跳过私有/保留地址
for net in "\${BYPASS_NETS[@]}"; do
    iptables -t nat -A PROXY_GLOBAL -d "\$net" -j RETURN
done
# 已建立连接直通（不干扰现有 SSH）
iptables -t nat -A PROXY_GLOBAL -m state --state ESTABLISHED,RELATED -j RETURN
# 重定向 TCP
iptables -t nat -A PROXY_GLOBAL -p tcp -j REDIRECT --to-ports "\$REDIR_PORT"
iptables -t nat -A OUTPUT -j PROXY_GLOBAL
echo "Global proxy rules applied."
GLOBAL_UP
    chmod +x "$CFG/global-up.sh"

    # global-down.sh
    cat > "$CFG/global-down.sh" <<'GLOBAL_DOWN'
#!/bin/bash
iptables -t nat -C OUTPUT -j PROXY_GLOBAL 2>/dev/null && \
    iptables -t nat -D OUTPUT -j PROXY_GLOBAL
iptables -t nat -F PROXY_GLOBAL 2>/dev/null || true
iptables -t nat -X PROXY_GLOBAL 2>/dev/null || true
echo "Global proxy rules cleared."
GLOBAL_DOWN
    chmod +x "$CFG/global-down.sh"
}

#═══════════════════════════════════════════════════════════════════════════════
# 创建系统服务
#═══════════════════════════════════════════════════════════════════════════════
create_services() {
    local protocol; protocol=$(get_protocol)
    local mode; mode=$(get_mode)

    if [[ "$DISTRO" == "alpine" ]]; then
        _create_services_openrc "$protocol" "$mode"
    else
        _create_services_systemd "$protocol" "$mode"
    fi
}

_create_services_systemd() {
    local protocol="$1" mode="$2"

    # 核心代理进程
    case "$protocol" in
        vless)
            cat > "/etc/systemd/system/${SVC_XRAY}.service" <<EOF
[Unit]
Description=VLESS Client (Xray)
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/xray run -c ${CFG}/config.json
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF
            ;;
        hy2)
            cat > "/etc/systemd/system/${SVC_SINGBOX}.service" <<EOF
[Unit]
Description=Hysteria2 Client (Sing-box)
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/sing-box run -c ${CFG}/singbox.json
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF
            ;;
    esac

    # TPROXY 服务
    if [[ "$mode" == "tproxy" ]]; then
        local core_svc
        [[ "$protocol" == "vless" ]] && core_svc="$SVC_XRAY" || core_svc="$SVC_SINGBOX"
        cat > "/etc/systemd/system/${SVC_TPROXY}.service" <<EOF
[Unit]
Description=TPROXY Transparent Proxy Rules
After=${core_svc}.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${CFG}/tproxy-up.sh
ExecStop=${CFG}/tproxy-down.sh

[Install]
WantedBy=multi-user.target
EOF
    fi

    # Global 服务
    if [[ "$mode" == "global" ]]; then
        local core_svc
        [[ "$protocol" == "vless" ]] && core_svc="$SVC_XRAY" || core_svc="$SVC_SINGBOX"
        cat > "/etc/systemd/system/${SVC_GLOBAL}.service" <<EOF
[Unit]
Description=Global Proxy Rules (iptables)
After=${core_svc}.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${CFG}/global-up.sh
ExecStop=${CFG}/global-down.sh

[Install]
WantedBy=multi-user.target
EOF
    fi

    # Watchdog（真正检测代理可用性）
    cat > "/etc/systemd/system/${SVC_WATCHDOG}.service" <<EOF
[Unit]
Description=VLess Client Watchdog
After=${SVC_XRAY}.service ${SVC_SINGBOX}.service

[Service]
Type=simple
ExecStart=/bin/bash -c '\
while true; do \
    sleep 60; \
    if ! curl -s --socks5-hostname 127.0.0.1:${SOCKS_PORT} \
        --connect-timeout 10 -o /dev/null http://cp.cloudflare.com/; then \
        echo "Watchdog: proxy unreachable, restarting..."; \
        systemctl restart ${SVC_XRAY} ${SVC_SINGBOX} 2>/dev/null || true; \
    fi; \
done'
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload 2>/dev/null
}

_create_services_openrc() {
    local protocol="$1" mode="$2"

    case "$protocol" in
        vless)
            cat > "/etc/init.d/${SVC_XRAY}" <<EOF
#!/sbin/openrc-run
description="VLESS Client (Xray)"
command="/usr/local/bin/xray"
command_args="run -c ${CFG}/config.json"
command_background=true
pidfile="/run/${SVC_XRAY}.pid"
EOF
            chmod +x "/etc/init.d/${SVC_XRAY}"
            ;;
        hy2)
            cat > "/etc/init.d/${SVC_SINGBOX}" <<EOF
#!/sbin/openrc-run
description="Hysteria2 Client (Sing-box)"
command="/usr/local/bin/sing-box"
command_args="run -c ${CFG}/singbox.json"
command_background=true
pidfile="/run/${SVC_SINGBOX}.pid"
EOF
            chmod +x "/etc/init.d/${SVC_SINGBOX}"
            ;;
    esac

    if [[ "$mode" == "tproxy" ]]; then
        cat > "/etc/init.d/${SVC_TPROXY}" <<EOF
#!/sbin/openrc-run
description="TPROXY Transparent Proxy"
depend() { need ${protocol == "vless" && echo "$SVC_XRAY" || echo "$SVC_SINGBOX"}; }
start() { ebegin "Starting TPROXY"; bash ${CFG}/tproxy-up.sh; eend \$?; }
stop()  { ebegin "Stopping TPROXY"; bash ${CFG}/tproxy-down.sh; eend \$?; }
EOF
        chmod +x "/etc/init.d/${SVC_TPROXY}"
    fi

    if [[ "$mode" == "global" ]]; then
        cat > "/etc/init.d/${SVC_GLOBAL}" <<EOF
#!/sbin/openrc-run
description="Global Proxy Rules"
start() { ebegin "Starting global proxy"; bash ${CFG}/global-up.sh; eend \$?; }
stop()  { ebegin "Stopping global proxy"; bash ${CFG}/global-down.sh; eend \$?; }
EOF
        chmod +x "/etc/init.d/${SVC_GLOBAL}"
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 启动 / 停止
#═══════════════════════════════════════════════════════════════════════════════
start_services() {
    local protocol; protocol=$(get_protocol)
    local mode; mode=$(get_mode)
    rm -f "$PAUSED_FILE"

    # 启动核心进程
    case "$protocol" in
        vless)
            svc enable "$SVC_XRAY" 2>/dev/null || true
            if svc start "$SVC_XRAY"; then
                _ok "Xray 已启动"
            else
                _err "Xray 启动失败，检查日志: journalctl -u ${SVC_XRAY} -n 30"
                return 1
            fi
            ;;
        hy2)
            svc enable "$SVC_SINGBOX" 2>/dev/null || true
            if svc start "$SVC_SINGBOX"; then
                _ok "Sing-box 已启动"
            else
                _err "Sing-box 启动失败，检查日志: journalctl -u ${SVC_SINGBOX} -n 30"
                return 1
            fi
            ;;
    esac

    sleep 1

    # 启动代理模式
    case "$mode" in
        tproxy)
            svc enable "$SVC_TPROXY" 2>/dev/null || true
            if svc start "$SVC_TPROXY"; then
                _ok "TPROXY 规则已加载"
            else
                _err "TPROXY 规则加载失败，正在输出详细错误："
                echo ""
                bash "$CFG/tproxy-up.sh" 2>&1 | sed 's/^/    /'
                echo ""
                _err "排查建议:"
                echo -e "  ${D}1. 检查虚拟化类型: systemd-detect-virt${NC}"
                echo -e "  ${D}2. 查看缺失模块:   modprobe xt_TPROXY 2>&1${NC}"
                echo -e "  ${D}3. KVM 系统可尝试: apt install linux-modules-extra-\$(uname -r)${NC}"
                echo -e "  ${D}4. OpenVZ/LXC 容器无法使用 TPROXY，请切换为全局代理模式${NC}"
                echo ""
                read -rp "  是否降级为全局代理模式继续? [Y/n]: " downgrade
                if [[ ! "$downgrade" =~ ^[nN]$ ]]; then
                    echo "global" > "$MODE_FILE"
                    create_proxy_scripts
                    create_services
                    svc enable "$SVC_GLOBAL" 2>/dev/null || true
                    if svc start "$SVC_GLOBAL"; then
                        _ok "已降级为全局代理模式（仅 TCP）"
                    else
                        _err "全局代理也启动失败"
                        return 1
                    fi
                else
                    return 1
                fi
            fi
            ;;
        global)
            svc enable "$SVC_GLOBAL" 2>/dev/null || true
            svc start "$SVC_GLOBAL" && _ok "全局代理规则已加载" || {
                _err "全局代理启动失败"; return 1
            }
            ;;
        socks)
            _ok "SOCKS5 代理: ${G}socks5://127.0.0.1:${SOCKS_PORT}${NC}"
            _ok "HTTP  代理: ${G}http://127.0.0.1:${HTTP_PORT}${NC}"
            ;;
    esac

    # Watchdog
    if [[ "$DISTRO" != "alpine" ]]; then
        svc enable "$SVC_WATCHDOG" 2>/dev/null || true
        svc start  "$SVC_WATCHDOG" 2>/dev/null || true
    fi
}

stop_services() {
    # 先停 iptables 规则，再停进程
    bash "$CFG/tproxy-down.sh"  2>/dev/null || true
    bash "$CFG/global-down.sh"  2>/dev/null || true
    svc stop "$SVC_TPROXY"   2>/dev/null || true
    svc stop "$SVC_GLOBAL"   2>/dev/null || true
    svc stop "$SVC_WATCHDOG" 2>/dev/null || true
    svc stop "$SVC_XRAY"     2>/dev/null || true
    svc stop "$SVC_SINGBOX"  2>/dev/null || true
}

#═══════════════════════════════════════════════════════════════════════════════
# JOIN 码解析
#═══════════════════════════════════════════════════════════════════════════════
parse_join_code() {
    local join_code="$1"
    local decoded

    # 去除 JOIN 码自身可能含有的空白字符（粘贴时带换行等）
    join_code="${join_code//[$'\t\r\n ']}"

    decoded=$(echo "$join_code" | base64 -d 2>/dev/null) || true
    [[ -z "$decoded" ]] && { _err "无效的 JOIN 码（base64 解码失败）"; return 1; }

    # 去除解码结果中所有换行/回车（base64 末尾带 \n 是常见问题）
    decoded="${decoded//$'\r'}"
    decoded="${decoded//$'\n'}"

    PARSED_PROTOCOL="" PARSED_SERVER_IP="" PARSED_PORT=""
    PARSED_UUID="" PARSED_PUBKEY="" PARSED_SID="" PARSED_SNI=""
    PARSED_PASSWORD=""

    if [[ "$decoded" =~ ^REALITY\| ]]; then
        IFS='|' read -r _ PARSED_SERVER_IP PARSED_PORT PARSED_UUID \
            PARSED_PUBKEY PARSED_SID PARSED_SNI <<< "$decoded"
        PARSED_PROTOCOL="vless"
    elif [[ "$decoded" =~ ^HY2\| ]]; then
        IFS='|' read -r _ PARSED_SERVER_IP PARSED_PORT \
            PARSED_PASSWORD PARSED_SNI <<< "$decoded"
        PARSED_PROTOCOL="hy2"
    else
        _err "不支持的 JOIN 码格式"
        _err "解码内容前缀: ${decoded:0:50}"
        _err "支持: REALITY|... 或 HY2|..."
        return 1
    fi

    # 清理各字段残余空白
    PARSED_SERVER_IP="${PARSED_SERVER_IP//[$'\r\n ']}"
    PARSED_PORT="${PARSED_PORT//[$'\r\n ']}"
    PARSED_UUID="${PARSED_UUID//[$'\r\n ']}"
    PARSED_PUBKEY="${PARSED_PUBKEY//[$'\r\n ']}"
    PARSED_SID="${PARSED_SID//[$'\r\n ']}"
    PARSED_SNI="${PARSED_SNI//[$'\r\n ']}"
    PARSED_PASSWORD="${PARSED_PASSWORD//[$'\r\n ']}"

    [[ -z "$PARSED_SERVER_IP" || -z "$PARSED_PORT" ]] && {
        _err "JOIN 码解析失败：缺少服务器 IP 或端口"; return 1
    }

    _info "解析成功: $(get_protocol_name "$PARSED_PROTOCOL") → ${PARSED_SERVER_IP}:${PARSED_PORT}"
}

#═══════════════════════════════════════════════════════════════════════════════
# 测试连接
#═══════════════════════════════════════════════════════════════════════════════
test_connection() {
    _info "测试代理连接..."
    local mode; mode=$(get_mode)
    local out_ip; out_ip=$(get_outbound_ip "$mode")

    if [[ -n "$out_ip" ]]; then
        _ok "连接正常，出口 IP: ${G}${out_ip}${NC}"
        return 0
    else
        _err "连接失败（超时或代理未就绪）"
        return 1
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 查看日志
#═══════════════════════════════════════════════════════════════════════════════
show_logs() {
    local protocol; protocol=$(get_protocol)
    local svc_name
    [[ "$protocol" == "vless" ]] && svc_name="$SVC_XRAY" || svc_name="$SVC_SINGBOX"

    if [[ "$DISTRO" == "alpine" ]]; then
        _warn "Alpine: 请查看 /var/log/messages"
        tail -n 50 /var/log/messages 2>/dev/null || true
    else
        journalctl -u "$svc_name" -n 60 --no-pager 2>/dev/null || \
            _err "无法获取日志"
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 状态显示
#═══════════════════════════════════════════════════════════════════════════════
show_status() {
    if ! is_installed; then
        echo -e "  状态: ${D}○ 未安装${NC}"
        return
    fi

    local protocol; protocol=$(get_protocol)
    local mode; mode=$(get_mode)
    local current_node; current_node=$(cat "$CURRENT_NODE_FILE" 2>/dev/null || echo "未知")

    local running=false
    case "$protocol" in
        vless) svc status "$SVC_XRAY"    2>/dev/null && running=true || true ;;
        hy2)   svc status "$SVC_SINGBOX" 2>/dev/null && running=true || true ;;
    esac

    if is_paused; then
        echo -e "  状态: ${Y}⏸  已暂停${NC}"
    elif $running; then
        echo -e "  状态: ${G}●  运行中${NC}"
    else
        echo -e "  状态: ${R}●  已停止${NC}"
    fi

    echo -e "  协议: ${C}$(get_protocol_name "$protocol")${NC}"
    echo -e "  模式: ${C}$(get_mode_name "$mode")${NC}"
    echo -e "  节点: ${C}${current_node}${NC}"

    if [[ "$mode" == "socks" || "$mode" == "tproxy" || "$mode" == "global" ]]; then
        echo -e "  SOCKS5: ${D}127.0.0.1:${SOCKS_PORT}${NC}   HTTP: ${D}127.0.0.1:${HTTP_PORT}${NC}"
    fi
}

show_node_info() {
    local node_name; node_name=$(cat "$CURRENT_NODE_FILE" 2>/dev/null)
    local node_file="$NODES_DIR/$node_name"
    [[ ! -f "$node_file" ]] && { _err "找不到当前节点文件"; return; }
    load_node "$node_file"

    _line
    echo -e "  ${W}节点: ${node_name}${NC}"
    echo -e "  协议: ${G}$(get_protocol_name "${protocol:-vless}")${NC}"
    echo -e "  服务器: ${G}${server_ip}:${port}${NC}"

    case "${protocol:-vless}" in
        vless)
            echo -e "  UUID:    ${C}${uuid}${NC}"
            echo -e "  公钥:    ${C}${pubkey}${NC}"
            echo -e "  SNI:     ${C}${sni}${NC}"
            echo -e "  ShortID: ${C}${sid}${NC}"
            ;;
        hy2)
            echo -e "  密码: ${C}${password}${NC}"
            echo -e "  SNI:  ${C}${sni}${NC}"
            ;;
    esac

    local clean; clean=$(strip_brackets "$server_ip")
    local lat; lat=$(test_latency "$clean" "$port" "${protocol:-vless}")
    if [[ "$lat" == "UDP" ]]; then
        echo -e "  延迟: ${C}UDP 协议${NC}"
    elif [[ "$lat" == "超时" ]]; then
        echo -e "  延迟: ${R}超时${NC}"
    else
        echo -e "  延迟: ${G}${lat}${NC}"
    fi
    _line
}

#═══════════════════════════════════════════════════════════════════════════════
# 安装向导
#═══════════════════════════════════════════════════════════════════════════════
do_install_client() {
    if is_installed; then
        _warn "客户端已安装，请先卸载再重新安装"
        return
    fi

    _header
    echo -e "  ${W}客户端安装向导${NC}"
    _line
    echo ""
    echo -e "  ${D}从服务端「查看配置→JOIN 码」处获取 JOIN 码${NC}"
    echo -e "  ${D}支持协议: VLESS+Reality、Hysteria2${NC}"
    echo ""
    read -rp "  输入 JOIN 码: " join_code
    [[ -z "$join_code" ]] && { _err "JOIN 码不能为空"; return; }

    parse_join_code "$join_code" || return

    echo ""
    _line
    echo -e "  服务器: ${G}${PARSED_SERVER_IP}:${PARSED_PORT}${NC}"
    echo -e "  协议:   ${G}$(get_protocol_name "$PARSED_PROTOCOL")${NC}"
    _line

    # 选择代理模式（先做 TPROXY 预检）
    echo ""
    echo -e "  ${W}选择代理模式${NC}"
    _line

    # 提前安装依赖，确保 iptables 可用以完成检测
    sync_time
    install_deps || { _err "依赖安装失败"; return; }

    local tproxy_available=true
    _info "检测 TPROXY 内核支持..."
    if check_tproxy_support 2>/dev/null; then
        _ok "TPROXY 可用"
        tproxy_available=true
    else
        tproxy_available=false
    fi
    echo ""

    _item "1" "SOCKS5/HTTP 代理  ${D}— 手动配置应用，最安全${NC}"
    _item "2" "全局代理          ${D}— iptables TCP 全局重定向${NC}"
    if [[ "$tproxy_available" == "true" ]]; then
        _item "3" "TPROXY 透明代理   ${D}— 内核级 TCP+UDP 全透明，性能最强${NC}"
    else
        echo -e "  ${D}3) TPROXY 透明代理   — 不可用（内核模块缺失，见上方提示）${NC}"
    fi
    echo ""

    local mode
    while true; do
        read -rp "  选择 [1-3]: " choice
        case $choice in
            1) mode="socks";  break ;;
            2) mode="global"; break ;;
            3)
                if [[ "$tproxy_available" == "false" ]]; then
                    _err "当前环境不支持 TPROXY，请选择 1 或 2"
                    echo ""
                    echo -e "  ${Y}提示: TPROXY 需要以下条件${NC}"
                    echo -e "  ${D}• KVM/物理机（不支持 OpenVZ/LXC）${NC}"
                    echo -e "  ${D}• 内核模块: xt_TPROXY, xt_socket, nf_tproxy_ipv4${NC}"
                    echo -e "  ${D}• 若为 KVM 但模块缺失，可尝试: apt install linux-modules-extra-\$(uname -r)${NC}"
                    echo ""
                else
                    mode="tproxy"; break
                fi
                ;;
            *) _err "无效选择，请输入 1-3" ;;
        esac
    done

    # 测试服务器连通性
    echo ""
    _info "测试服务器连通性..."
    local clean_ip; clean_ip=$(strip_brackets "$PARSED_SERVER_IP")
    if [[ "$PARSED_PROTOCOL" == "hy2" ]]; then
        _warn "Hysteria2 使用 UDP，跳过 TCP 端口测试"
    else
        if timeout 5 bash -c "echo >/dev/tcp/${clean_ip}/${PARSED_PORT}" 2>/dev/null; then
            _ok "服务器可达"
        else
            _warn "TCP 连接超时（端口 ${PARSED_PORT} 不可达）"
            read -rp "  仍要继续安装? [y/N]: " force
            [[ ! "$force" =~ ^[yY]$ ]] && return
        fi
    fi

    echo ""

    case "$PARSED_PROTOCOL" in
        vless) install_xray    || { _err "Xray 安装失败"; return; } ;;
        hy2)   install_singbox || { _err "Sing-box 安装失败"; return; } ;;
    esac

    mkdir -p "$CFG" "$NODES_DIR"
    echo "$mode" > "$MODE_FILE"
    echo "$PARSED_PROTOCOL" > "$CFG/protocol"

    _info "生成配置文件..."
    local node_name="默认_${clean_ip}_${PARSED_PORT}"
    case "$PARSED_PROTOCOL" in
        vless)
            gen_vless_client_config "$PARSED_SERVER_IP" "$PARSED_PORT" \
                "$PARSED_UUID" "$PARSED_PUBKEY" "$PARSED_SID" "$PARSED_SNI"
            save_node "$node_name" vless \
                "$PARSED_SERVER_IP" "$PARSED_PORT" "$PARSED_UUID" \
                "$PARSED_PUBKEY" "$PARSED_SID" "$PARSED_SNI"
            ;;
        hy2)
            gen_hy2_client_config "$PARSED_SERVER_IP" "$PARSED_PORT" \
                "$PARSED_PASSWORD" "$PARSED_SNI"
            save_node "$node_name" hy2 \
                "$PARSED_SERVER_IP" "$PARSED_PORT" "$PARSED_PASSWORD" "$PARSED_SNI"
            ;;
    esac
    echo "$node_name" > "$CURRENT_NODE_FILE"

    create_proxy_scripts
    create_services

    _info "启动服务..."
    if start_services; then
        _dline
        echo -e "  ${G}✓  安装完成！${NC}"
        echo -e "  协议: ${G}$(get_protocol_name "$PARSED_PROTOCOL")${NC}"
        echo -e "  模式: ${G}$(get_mode_name "$mode")${NC}"
        if [[ "$mode" == "socks" ]]; then
            echo -e "  SOCKS5: ${G}socks5://127.0.0.1:${SOCKS_PORT}${NC}"
            echo -e "  HTTP:   ${G}http://127.0.0.1:${HTTP_PORT}${NC}"
        fi
        echo -e "  快捷命令: ${G}vless-client${NC}"
        _dline
        create_shortcut
        [[ "$mode" != "socks" ]] && { sleep 2; test_connection; }
    else
        _err "安装失败，正在清理..."
        _cleanup_install
    fi
}

_cleanup_install() {
    stop_services 2>/dev/null || true
    rm -rf "$CFG"
    if [[ "$DISTRO" != "alpine" ]]; then
        rm -f /etc/systemd/system/${SVC_XRAY}.service \
              /etc/systemd/system/${SVC_SINGBOX}.service \
              /etc/systemd/system/${SVC_TPROXY}.service \
              /etc/systemd/system/${SVC_GLOBAL}.service \
              /etc/systemd/system/${SVC_WATCHDOG}.service
        systemctl daemon-reload 2>/dev/null || true
    else
        rm -f /etc/init.d/${SVC_XRAY} /etc/init.d/${SVC_SINGBOX} \
              /etc/init.d/${SVC_TPROXY} /etc/init.d/${SVC_GLOBAL}
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 节点操作
#═══════════════════════════════════════════════════════════════════════════════
do_add_node() {
    _header
    echo -e "  ${W}添加节点${NC}"
    _line
    echo ""
    read -rp "  JOIN 码: " join_code
    [[ -z "$join_code" ]] && { _err "JOIN 码不能为空"; return; }

    parse_join_code "$join_code" || return

    local cur_proto; cur_proto=$(get_protocol)
    if [[ "$PARSED_PROTOCOL" != "$cur_proto" ]]; then
        _err "节点协议 ($(get_protocol_name "$PARSED_PROTOCOL")) 与当前客户端协议 ($(get_protocol_name "$cur_proto")) 不匹配"
        _warn "如需切换协议，请卸载后重新安装"
        return 1
    fi

    echo ""
    echo -e "  服务器: ${G}${PARSED_SERVER_IP}:${PARSED_PORT}${NC}"
    echo -e "  协议:   ${G}$(get_protocol_name "$PARSED_PROTOCOL")${NC}"
    read -rp "  节点名称 [留空自动生成]: " node_name
    local clean_ip; clean_ip=$(strip_brackets "$PARSED_SERVER_IP")
    [[ -z "$node_name" ]] && node_name="node_${clean_ip}_${PARSED_PORT}"

    # 节点名不能含特殊字符（避免路径问题）
    node_name="${node_name//[^a-zA-Z0-9_\-.]/_}"

    case "$PARSED_PROTOCOL" in
        vless) save_node "$node_name" vless "$PARSED_SERVER_IP" "$PARSED_PORT" \
                   "$PARSED_UUID" "$PARSED_PUBKEY" "$PARSED_SID" "$PARSED_SNI" ;;
        hy2)   save_node "$node_name" hy2 "$PARSED_SERVER_IP" "$PARSED_PORT" \
                   "$PARSED_PASSWORD" "$PARSED_SNI" ;;
    esac
    _ok "节点已添加: $node_name"

    read -rp "  立即切换到该节点? [Y/n]: " sw
    [[ ! "$sw" =~ ^[nN]$ ]] && _switch_node "$NODES_DIR/$node_name"
}

_switch_node() {
    local node_file="$1"
    [[ ! -f "$node_file" ]] && { _err "节点文件不存在"; return 1; }
    _info "切换到节点: $(basename "$node_file")..."
    stop_services 2>/dev/null || true
    apply_node_config "$node_file" || return 1
    create_proxy_scripts
    create_services
    echo "$(basename "$node_file")" > "$CURRENT_NODE_FILE"
    start_services && _ok "节点切换完成"
    sleep 1
    test_connection
}

do_switch_node() {
    _header
    echo -e "  ${W}切换节点${NC}"
    _line
    select_node "选择节点" || return
    _switch_node "$SELECTED_NODE"
}

do_delete_node() {
    _header
    echo -e "  ${W}删除节点${NC}"
    _line
    select_node "选择要删除的节点" || return
    local name; name=$(basename "$SELECTED_NODE")
    local current; current=$(cat "$CURRENT_NODE_FILE" 2>/dev/null)
    [[ "$name" == "$current" ]] && { _err "不能删除当前使用的节点，请先切换"; return; }
    read -rp "  确认删除 \"$name\"? [y/N]: " confirm
    [[ "$confirm" =~ ^[yY]$ ]] && { rm -f "$SELECTED_NODE"; _ok "已删除: $name"; }
}

#═══════════════════════════════════════════════════════════════════════════════
# 切换代理模式
#═══════════════════════════════════════════════════════════════════════════════
do_switch_mode() {
    local current; current=$(get_mode)
    _header
    echo -e "  ${W}切换代理模式${NC}"
    echo -e "  当前: ${G}$(get_mode_name "$current")${NC}"
    _line
    _item "1" "SOCKS5/HTTP 代理"
    _item "2" "全局代理 (iptables TCP)"
    _item "3" "TPROXY 透明代理 (TCP+UDP)"
    echo ""

    local new_mode
    while true; do
        read -rp "  选择 [1-3]: " choice
        case $choice in
            1) new_mode="socks";  break ;;
            2) new_mode="global"; break ;;
            3) new_mode="tproxy"; break ;;
            *) _err "无效选择" ;;
        esac
    done

    [[ "$new_mode" == "$current" ]] && { _warn "已是当前模式，无需切换"; return; }

    _info "切换模式..."
    stop_services 2>/dev/null || true
    echo "$new_mode" > "$MODE_FILE"

    local node_name; node_name=$(cat "$CURRENT_NODE_FILE" 2>/dev/null)
    apply_node_config "$NODES_DIR/$node_name"
    create_proxy_scripts
    create_services
    start_services
    _ok "切换完成: $(get_mode_name "$new_mode")"
    [[ "$new_mode" != "socks" ]] && { sleep 1; test_connection; }
}

#═══════════════════════════════════════════════════════════════════════════════
# 卸载
#═══════════════════════════════════════════════════════════════════════════════
do_uninstall() {
    echo ""
    _warn "卸载将停止所有服务并删除配置文件"
    _warn "二进制文件 (xray, sing-box) 默认保留"
    echo ""
    read -rp "  确认卸载? [y/N]: " confirm
    [[ ! "$confirm" =~ ^[yY]$ ]] && { _info "已取消"; return; }

    _info "停止并禁用服务..."
    stop_services

    for svc_name in "$SVC_XRAY" "$SVC_SINGBOX" "$SVC_TPROXY" \
                    "$SVC_GLOBAL" "$SVC_WATCHDOG"; do
        svc disable "$svc_name" 2>/dev/null || true
    done

    if [[ "$DISTRO" != "alpine" ]]; then
        rm -f /etc/systemd/system/${SVC_XRAY}.service \
              /etc/systemd/system/${SVC_SINGBOX}.service \
              /etc/systemd/system/${SVC_TPROXY}.service \
              /etc/systemd/system/${SVC_GLOBAL}.service \
              /etc/systemd/system/${SVC_WATCHDOG}.service
        systemctl daemon-reload 2>/dev/null || true
    else
        rm -f /etc/init.d/${SVC_XRAY} /etc/init.d/${SVC_SINGBOX} \
              /etc/init.d/${SVC_TPROXY} /etc/init.d/${SVC_GLOBAL}
    fi

    _info "删除配置..."
    rm -rf "$CFG"
    rm -f /usr/local/bin/vless-client /usr/bin/vless-client 2>/dev/null || true

    _ok "卸载完成"
    echo ""
    read -rp "  同时删除 xray 和 sing-box 二进制? [y/N]: " del_bin
    if [[ "$del_bin" =~ ^[yY]$ ]]; then
        rm -f /usr/local/bin/xray /usr/local/bin/sing-box
        _ok "二进制文件已删除"
    fi
}

#═══════════════════════════════════════════════════════════════════════════════
# 快捷命令
#═══════════════════════════════════════════════════════════════════════════════
create_shortcut() {
    local script_path
    script_path=$(readlink -f "$0" 2>/dev/null || echo "$0")
    ln -sf "$script_path" /usr/local/bin/vless-client 2>/dev/null || true
    hash -r 2>/dev/null || true
    _ok "快捷命令已创建: vless-client"
}

#═══════════════════════════════════════════════════════════════════════════════
# 主菜单
#═══════════════════════════════════════════════════════════════════════════════
main_menu() {
    check_root

    while true; do
        _header
        echo ""
        show_status
        echo ""
        _line

        if is_installed; then
            _item "1" "查看节点信息"
            _item "2" "切换代理模式"
            _item "3" "测试连接"
            _item "4" "查看日志"
            _item "5" "添加节点"
            _item "6" "切换节点"
            _item "7" "删除节点"
            if is_paused; then
                _item "8" "恢复服务"
            else
                _item "8" "暂停服务"
            fi
            _item "9" "重启服务"
            _item "u" "卸载客户端"
        else
            _item "1" "安装客户端 (粘贴 JOIN 码)"
        fi
        _item "0" "退出"
        _line
        echo ""

        read -rp "  请选择: " choice || exit 0

        if is_installed; then
            case $choice in
                1) show_node_info; _pause ;;
                2) do_switch_mode; _pause ;;
                3) test_connection; _pause ;;
                4) show_logs; _pause ;;
                5) do_add_node; _pause ;;
                6) do_switch_node; _pause ;;
                7) do_delete_node; _pause ;;
                8)
                    if is_paused; then
                        _info "恢复服务..."
                        start_services && _ok "服务已恢复"
                    else
                        _info "暂停服务..."
                        stop_services
                        touch "$PAUSED_FILE"
                        _ok "服务已暂停"
                    fi
                    _pause
                    ;;
                9)
                    _info "重启服务..."
                    stop_services
                    sleep 1
                    start_services && _ok "重启完成"
                    _pause
                    ;;
                u|U) do_uninstall; _pause ;;
                0) exit 0 ;;
                *) _err "无效选择，请重新输入" ;;
            esac
        else
            case $choice in
                1) do_install_client; _pause ;;
                0) exit 0 ;;
                *) _err "无效选择" ;;
            esac
        fi
    done
}

main_menu
