#!/usr/bin/env bash # cellular-proxy 一键安装 # 语义:走本代理的连接一律从数据网卡(流量)出口;系统默认仍可走 WiFi # # 用法示例: # curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash # curl -fsSL '...' | sudo bash -s -- --iface wwan0 --secret 'xxx' --user u --pass p # sudo bash install.sh --iface usb0 --port 7890 --panel-port 9090 # 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}" # 可用环境变量覆盖:CELLULAR_PROXY_REF=v1.0.0 或 commit sha REF="${CELLULAR_PROXY_REF:-$BRANCH}" INSTALL_DIR="${INSTALL_DIR:-/opt/cellular-proxy}" WORK_DIR="${WORK_DIR:-/tmp/cellular-proxy-install-$$}" # 可选 CLI 预填(也可装完再改 settings) OPT_IFACE="" OPT_SECRET="" OPT_USER="" OPT_PASS="" OPT_PORT="" OPT_PANEL_PORT="" OPT_MEMORY="" OPT_SKIP_START=0 OPT_LOCAL_DIR="" 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 一键安装 选项: --iface NAME 数据网卡名(如 wwan0 / usb0) --secret STR 面板 PANEL_SECRET --user USER 代理账号 PROXY_USER --pass PASS 代理密码 PROXY_PASS --port N 代理 mixed 端口(默认 7890) --panel-port N 面板端口(默认 9090) --memory MB MemoryMax MB(默认 96;0=不限制) --install-dir PATH 安装目录(默认 /opt/cellular-proxy) --local DIR 使用本地源码目录,不从 Gitea 下载 --ref REF 分支/tag/commit(默认 main) --skip-start 只安装不启动 -h, --help 帮助 环境变量: GITEA_BASE REPO_OWNER REPO_NAME CELLULAR_PROXY_REF INSTALL_DIR 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 ;; -h|--help) usage; exit 0 ;; *) die "未知参数: $1(--help 查看用法)" ;; esac done need_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then die "请用 root 运行:sudo bash install.sh ..." fi } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1" } 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" elif command -v wget >/dev/null 2>&1; then wget -O "$dest" "$url" else die "需要 curl 或 wget" fi } set_kv() { # set_kv file KEY value — 存在则替换行,否则追加 local file="$1" key="$2" val="$3" local tmp tmp="$(mktemp)" if grep -qE "^${key}=" "$file" 2>/dev/null; then # 用 awk 避免 sed 对特殊字符敏感 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 "下载失败。请检查网络或: git clone ${GITEA_BASE}/${REPO_OWNER}/${REPO_NAME}.git" 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" if [[ -n "$OPT_IFACE" ]]; then set_kv "$conf" CELLULAR_IFACE "$OPT_IFACE" fi if [[ -n "$OPT_SECRET" ]]; then set_kv "$conf" PANEL_SECRET "$OPT_SECRET" else # 随机 secret,避免默认 please-change-me 裸奔 local gen gen="$(openssl rand -hex 12 2>/dev/null || head -c 16 /dev/urandom | xxd -p)" set_kv "$conf" PANEL_SECRET "$gen" warn "已生成随机 PANEL_SECRET(见 $INSTALL_DIR/etc/settings.conf)" fi if [[ -n "$OPT_USER" ]]; then set_kv "$conf" PROXY_USER "$OPT_USER" fi if [[ -n "$OPT_PASS" ]]; then set_kv "$conf" PROXY_PASS "$OPT_PASS" fi if [[ -n "$OPT_PORT" ]]; then set_kv "$conf" PROXY_MIXED_PORT "$OPT_PORT" fi if [[ -n "$OPT_PANEL_PORT" ]]; then set_kv "$conf" PANEL_PORT "$OPT_PANEL_PORT" fi if [[ -n "$OPT_MEMORY" ]]; then set_kv "$conf" MEMORY_MAX_MB "$OPT_MEMORY" fi set_kv "$conf" INSTALL_DIR "$INSTALL_DIR" } main() { need_root need_cmd tar need_cmd awk command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || die "需要 curl 或 wget" trap 'rm -rf "$WORK_DIR"' EXIT info "1/4 获取源码" fetch_source info "2/4 写入 settings.conf" prepare_settings info "3/4 调用官方 install.sh(下载 sing-box + systemd)" # 临时把 install 行为调整:settings 已存在 export CONFIG_FILE="$WORK_DIR/src/config/settings.conf" bash "$WORK_DIR/src/scripts/install.sh" if [[ "$OPT_SKIP_START" -eq 1 ]]; then systemctl stop cellular-proxy 2>/dev/null || true info "已按 --skip-start 停止服务" fi info "4/4 完成" local port panel secret # shellcheck disable=SC1090 source "$INSTALL_DIR/etc/settings.conf" 2>/dev/null || true port="${PROXY_MIXED_PORT:-7890}" panel="${PANEL_PORT:-9090}" secret="${PANEL_SECRET:-见 $INSTALL_DIR/etc/settings.conf}" 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 <}:${port} 面板: http://${lan:-}:${panel}/ 密钥: ${secret} 配置: $INSTALL_DIR/etc/settings.conf 验证: cpxy verify curl --socks5-hostname 127.0.0.1:${port} https://ifconfig.me 管理: cpxy status | logs | generate | restart EOM } main "$@"