415 lines
13 KiB
Bash
Executable File
415 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# cellular-proxy 一条命令:未安装 → 全量安装;已安装 → 增量升级
|
||
# 语义:走本代理的连接一律从数据网卡(流量)出口;系统默认仍可走 WiFi
|
||
#
|
||
# ★ 唯一推荐(装 / 升 通用):
|
||
# curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash
|
||
#
|
||
# 已装后也可: sudo cpxy upgrade
|
||
#
|
||
# 可选参数:
|
||
# ... | sudo bash -s -- --iface wwan0 --secret xxx --user u --pass p
|
||
# ... | sudo bash -s -- --force-install # 已装也强制全量
|
||
# ... | sudo bash -s -- --force-binary # 升级时强制重下 sing-box
|
||
#
|
||
set -euo pipefail
|
||
|
||
REPO_OWNER="${REPO_OWNER:-Hermes}"
|
||
REPO_NAME="${REPO_NAME:-cellular-proxy}"
|
||
GITEA_BASE="${GITEA_BASE:-https://gitea.chickliu.fun}"
|
||
BRANCH="${BRANCH:-main}"
|
||
REF="${CELLULAR_PROXY_REF:-$BRANCH}"
|
||
|
||
INSTALL_DIR="${INSTALL_DIR:-/opt/cellular-proxy}"
|
||
WORK_DIR="${WORK_DIR:-/tmp/cellular-proxy-install-$$}"
|
||
|
||
OPT_IFACE=""
|
||
OPT_SECRET=""
|
||
OPT_USER=""
|
||
OPT_PASS=""
|
||
OPT_PORT=""
|
||
OPT_PANEL_PORT=""
|
||
OPT_MEMORY=""
|
||
OPT_SKIP_START=0
|
||
OPT_SKIP_VERIFY=0
|
||
OPT_LOCAL_DIR=""
|
||
OPT_NO_AUTO_DETECT=0
|
||
OPT_UPGRADE=0
|
||
OPT_UI_ONLY=0
|
||
OPT_FORCE_BINARY=0
|
||
OPT_REBIND=0
|
||
OPT_FORCE_INSTALL=0
|
||
OPT_DO_VERIFY=0
|
||
|
||
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }
|
||
info() { log "INFO $*"; }
|
||
warn() { log "WARN $*"; }
|
||
err() { log "ERROR $*"; }
|
||
die() { err "$*"; exit 1; }
|
||
|
||
usage() {
|
||
cat <<'H'
|
||
cellular-proxy — 一条命令自动安装或升级
|
||
|
||
curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash
|
||
|
||
行为(无参数时自动判断):
|
||
· 未检测到安装 → 全量安装(探测网卡 + 下载 sing-box + 启动)
|
||
· 已安装 → 增量升级(保留密钥/网卡;默认不重下二进制)
|
||
|
||
强制选项:
|
||
--upgrade 强制走增量升级(未安装会报错)
|
||
--force-install 即使已安装也走全量安装
|
||
--ui-only 仅更新面板(隐含升级)
|
||
--force-binary 升级时强制重下 sing-box
|
||
--rebind 升级时重新探测数据网卡
|
||
--verify 升级后跑出口验证(升级默认跳过)
|
||
|
||
其它选项:
|
||
--iface NAME 强制指定数据网卡
|
||
--secret STR 面板 PANEL_SECRET(默认随机;升级不改已有)
|
||
--user USER / --pass PASS
|
||
--port N / --panel-port N
|
||
--memory MB MemoryMax(默认 96)
|
||
--install-dir PATH
|
||
--local DIR 本地源码
|
||
--ref REF
|
||
--skip-start / --skip-verify
|
||
--no-auto-detect 禁止自动探测(必须 --iface)
|
||
-h, --help
|
||
H
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--iface) OPT_IFACE="${2:-}"; shift 2 ;;
|
||
--secret) OPT_SECRET="${2:-}"; shift 2 ;;
|
||
--user) OPT_USER="${2:-}"; shift 2 ;;
|
||
--pass) OPT_PASS="${2:-}"; shift 2 ;;
|
||
--port) OPT_PORT="${2:-}"; shift 2 ;;
|
||
--panel-port) OPT_PANEL_PORT="${2:-}"; shift 2 ;;
|
||
--memory) OPT_MEMORY="${2:-}"; shift 2 ;;
|
||
--install-dir) INSTALL_DIR="${2:-}"; shift 2 ;;
|
||
--local) OPT_LOCAL_DIR="${2:-}"; shift 2 ;;
|
||
--ref) REF="${2:-}"; shift 2 ;;
|
||
--skip-start) OPT_SKIP_START=1; shift ;;
|
||
--skip-verify) OPT_SKIP_VERIFY=1; shift ;;
|
||
--no-auto-detect) OPT_NO_AUTO_DETECT=1; shift ;;
|
||
--upgrade) OPT_UPGRADE=1; shift ;;
|
||
--ui-only) OPT_UI_ONLY=1; shift ;;
|
||
--force-binary) OPT_FORCE_BINARY=1; shift ;;
|
||
--rebind) OPT_REBIND=1; shift ;;
|
||
--force-install) OPT_FORCE_INSTALL=1; shift ;;
|
||
--verify) OPT_DO_VERIFY=1; shift ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) die "未知参数: $1(--help)" ;;
|
||
esac
|
||
done
|
||
|
||
need_root() {
|
||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||
die "请用 root:curl -fsSL '...' | sudo bash"
|
||
fi
|
||
}
|
||
|
||
need_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"
|
||
}
|
||
|
||
is_installed() {
|
||
local d="${1:-$INSTALL_DIR}"
|
||
# settings 存在即视为已装(可增量升级);兼容只有 conf 或 unit 的半装状态
|
||
if [[ -f "$d/etc/settings.conf" ]]; then
|
||
return 0
|
||
fi
|
||
if systemctl cat cellular-proxy.service >/dev/null 2>&1; then
|
||
# unit 在但目录被删:仍尝试按 conf 探测
|
||
[[ -f /opt/cellular-proxy/etc/settings.conf ]] && return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
ensure_deps() {
|
||
local missing=()
|
||
for c in tar awk ip curl; do
|
||
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
|
||
done
|
||
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
|
||
missing+=("curl")
|
||
fi
|
||
if [[ ${#missing[@]} -eq 0 ]]; then
|
||
return 0
|
||
fi
|
||
info "尝试自动安装依赖: ${missing[*]}"
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
export DEBIAN_FRONTEND=noninteractive
|
||
apt-get update -qq
|
||
apt-get install -y -qq ca-certificates curl tar iproute2 openssl coreutils findutils 2>/dev/null \
|
||
|| apt-get install -y ca-certificates curl tar iproute2 openssl
|
||
elif command -v apk >/dev/null 2>&1; then
|
||
apk add --no-cache ca-certificates curl tar iproute2 openssl
|
||
else
|
||
die "缺少依赖 ${missing[*]},且无法自动安装(请先装 curl/tar/iproute2)"
|
||
fi
|
||
}
|
||
|
||
download() {
|
||
local url="$1" dest="$2"
|
||
if command -v curl >/dev/null 2>&1; then
|
||
curl -fL --retry 3 --connect-timeout 20 -o "$dest" "$url"
|
||
else
|
||
wget -O "$dest" "$url"
|
||
fi
|
||
}
|
||
|
||
set_kv() {
|
||
local file="$1" key="$2" val="$3"
|
||
local tmp
|
||
tmp="$(mktemp)"
|
||
if grep -qE "^${key}=" "$file" 2>/dev/null; then
|
||
awk -v k="$key" -v v="$val" '
|
||
BEGIN { done=0 }
|
||
index($0, k "=")==1 { print k "=" v; done=1; next }
|
||
{ print }
|
||
END { if (!done) print k "=" v }
|
||
' "$file" > "$tmp"
|
||
mv "$tmp" "$file"
|
||
else
|
||
printf '%s=%s\n' "$key" "$val" >> "$file"
|
||
rm -f "$tmp"
|
||
fi
|
||
}
|
||
|
||
fetch_source() {
|
||
mkdir -p "$WORK_DIR"
|
||
if [[ -n "$OPT_LOCAL_DIR" ]]; then
|
||
[[ -d "$OPT_LOCAL_DIR" ]] || die "本地目录不存在: $OPT_LOCAL_DIR"
|
||
info "使用本地源码: $OPT_LOCAL_DIR"
|
||
mkdir -p "$WORK_DIR/src"
|
||
if command -v rsync >/dev/null 2>&1; then
|
||
rsync -a --delete \
|
||
--exclude '.git' --exclude 'generated' --exclude 'config/settings.conf' \
|
||
"$OPT_LOCAL_DIR"/ "$WORK_DIR/src"/
|
||
else
|
||
cp -a "$OPT_LOCAL_DIR"/. "$WORK_DIR/src"/
|
||
rm -rf "$WORK_DIR/src/.git" "$WORK_DIR/src/generated" "$WORK_DIR/src/config/settings.conf" 2>/dev/null || true
|
||
fi
|
||
return
|
||
fi
|
||
|
||
local tarball="$WORK_DIR/src.tgz"
|
||
local url_archive="${GITEA_BASE}/${REPO_OWNER}/${REPO_NAME}/archive/${REF}.tar.gz"
|
||
info "下载源码: $url_archive"
|
||
if ! download "$url_archive" "$tarball"; then
|
||
die "下载失败: $url_archive"
|
||
fi
|
||
mkdir -p "$WORK_DIR/extract"
|
||
tar -xzf "$tarball" -C "$WORK_DIR/extract"
|
||
local top
|
||
top="$(find "$WORK_DIR/extract" -mindepth 1 -maxdepth 1 -type d | head -1)"
|
||
[[ -n "$top" ]] || die "压缩包内容异常"
|
||
mkdir -p "$WORK_DIR/src"
|
||
if command -v rsync >/dev/null 2>&1; then
|
||
rsync -a "$top"/ "$WORK_DIR/src"/
|
||
else
|
||
cp -a "$top"/. "$WORK_DIR/src"/
|
||
fi
|
||
}
|
||
|
||
prepare_settings() {
|
||
local src="$WORK_DIR/src"
|
||
local conf="$src/config/settings.conf"
|
||
local live="${INSTALL_DIR}/etc/settings.conf"
|
||
[[ -f "$src/config/settings.conf.example" ]] || die "缺少 settings.conf.example"
|
||
|
||
# 已有安装:以 live settings 为底,不重置密钥/网卡
|
||
if [[ -f "$live" ]]; then
|
||
info "沿用已有配置: $live"
|
||
cp "$live" "$conf"
|
||
else
|
||
cp "$src/config/settings.conf.example" "$conf"
|
||
fi
|
||
|
||
# shellcheck source=/dev/null
|
||
source "$src/scripts/lib.sh"
|
||
CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}"
|
||
REQUIRE_CELLULAR_IFACE=false
|
||
|
||
local cell=""
|
||
if [[ -n "$OPT_IFACE" ]]; then
|
||
cell="$OPT_IFACE"
|
||
info "使用 --iface $cell"
|
||
elif [[ -f "$live" ]] && grep -qE '^CELLULAR_IFACE=.' "$live" 2>/dev/null; then
|
||
# shellcheck disable=SC1090
|
||
cell="$(awk -F= '/^CELLULAR_IFACE=/{print substr($0,index($0,"=")+1); exit}' "$live")"
|
||
if [[ -n "$cell" ]] && iface_exists "$cell"; then
|
||
info "沿用已绑定网卡: $cell"
|
||
else
|
||
cell=""
|
||
fi
|
||
fi
|
||
|
||
if [[ -z "$cell" ]]; then
|
||
if [[ "$OPT_NO_AUTO_DETECT" -eq 1 ]]; then
|
||
die "--no-auto-detect 时必须提供 --iface"
|
||
fi
|
||
info "自动探测数据网卡…"
|
||
cell="$(detect_cellular_iface "" || true)"
|
||
fi
|
||
|
||
if [[ -z "$cell" ]]; then
|
||
die "自动探测失败:未找到数据网卡。
|
||
请确认模组/拨号已 up,或指定:
|
||
... | sudo bash -s -- --iface <网卡名>
|
||
可先手动: ip -br a"
|
||
fi
|
||
|
||
if ! iface_exists "$cell"; then
|
||
die "网卡不存在: $cell"
|
||
fi
|
||
|
||
local src_ip
|
||
src_ip="$(detect_source_ip "$cell" || true)"
|
||
set_kv "$conf" CELLULAR_IFACE "$cell"
|
||
if [[ -n "$src_ip" ]]; then
|
||
set_kv "$conf" CELLULAR_SOURCE_IP "$src_ip"
|
||
fi
|
||
info "已写入 CELLULAR_IFACE=$cell ip=${src_ip:-none}"
|
||
|
||
# secret:仅全量且未指定且当前是占位时生成;有 live 则不覆盖
|
||
if [[ -n "$OPT_SECRET" ]]; then
|
||
set_kv "$conf" PANEL_SECRET "$OPT_SECRET"
|
||
elif [[ ! -f "$live" ]]; then
|
||
local cur
|
||
cur="$(awk -F= '/^PANEL_SECRET=/{print substr($0,index($0,"=")+1); exit}' "$conf" 2>/dev/null || true)"
|
||
if [[ -z "$cur" || "$cur" == "please-change-me" || "$cur" == "change-this-secret" ]]; then
|
||
local gen
|
||
gen="$(openssl rand -hex 12 2>/dev/null || head -c 16 /dev/urandom | xxd -p | tr -d '\n')"
|
||
set_kv "$conf" PANEL_SECRET "$gen"
|
||
info "已自动生成 PANEL_SECRET"
|
||
fi
|
||
fi
|
||
|
||
[[ -n "$OPT_USER" ]] && set_kv "$conf" PROXY_USER "$OPT_USER"
|
||
[[ -n "$OPT_PASS" ]] && set_kv "$conf" PROXY_PASS "$OPT_PASS"
|
||
[[ -n "$OPT_PORT" ]] && set_kv "$conf" PROXY_MIXED_PORT "$OPT_PORT"
|
||
[[ -n "$OPT_PANEL_PORT" ]] && set_kv "$conf" PANEL_PORT "$OPT_PANEL_PORT"
|
||
[[ -n "$OPT_MEMORY" ]] && set_kv "$conf" MEMORY_MAX_MB "$OPT_MEMORY"
|
||
set_kv "$conf" INSTALL_DIR "$INSTALL_DIR"
|
||
set_kv "$conf" REQUIRE_CELLULAR_IFACE "true"
|
||
|
||
export DETECTED_CELL="$cell"
|
||
export DETECTED_SRC_IP="$src_ip"
|
||
}
|
||
|
||
run_upgrade() {
|
||
info "======== cellular-proxy 增量升级入口 ========"
|
||
fetch_source
|
||
local args=()
|
||
[[ "$OPT_UI_ONLY" -eq 1 ]] && args+=(--ui-only)
|
||
[[ "$OPT_FORCE_BINARY" -eq 1 ]] && args+=(--force-binary)
|
||
[[ "$OPT_REBIND" -eq 1 ]] && args+=(--rebind)
|
||
# 升级默认不 verify(加快);需要时: --verify 或 UPGRADE_VERIFY=1
|
||
if [[ "${OPT_DO_VERIFY:-0}" -eq 1 ]] || [[ "${UPGRADE_VERIFY:-}" == "1" ]]; then
|
||
args+=(--verify)
|
||
fi
|
||
# 用户传了 skip-start
|
||
if [[ "$OPT_SKIP_START" -eq 1 ]]; then
|
||
args+=(--skip-start)
|
||
fi
|
||
# 把 --iface 映射为 rebind 场景下的强制:简单做法:若给了 --iface,写入 live 再 upgrade
|
||
if [[ -n "$OPT_IFACE" && -f "$INSTALL_DIR/etc/settings.conf" ]]; then
|
||
set_kv "$INSTALL_DIR/etc/settings.conf" CELLULAR_IFACE "$OPT_IFACE"
|
||
info "已写入 --iface $OPT_IFACE 到现有 settings"
|
||
fi
|
||
export INSTALL_DIR
|
||
export CONFIG_FILE="$INSTALL_DIR/etc/settings.conf"
|
||
bash "$WORK_DIR/src/scripts/upgrade.sh" "${args[@]}"
|
||
}
|
||
|
||
main() {
|
||
need_root
|
||
ensure_deps
|
||
need_cmd tar
|
||
need_cmd awk
|
||
need_cmd ip
|
||
|
||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||
|
||
# --ui-only 暗示升级
|
||
if [[ "$OPT_UI_ONLY" -eq 1 ]]; then
|
||
OPT_UPGRADE=1
|
||
fi
|
||
|
||
# ★ 一条命令自动分流:已安装 → 升级;未安装 → 全量安装
|
||
if [[ "$OPT_FORCE_INSTALL" -eq 0 && "$OPT_UPGRADE" -eq 0 ]]; then
|
||
if is_installed "$INSTALL_DIR"; then
|
||
info "检测到已安装: $INSTALL_DIR → 自动增量升级(保留密钥/网卡)"
|
||
OPT_UPGRADE=1
|
||
else
|
||
info "未检测到安装 → 自动全量安装"
|
||
fi
|
||
fi
|
||
|
||
if [[ "$OPT_UPGRADE" -eq 1 ]]; then
|
||
if ! is_installed "$INSTALL_DIR" && [[ ! -f "$INSTALL_DIR/etc/settings.conf" ]]; then
|
||
die "指定了升级,但未找到已安装实例 ($INSTALL_DIR)。
|
||
直接无参执行即可(会自动全量安装):
|
||
curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash"
|
||
fi
|
||
info "模式: 增量升级"
|
||
run_upgrade
|
||
return 0
|
||
fi
|
||
|
||
info "模式: 全量安装"
|
||
info "======== cellular-proxy 全自动安装 ========"
|
||
info "1/3 获取源码"
|
||
fetch_source
|
||
|
||
info "2/3 自动探测数据网卡并写配置"
|
||
prepare_settings
|
||
|
||
info "3/3 安装 sing-box、绑定出口、启动"
|
||
export CONFIG_FILE="$WORK_DIR/src/config/settings.conf"
|
||
export AUTO_DETECT=false
|
||
export AUTO_VERIFY=true
|
||
if [[ "$OPT_SKIP_VERIFY" -eq 1 ]]; then
|
||
export AUTO_VERIFY=false
|
||
fi
|
||
if [[ "$OPT_SKIP_START" -eq 1 ]]; then
|
||
export SKIP_START=true
|
||
else
|
||
export SKIP_START=false
|
||
fi
|
||
bash "$WORK_DIR/src/scripts/install.sh"
|
||
|
||
# shellcheck disable=SC1090
|
||
source "$INSTALL_DIR/etc/settings.conf" 2>/dev/null || true
|
||
local port="${PROXY_MIXED_PORT:-7890}"
|
||
local panel="${PANEL_PORT:-9090}"
|
||
local secret="${PANEL_SECRET:-}"
|
||
local cell="${CELLULAR_IFACE:-$DETECTED_CELL}"
|
||
local lan
|
||
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 <<EOM
|
||
|
||
========== 全自动安装完成 ==========
|
||
数据网卡(已绑定): ${cell}
|
||
语义: 走代理 → 数据流量;不走代理 → WiFi/默认
|
||
|
||
代理: HTTP/SOCKS ${lan:-127.0.0.1}:${port}
|
||
面板: http://${lan:-<LAN-IP>}:${panel}/ui/
|
||
密钥: ${secret}
|
||
配置: $INSTALL_DIR/etc/settings.conf
|
||
|
||
验证: cpxy verify
|
||
重绑: cpxy rebind
|
||
之后更新: 再执行同一条 curl 命令,或: sudo cpxy upgrade
|
||
日志: cpxy logs
|
||
EOM
|
||
}
|
||
|
||
main "$@"
|