Files
cellular-proxy/install.sh
T
Hermes aec388682b fix: 安装启动后不再卡住,保证脚本退出
- 去掉可能阻塞的 systemctl status 全量输出
- verify 使用 connect/max 超时;安装阶段 8s 快速验证
- 验证失败只告警,不阻塞安装结束
2026-07-21 11:26:30 +00:00

305 lines
9.1 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
#
# 最简(自动探测数据网卡 + 自动生成密钥 + 下载 sing-box + 启动 + 验证):
# curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash
#
# 可选参数仍可用:
# ... | sudo bash -s -- --iface wwan0 --secret xxx --user u --pass p
#
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
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 全自动一键安装
无参数即可:自动探测数据网卡 → 绑定出口 → 装 sing-box → 启动 → 验证
选项(全可选):
--iface NAME 强制指定数据网卡(跳过自动探测)
--secret STR 面板 PANEL_SECRET(默认随机生成)
--user USER 代理账号
--pass PASS 代理密码
--port N 代理端口(默认 7890)
--panel-port N 面板端口(默认 9090)
--memory MB MemoryMax(默认 96
--install-dir PATH 安装目录
--local DIR 本地源码,不拉 Gitea
--ref REF 分支/tag
--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 ;;
-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"
}
ensure_deps() {
local missing=()
for c in tar awk ip curl; do
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
done
# curl 可用 wget 替代
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"
[[ -f "$src/config/settings.conf.example" ]] || die "缺少 settings.conf.example"
cp "$src/config/settings.conf.example" "$conf"
# 预加载 lib 做自动探测(源码里的 lib)
# 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 [[ "$OPT_NO_AUTO_DETECT" -eq 1 ]]; then
die "--no-auto-detect 时必须提供 --iface"
else
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}"
if [[ -n "$OPT_SECRET" ]]; then
set_kv "$conf" PANEL_SECRET "$OPT_SECRET"
else
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
[[ -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"
# 给子 install 用
export DETECTED_CELL="$cell"
export DETECTED_SRC_IP="$src_ip"
}
main() {
need_root
ensure_deps
need_cmd tar
need_cmd awk
need_cmd ip
trap 'rm -rf "$WORK_DIR"' EXIT
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 # 已在 prepare 写死网卡
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
# 确保 settings 路径对 scripts/install 可见
# scripts/install 读 ROOT/config/settings.conf
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}/
密钥: ${secret}
配置: $INSTALL_DIR/etc/settings.conf
验证: cpxy verify
重绑: cpxy rebind # 换卡/重插后重新自动探测
日志: cpxy logs
EOM
}
main "$@"