Files
cellular-proxy/install.sh
T
Hermes 88f2ff44aa fix(detect): never bind USB gadget iface as proxy egress
设备把自己当 USB 网卡挂给上游主机时会出现 usb0(驱动 configfs-gadget.g1,
IP 192.168.68.1)。该链路只通向 PC,被当成出口后所有出站都 context
deadline exceeded,客户端看到 socks connect failed rep=0x01。

误选原因:score_iface_as_cellular 给 usb0 与真出口 wwan0 都打 175 分。
usb0 靠名称命中 patterns 里的 usb(+80)与非默认路由(+40);wwan0 的
bam-dmux 驱动不在白名单里,白丢 +100。detect_cellular_iface 用 -gt 比较,
list_ifaces 按名排序让 usb0 先入选,平分下无法被顶替。

改动:
- lib.sh 新增 iface_is_usb_gadget(),按驱动名与 gadget 总线路径识别,
  并并入 is_virtual_or_skip_iface(usb0 评分 175 -> 0)
- 驱动白名单补 bam-dmux/bam_dmux/qcom-ipa/ipa_wan;名称权重 80 -> 40,
  确保驱动证据始终压过名称猜测
- 新增 detect_cellular_iface_via_mm(),把 ModemManager bearer 的
  interface: 作为第 0 步权威来源
- detect_cellular_iface / resolve_cellular 全链路拒绝 gadget 网卡,
  取不到真出口时按 REQUIRE_CELLULAR_IFACE 直接失败而不是绑错
- generate/install/upgrade/watch/detect/verify 各入口独立设闸,
  verify.sh 发现出口是 gadget 时直接 exit 4 并给出修复命令
- 默认 CELLULAR_IFACE_PATTERNS 去掉 usb/enx;upgrade.sh 与根 install.sh
  就地迁移已有 settings.conf,旧机器升级即修复
- 新增 tests/detect-gadget.sh:18 条离线断言,无需真机
2026-08-23 18:39:34 +08:00

438 lines
14 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,ppp,cdc,rmnet,ccmni,mbim,qmi}"
REQUIRE_CELLULAR_IFACE=false
# 迁移旧配置:usb/enx 关键词会命中本机的 USB gadget 网卡(usb0),必须剔除
local old_patterns cleaned
old_patterns="$(awk -F= '/^CELLULAR_IFACE_PATTERNS=/{print substr($0,index($0,"=")+1); exit}' "$conf" 2>/dev/null || true)"
if [[ -n "$old_patterns" ]]; then
cleaned="$(printf '%s' "$old_patterns" | tr ',' '\n' | sed 's/[[:space:]]//g' \
| grep -vxE 'usb|enx' | paste -sd, - 2>/dev/null || true)"
if [[ -n "$cleaned" && "$cleaned" != "$old_patterns" ]]; then
set_kv "$conf" CELLULAR_IFACE_PATTERNS "$cleaned"
CELLULAR_IFACE_PATTERNS="$cleaned"
warn "已清理 CELLULAR_IFACE_PATTERNS: $old_patterns -> $cleaned"
fi
fi
local cell=""
if [[ -n "$OPT_IFACE" ]]; then
cell="$OPT_IFACE"
info "使用 --iface $cell"
if iface_is_usb_gadget "$cell"; then
die "--iface $cell 是本机对上游主机暴露的 USB gadget 网卡,不能作为数据出口"
fi
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_is_usb_gadget "$cell"; then
warn "已有配置里的 $cell 是 USB gadget 网卡(面向上游主机),丢弃并重新探测"
cell=""
elif [[ -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
if iface_is_usb_gadget "$cell"; then
die "拒绝把 USB gadget 网卡 $cell 写成数据出口,请用 --iface wwanX 指定真实数据网卡"
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 "$@"