#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=lib.sh source "$ROOT_DIR/scripts/lib.sh" AUTO_DETECT="${AUTO_DETECT:-true}" AUTO_VERIFY="${AUTO_VERIFY:-true}" SKIP_START="${SKIP_START:-false}" if [[ ! -f "$ROOT_DIR/config/settings.conf" ]]; then cp "$ROOT_DIR/config/settings.conf.example" "$ROOT_DIR/config/settings.conf" warn "已创建 config/settings.conf(将自动探测数据网卡并生成密钥)" fi load_config need_root info "1/6 自动探测并绑定数据网卡" "$ROOT_DIR/scripts/detect.sh" || true cell="" if [[ -n "${CELLULAR_IFACE:-}" ]] && iface_exists "$CELLULAR_IFACE"; then cell="$CELLULAR_IFACE" info "使用配置的数据网卡: $cell" elif [[ "$AUTO_DETECT" == "true" ]]; then cell="$(detect_cellular_iface "" || true)" fi if [[ -z "$cell" ]]; then die "未能自动探测数据网卡。 请插入/拨通数据模块后重试,或手动: 1) ./scripts/detect.sh 2) 编辑 config/settings.conf 设置 CELLULAR_IFACE=网卡名 3) sudo ./scripts/install.sh" fi src_ip="$(detect_source_ip "$cell" || true)" persist_cellular_to_settings "$ROOT_DIR/config/settings.conf" "$cell" "$src_ip" # 重新加载 CELLULAR_IFACE="$cell" CELLULAR_SOURCE_IP="$src_ip" info "已绑定 CELLULAR_IFACE=$cell source_ip=${src_ip:-auto}" # 随机面板密钥(若仍是默认) if [[ "$PANEL_SECRET" == "please-change-me" || "$PANEL_SECRET" == "change-this-secret" || -z "$PANEL_SECRET" ]]; then gen="$(openssl rand -hex 12 2>/dev/null || head -c 16 /dev/urandom | xxd -p | tr -d '\n')" tmp="$(mktemp)" awk -v k="PANEL_SECRET" -v v="$gen" ' BEGIN { done=0 } index($0, k "=")==1 { print k "=" v; done=1; next } { print } END { if (!done) print k "=" v } ' "$ROOT_DIR/config/settings.conf" > "$tmp" mv "$tmp" "$ROOT_DIR/config/settings.conf" PANEL_SECRET="$gen" warn "已自动生成 PANEL_SECRET(见 settings.conf)" fi # 再 load 一次保证变量一致 load_config CELLULAR_IFACE="$cell" CELLULAR_SOURCE_IP="${src_ip:-$CELLULAR_SOURCE_IP}" info "2/6 下载/安装 sing-box + UI" "$ROOT_DIR/scripts/fetch-binaries.sh" info "3/6 生成配置(代理出口 bind = $cell)" "$ROOT_DIR/scripts/generate.sh" "$ROOT_DIR/generated" ensure_dirs mkdir -p /var/lib/cellular-proxy install -m 0644 "$ROOT_DIR/generated/config.json" "$INSTALL_DIR/etc/config.json" install -m 0644 "$ROOT_DIR/generated/runtime.env" "$INSTALL_DIR/etc/runtime.env" install -m 0644 "$ROOT_DIR/config/settings.conf" "$INSTALL_DIR/etc/settings.conf" cp -a "$ROOT_DIR/scripts" "$INSTALL_DIR/" cp -a "$ROOT_DIR/ui" "$INSTALL_DIR/" 2>/dev/null || true if ! "$INSTALL_DIR/bin/sing-box" check -c "$INSTALL_DIR/etc/config.json"; then die "sing-box 配置校验失败" fi # 确认 bind_interface 写进配置 if ! grep -q "\"bind_interface\": \"$cell\"" "$INSTALL_DIR/etc/config.json" \ && ! grep -q "\"bind_interface\": \"$cell\"" "$INSTALL_DIR/etc/config.json" 2>/dev/null; then # JSON 可能无空格差异 if ! python3 - "$INSTALL_DIR/etc/config.json" "$cell" <<'PY' import json,sys c=json.load(open(sys.argv[1])) cell=sys.argv[2] ok=any(o.get("tag")=="cellular" and o.get("bind_interface")==cell for o in c.get("outbounds",[])) sys.exit(0 if ok else 1) PY then die "配置未正确绑定数据网卡 $cell" fi fi info "配置已确认 bind_interface=$cell" install -m 0755 /dev/stdin "$INSTALL_DIR/bin/cpxy" <<'EOF' #!/usr/bin/env bash set -euo pipefail # 经 /usr/local/bin/cpxy 符号链接调用时,$0 仍是链接路径;必须 resolve 真实路径 _self="${BASH_SOURCE[0]:-$0}" if command -v readlink >/dev/null 2>&1; then _resolved="$(readlink -f "$_self" 2>/dev/null || true)" [[ -n "$_resolved" ]] && _self="$_resolved" fi BASE="$(cd "$(dirname "$_self")/.." && pwd)" # 兜底:避免 symlink 解析失败时 BASE 变成 /usr/local if [[ ! -d "$BASE/scripts" && -d /opt/cellular-proxy/scripts ]]; then BASE="/opt/cellular-proxy" fi export CONFIG_FILE="${CONFIG_FILE:-$BASE/etc/settings.conf}" cmd="${1:-help}" shift || true case "$cmd" in detect) exec "$BASE/scripts/detect.sh" "$@" ;; generate) # 重新探测:若 settings 里网卡丢失则自动补;源 IP 漂移也刷新 # shellcheck source=/dev/null source "$BASE/scripts/lib.sh" load_config if [[ -z "${CELLULAR_IFACE:-}" ]] || ! iface_exists "${CELLULAR_IFACE:-}"; then cell="$(detect_cellular_iface "" || true)" if [[ -n "$cell" ]]; then src="$(detect_source_ip "$cell" || true)" persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" CELLULAR_IFACE="$cell" CELLULAR_SOURCE_IP="${src:-}" fi elif [[ -n "${CELLULAR_IFACE:-}" ]]; then src="$(detect_source_ip "$CELLULAR_IFACE" || true)" if [[ -n "$src" && "${CELLULAR_SOURCE_IP:-}" != "$src" ]]; then persist_cellular_to_settings "$CONFIG_FILE" "$CELLULAR_IFACE" "$src" CELLULAR_SOURCE_IP="$src" fi fi "$BASE/scripts/generate.sh" "$BASE/generated" install -m 0644 "$BASE/generated/config.json" "$BASE/etc/config.json" install -m 0644 "$BASE/generated/runtime.env" "$BASE/etc/runtime.env" install -m 0644 "$CONFIG_FILE" "$BASE/etc/settings.conf" 2>/dev/null || true "$BASE/bin/sing-box" check -c "$BASE/etc/config.json" systemctl restart cellular-proxy 2>/dev/null || true ;; rebind) # shellcheck source=/dev/null source "$BASE/scripts/lib.sh" load_config cell="$(detect_cellular_iface "" || true)" [[ -n "$cell" ]] || die "未能探测数据网卡" src="$(detect_source_ip "$cell" || true)" persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" info "已重新绑定 $cell src=${src:-}" exec "$0" generate ;; start) systemctl start cellular-proxy ;; stop) systemctl stop cellular-proxy ;; restart) systemctl restart cellular-proxy ;; status) systemctl status cellular-proxy --no-pager || true ;; verify) exec "$BASE/scripts/verify.sh" "$@" ;; auth) exec "$BASE/scripts/apply-proxy-auth.sh" "$@" ;; watch) case "${1:-status}" in once) exec "$BASE/scripts/watch-cellular.sh" --once ;; status) systemctl status cellular-proxy-watch.service --no-pager 2>/dev/null || true systemctl status cellular-proxy-watch.timer --no-pager 2>/dev/null || true ;; start|enable) systemctl enable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true ;; stop|disable) systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true ;; logs) journalctl -u cellular-proxy-watch -n "${2:-80}" --no-pager ;; *) exec "$BASE/scripts/watch-cellular.sh" "$@" ;; esac ;; upgrade) # 一条命令在线增量升级(保留密钥/网卡) if [[ -x "$BASE/scripts/online-upgrade.sh" ]]; then exec "$BASE/scripts/online-upgrade.sh" "$@" fi # 兜底:直接 curl 入口 url="${CELLULAR_PROXY_INSTALL_URL:-https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh}" exec bash -c 'curl -fsSL "$1" | bash -s -- --upgrade --install-dir "$2" "${@:3}"' _ "$url" "$BASE" "$@" ;; logs) journalctl -u cellular-proxy -n "${1:-80}" -f ;; help|*) cat </dev/null || true ln -sfn "$INSTALL_DIR/bin/cpxy" /usr/local/bin/cpxy info "4/6 安装 systemd" MEM="${MEMORY_MAX_MB:-96}" install -m 0644 "$ROOT_DIR/systemd/cellular-proxy.service" /etc/systemd/system/cellular-proxy.service sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy.service sed -i "s|@LOG_DIR@|$LOG_DIR|g" /etc/systemd/system/cellular-proxy.service if [[ "$MEM" != "0" && -n "$MEM" ]]; then sed -i "s|@MEMORY_MAX@|${MEM}M|g" /etc/systemd/system/cellular-proxy.service else sed -i '/MemoryMax=@MEMORY_MAX@/d' /etc/systemd/system/cellular-proxy.service fi # 管理 API(UI 配置代理账密) if [[ -f "$ROOT_DIR/systemd/cellular-proxy-admin.service" ]]; then install -m 0644 "$ROOT_DIR/systemd/cellular-proxy-admin.service" /etc/systemd/system/cellular-proxy-admin.service sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy-admin.service fi # WWAN 自动监控:事件驱动 + 定时兜底 install_watch_units() { local u for u in cellular-proxy-watch.service cellular-proxy-watch-once.service cellular-proxy-watch.timer; do if [[ -f "$ROOT_DIR/systemd/$u" ]]; then install -m 0644 "$ROOT_DIR/systemd/$u" "/etc/systemd/system/$u" sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" "/etc/systemd/system/$u" sed -i "s|@LOG_DIR@|${LOG_DIR}|g" "/etc/systemd/system/$u" fi done chmod +x "$INSTALL_DIR/scripts/watch-cellular.sh" 2>/dev/null || true } install_watch_units systemctl daemon-reload info "5/6 启动服务" export SYSTEMD_PAGER=cat export SYSTEMD_COLORS=0 if [[ "$SKIP_START" == "true" ]]; then systemctl enable cellular-proxy.service systemctl enable cellular-proxy-admin.service 2>/dev/null || true systemctl stop cellular-proxy.service 2>/dev/null || true systemctl stop cellular-proxy-admin.service 2>/dev/null || true info "已按 SKIP_START 跳过启动" else systemctl enable cellular-proxy.service systemctl enable cellular-proxy-admin.service 2>/dev/null || true systemctl restart cellular-proxy.service systemctl restart cellular-proxy-admin.service 2>/dev/null || true # 等待 active,最多约 8 秒(避免 status 卡住) ok=0 for _ in 1 2 3 4 5 6 7 8; do if systemctl is-active --quiet cellular-proxy.service; then ok=1 break fi sleep 1 done active_state="$(systemctl is-active cellular-proxy.service 2>/dev/null || echo unknown)" info "服务状态: $active_state" if [[ "$ok" -ne 1 ]]; then err "服务未处于 active" systemctl show cellular-proxy.service -p ActiveState -p SubState -p Result -p ExecMainStatus --no-pager 2>/dev/null || true journalctl -u cellular-proxy -n 40 --no-pager 2>/dev/null || true die "启动失败,请检查数据网卡 $cell 是否 up 且有 IP" fi fi # 默认开启 watch(ENABLE_CELLULAR_WATCH!=false) watch_flag="$(echo "${ENABLE_CELLULAR_WATCH:-true}" | tr '[:upper:]' '[:lower:]')" if [[ "$watch_flag" == "false" || "$watch_flag" == "0" || "$watch_flag" == "no" ]]; then systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true info "ENABLE_CELLULAR_WATCH=false,未启用自动监控" else systemctl enable cellular-proxy-watch.service 2>/dev/null || true systemctl enable cellular-proxy-watch.timer 2>/dev/null || true if [[ "$SKIP_START" != "true" ]]; then systemctl restart cellular-proxy-watch.service 2>/dev/null || true systemctl restart cellular-proxy-watch.timer 2>/dev/null || true fi info "已启用 cellular-proxy-watch(网卡/源 IP 变化自动 rebind)" fi info "6/6 自动验证出口(短超时,失败不阻塞安装结束)" if [[ "$SKIP_START" != "true" && "$AUTO_VERIFY" == "true" ]]; then set +e # 安装阶段用短超时;VERIFY_QUICK=1 跳过 detect 长输出 VERIFY_QUICK=1 EGRESS_TIMEOUT="${EGRESS_TIMEOUT:-8}" \ "$ROOT_DIR/scripts/verify.sh" vr=$? set -e if [[ "$vr" -eq 0 ]]; then info "出口验证通过:代理已从数据网卡出" else warn "自动验证未通过(exit=$vr)。安装仍算完成,稍后可: cpxy verify" warn "当前绑定: CELLULAR_IFACE=$cell ip=${src_ip:-}" fi else info "已跳过自动验证" fi echo info "安装完成(全自动探测 + 绑定)" echo " 数据网卡: $cell (${src_ip:-no-ipv4})" echo " 语义: 不走代理 → 系统默认(WiFi);走代理 → 数据流量" echo " 代理: HTTP/SOCKS ${PROXY_LISTEN_HOST}:${PROXY_MIXED_PORT}" if [[ -n "${PROXY_USER:-}" ]]; then echo " 代理鉴权: 用户 ${PROXY_USER}(密码已设置)" else echo " 代理鉴权: 无(建议: cpxy auth --user u --pass p 或面板配置)" fi echo " 面板: http://:${PANEL_PORT}/ui/ 密钥: $PANEL_SECRET" echo " 管理API: http://:9091 (UI 里改代理账密用,密钥同上)" echo " 配置: $INSTALL_DIR/etc/settings.conf" echo " 管理: cpxy rebind | verify | logs | status" echo " 账密: cpxy auth --user U --pass P | --clear | --show" echo " 若刚才像卡住:多半在测公网出口,现已改为短超时并保证退出" # 安装脚本始终以 0 结束(服务已 active);验证失败只告警 exit 0