#!/usr/bin/env bash # 增量升级:保留 settings/密钥/网卡绑定,默认不重下 sing-box、不重探测、不强制 verify # 用法: # sudo ./scripts/upgrade.sh # sudo ./scripts/upgrade.sh --ui-only # sudo ./scripts/upgrade.sh --force-binary # sudo ./scripts/upgrade.sh --rebind # sudo ./scripts/upgrade.sh --verify set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=lib.sh source "$ROOT_DIR/scripts/lib.sh" UI_ONLY=false FORCE_BINARY=false DO_REBIND=false DO_VERIFY=false SKIP_START=false usage() { cat <<'H' cellular-proxy 增量升级 保留: settings.conf / PANEL_SECRET / PROXY_* / CELLULAR_IFACE 默认: 不重新探测网卡、不重下 sing-box(已存在且可执行则跳过)、不强制出口验证 选项: --ui-only 只更新 UI 面板 --force-binary 强制重下/重装 sing-box --rebind 升级时重新探测数据网卡并写回配置 --verify 升级后跑出口验证 --skip-start 只落盘,不 restart 服务 -h, --help H } while [[ $# -gt 0 ]]; do case "$1" in --ui-only) UI_ONLY=true; shift ;; --force-binary) FORCE_BINARY=true; shift ;; --rebind) DO_REBIND=true; shift ;; --verify) DO_VERIFY=true; shift ;; --skip-start) SKIP_START=true; shift ;; -h|--help) usage; exit 0 ;; *) die "未知参数: $1" ;; esac done need_root # 优先用已安装目录的配置 INST_CANDIDATES=( "${INSTALL_DIR:-}" "/opt/cellular-proxy" "$ROOT_DIR" ) EXISTING="" for d in "${INST_CANDIDATES[@]}"; do [[ -n "$d" ]] || continue if [[ -f "$d/etc/settings.conf" ]]; then EXISTING="$d" break fi done if [[ -z "$EXISTING" ]]; then die "未检测到已安装实例(缺少 \$INSTALL_DIR/etc/settings.conf)。 请先全量安装: curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash" fi export CONFIG_FILE="$EXISTING/etc/settings.conf" load_config INSTALL_DIR="${INSTALL_DIR:-$EXISTING}" # 若 settings 里 INSTALL_DIR 与探测到的不一致,以配置为准;配置空则用探测目录 if [[ -z "${INSTALL_DIR:-}" || ! -d "$INSTALL_DIR" ]]; then INSTALL_DIR="$EXISTING" fi export INSTALL_DIR info "======== cellular-proxy 增量升级 ========" info "安装目录: $INSTALL_DIR" info "配置文件: $CONFIG_FILE(将保留密钥/网卡/账密)" ensure_dirs mkdir -p /var/lib/cellular-proxy "$INSTALL_DIR/bin" "$INSTALL_DIR/etc" "$INSTALL_DIR/ui" "$INSTALL_DIR/scripts" "$INSTALL_DIR/generated" # --- UI only fast path --- if [[ "$UI_ONLY" == "true" ]]; then info "模式: 仅更新 UI" if [[ ! -f "$ROOT_DIR/ui/index.html" ]]; then die "源码缺少 ui/index.html" fi cp -a "$ROOT_DIR/ui/." "$INSTALL_DIR/ui/" info "UI 已更新 -> $INSTALL_DIR/ui" # 静态文件无需重启代理;admin 也不必 echo info "完成(ui-only)。浏览器强刷: http://:${PANEL_PORT}/ui/" exit 0 fi # --- 可选 rebind --- if [[ "$DO_REBIND" == "true" ]]; then info "重新探测数据网卡…" cell="$(detect_cellular_iface "" || true)" [[ -n "$cell" ]] || die "未能探测数据网卡" src="$(detect_source_ip "$cell" || true)" persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" load_config CELLULAR_IFACE="$cell" CELLULAR_SOURCE_IP="${src:-}" info "已 rebind: $CELLULAR_IFACE ${CELLULAR_SOURCE_IP:-}" fi cell="${CELLULAR_IFACE:-}" if [[ -z "$cell" ]] || ! iface_exists "$cell"; then warn "配置中的 CELLULAR_IFACE=${cell:-空} 无效,尝试自动探测(仅本次)" cell="$(detect_cellular_iface "" || true)" if [[ -n "$cell" ]]; then src="$(detect_source_ip "$cell" || true)" persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" load_config CELLULAR_IFACE="$cell" CELLULAR_SOURCE_IP="${src:-$CELLULAR_SOURCE_IP}" else die "无有效数据网卡。请: cpxy rebind 或 --rebind / --iface" fi fi src_ip="${CELLULAR_SOURCE_IP:-$(detect_source_ip "$cell" || true)}" # --- binary --- need_binary=true if [[ "$FORCE_BINARY" != "true" && -x "$INSTALL_DIR/bin/sing-box" ]]; then if "$INSTALL_DIR/bin/sing-box" version >/dev/null 2>&1; then cur="$("$INSTALL_DIR/bin/sing-box" version 2>/dev/null | head -1 || true)" info "复用已有 sing-box: $cur" need_binary=false fi fi if [[ "$need_binary" == "true" ]]; then info "安装/更新 sing-box 二进制" FORCE_DOWNLOAD="${FORCE_DOWNLOAD:-}" if [[ "$FORCE_BINARY" == "true" ]]; then export FORCE_DOWNLOAD=1 fi # fetch 会同时拷 UI;后面还会再拷一遍 scripts/ui 没关系 export CONFIG_FILE "$ROOT_DIR/scripts/fetch-binaries.sh" else info "跳过二进制下载(需要强制时加 --force-binary)" mkdir -p "$INSTALL_DIR/ui" if [[ -f "$ROOT_DIR/ui/index.html" ]]; then cp -a "$ROOT_DIR/ui/." "$INSTALL_DIR/ui/" info "已更新 UI" fi fi # --- scripts / cpxy / generate config --- info "更新 scripts / 配置生成 / systemd" # 同步新脚本到安装目录,但不要覆盖 etc/settings if command -v rsync >/dev/null 2>&1; then rsync -a --delete \ --exclude 'settings.conf' \ "$ROOT_DIR/scripts/" "$INSTALL_DIR/scripts/" else mkdir -p "$INSTALL_DIR/scripts" cp -a "$ROOT_DIR/scripts/." "$INSTALL_DIR/scripts/" fi chmod +x "$INSTALL_DIR/scripts/"*.sh "$INSTALL_DIR/scripts/"*.py 2>/dev/null || true # 生成配置(用现有 settings,不改 secret) export CONFIG_FILE "$ROOT_DIR/scripts/generate.sh" "$INSTALL_DIR/generated" install -m 0644 "$INSTALL_DIR/generated/config.json" "$INSTALL_DIR/etc/config.json" install -m 0644 "$INSTALL_DIR/generated/runtime.env" "$INSTALL_DIR/etc/runtime.env" # 保留 settings:只确保 INSTALL_DIR 字段正确 if ! grep -qE '^INSTALL_DIR=' "$CONFIG_FILE" 2>/dev/null; then printf 'INSTALL_DIR=%s\n' "$INSTALL_DIR" >> "$CONFIG_FILE" fi install -m 0644 "$CONFIG_FILE" "$INSTALL_DIR/etc/settings.conf" if ! "$INSTALL_DIR/bin/sing-box" check -c "$INSTALL_DIR/etc/config.json"; then die "sing-box 配置校验失败" fi # 校验 bind 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 info "配置已确认 bind_interface=$cell" # 安装 cpxy 包装器(与 install.sh 一致,含 upgrade 子命令) 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)" 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) # 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 systemctl restart cellular-proxy-admin 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 ;; upgrade) # 一条命令在线升级:拉最新 install.sh 再 --upgrade if [[ -x "$BASE/scripts/online-upgrade.sh" ]]; then exec "$BASE/scripts/online-upgrade.sh" "$@" fi 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" "$@" ;; start) systemctl start cellular-proxy cellular-proxy-admin 2>/dev/null || systemctl start cellular-proxy ;; stop) systemctl stop cellular-proxy-admin 2>/dev/null || true; systemctl stop cellular-proxy ;; restart) systemctl restart cellular-proxy; systemctl restart cellular-proxy-admin 2>/dev/null || true ;; status) systemctl status cellular-proxy --no-pager || true systemctl status cellular-proxy-admin --no-pager 2>/dev/null || 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 ;; logs) journalctl -u cellular-proxy -n "${1:-80}" -f ;; help|*) cat </dev/null || true systemctl daemon-reload export SYSTEMD_PAGER=cat export SYSTEMD_COLORS=0 if [[ "$SKIP_START" == "true" ]]; then systemctl enable cellular-proxy.service 2>/dev/null || true systemctl enable cellular-proxy-admin.service 2>/dev/null || true info "已跳过 restart(--skip-start)" else systemctl enable cellular-proxy.service 2>/dev/null || true 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 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" journalctl -u cellular-proxy -n 30 --no-pager 2>/dev/null || true die "升级后启动失败" fi fi 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 if [[ "$DO_VERIFY" == "true" && "$SKIP_START" != "true" ]]; then info "出口验证…" set +e VERIFY_QUICK=1 EGRESS_TIMEOUT="${EGRESS_TIMEOUT:-8}" "$INSTALL_DIR/scripts/verify.sh" vr=$? set -e if [[ "$vr" -eq 0 ]]; then info "出口验证通过" else warn "验证未通过(exit=$vr),可稍后: cpxy verify" fi else info "已跳过出口验证(需要时: --verify 或 cpxy verify)" fi # shellcheck disable=SC1090 source "$INSTALL_DIR/etc/settings.conf" 2>/dev/null || true lan="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}' || true)" cat <}:${PANEL_PORT:-9090}/ui/ 管理: :9091(账密配置) 下次升级: sudo cpxy upgrade sudo cpxy upgrade --ui-only sudo cpxy upgrade --force-binary EOM exit 0