Files
cellular-proxy/install.sh
T

415 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "请用 rootcurl -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 "$@"