Files
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

456 lines
17 KiB
Bash
Executable File
Raw Permalink 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
# 增量升级:保留 settings/密钥/网卡绑定,默认不重下 sing-box、不重探测、不强制 verify
# 用法:
# sudo ./scripts/upgrade.sh
# sudo ./scripts/upgrade.sh --ui-only
# sudo ./scripts/upgrade.sh --force-binary
# sudo ./scripts/upgrade.sh --rebind
# sudo ./scripts/upgrade.sh --verify
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=lib.sh
source "$ROOT_DIR/scripts/lib.sh"
UI_ONLY=false
FORCE_BINARY=false
DO_REBIND=false
DO_VERIFY=false
SKIP_START=false
usage() {
cat <<'H'
cellular-proxy 增量升级
保留: settings.conf / PANEL_SECRET / PROXY_* / CELLULAR_IFACE
默认: 不重新探测网卡、不重下 sing-box(已存在且可执行则跳过)、不强制出口验证
选项:
--ui-only 只更新 UI 面板
--force-binary 强制重下/重装 sing-box
--rebind 升级时重新探测数据网卡并写回配置
--verify 升级后跑出口验证
--skip-start 只落盘,不 restart 服务
-h, --help
H
}
while [[ $# -gt 0 ]]; do
case "$1" in
--ui-only) UI_ONLY=true; shift ;;
--force-binary) FORCE_BINARY=true; shift ;;
--rebind) DO_REBIND=true; shift ;;
--verify) DO_VERIFY=true; shift ;;
--skip-start) SKIP_START=true; shift ;;
-h|--help) usage; exit 0 ;;
*) die "未知参数: $1" ;;
esac
done
need_root
# 优先用已安装目录的配置
INST_CANDIDATES=(
"${INSTALL_DIR:-}"
"/opt/cellular-proxy"
"$ROOT_DIR"
)
EXISTING=""
for d in "${INST_CANDIDATES[@]}"; do
[[ -n "$d" ]] || continue
if [[ -f "$d/etc/settings.conf" ]]; then
EXISTING="$d"
break
fi
done
if [[ -z "$EXISTING" ]]; then
die "未检测到已安装实例(缺少 \$INSTALL_DIR/etc/settings.conf)。
请先全量安装:
curl -fsSL 'https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh' | sudo bash"
fi
export CONFIG_FILE="$EXISTING/etc/settings.conf"
load_config
INSTALL_DIR="${INSTALL_DIR:-$EXISTING}"
# 若 settings 里 INSTALL_DIR 与探测到的不一致,以配置为准;配置空则用探测目录
if [[ -z "${INSTALL_DIR:-}" || ! -d "$INSTALL_DIR" ]]; then
INSTALL_DIR="$EXISTING"
fi
export INSTALL_DIR
info "======== cellular-proxy 增量升级 ========"
info "安装目录: $INSTALL_DIR"
info "配置文件: $CONFIG_FILE(将保留密钥/网卡/账密)"
# --- 迁移:老配置里的 usb/enx 关键词会把 USB gadget 网卡(usb0)当数据出口 ---
# 升级保留 settings.conf,因此必须在这里就地清理,否则旧机器升级后仍会误绑。
migrate_iface_patterns() {
local conf="$1" cur cleaned tmp
[[ -f "$conf" ]] || return 0
cur="$(awk -F= '/^CELLULAR_IFACE_PATTERNS=/{print substr($0,index($0,"=")+1); exit}' "$conf" 2>/dev/null || true)"
[[ -n "$cur" ]] || return 0
cleaned="$(printf '%s' "$cur" | tr ',' '\n' | sed 's/[[:space:]]//g' \
| grep -vxE 'usb|enx' | paste -sd, - 2>/dev/null || true)"
[[ -n "$cleaned" && "$cleaned" != "$cur" ]] || return 0
tmp="$(mktemp)"
awk -v v="$cleaned" '
/^CELLULAR_IFACE_PATTERNS=/ { print "CELLULAR_IFACE_PATTERNS=" v; next }
{ print }
' "$conf" > "$tmp"
mv "$tmp" "$conf"
warn "已从 CELLULAR_IFACE_PATTERNS 移除 usb/enx(会误命中 USB gadget 网卡): $cur -> $cleaned"
CELLULAR_IFACE_PATTERNS="$cleaned"
}
migrate_iface_patterns "$CONFIG_FILE"
# 迁移:已写死 gadget 网卡的旧配置,清空后走重新探测
if [[ -n "${CELLULAR_IFACE:-}" ]] && iface_is_usb_gadget "${CELLULAR_IFACE:-}"; then
warn "旧配置 CELLULAR_IFACE=$CELLULAR_IFACE 是 USB gadget 网卡,将重新探测数据出口"
CELLULAR_IFACE=""
fi
ensure_dirs
mkdir -p /var/lib/cellular-proxy "$INSTALL_DIR/bin" "$INSTALL_DIR/etc" "$INSTALL_DIR/ui" "$INSTALL_DIR/scripts" "$INSTALL_DIR/generated"
# --- UI only fast path ---
if [[ "$UI_ONLY" == "true" ]]; then
info "模式: 仅更新 UI"
if [[ ! -f "$ROOT_DIR/ui/index.html" ]]; then
die "源码缺少 ui/index.html"
fi
cp -a "$ROOT_DIR/ui/." "$INSTALL_DIR/ui/"
info "UI 已更新 -> $INSTALL_DIR/ui"
# 静态文件无需重启代理;admin 也不必
echo
info "完成(ui-only)。浏览器强刷: http://<LAN-IP>:${PANEL_PORT}/ui/"
exit 0
fi
# --- 可选 rebind ---
if [[ "$DO_REBIND" == "true" ]]; then
info "重新探测数据网卡…"
cell="$(detect_cellular_iface "" || true)"
[[ -n "$cell" ]] || die "未能探测数据网卡"
! iface_is_usb_gadget "$cell" || die "探测到的 $cell 是 USB gadget 网卡(面向上游主机),不能作为数据出口"
src="$(detect_source_ip "$cell" || true)"
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src"
load_config
CELLULAR_IFACE="$cell"
CELLULAR_SOURCE_IP="${src:-}"
info "已 rebind: $CELLULAR_IFACE ${CELLULAR_SOURCE_IP:-}"
fi
cell="${CELLULAR_IFACE:-}"
if [[ -z "$cell" ]] || ! iface_exists "$cell" || iface_is_usb_gadget "$cell"; then
if [[ -n "$cell" ]] && iface_is_usb_gadget "$cell"; then
warn "配置中的 CELLULAR_IFACE=$cell 是 USB gadget 网卡,不能作为出口,重新探测"
else
warn "配置中的 CELLULAR_IFACE=${cell:-} 无效,尝试自动探测(仅本次)"
fi
cell="$(detect_cellular_iface "" || true)"
if [[ -n "$cell" ]] && ! iface_is_usb_gadget "$cell"; then
src="$(detect_source_ip "$cell" || true)"
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src"
load_config
CELLULAR_IFACE="$cell"
CELLULAR_SOURCE_IP="${src:-$CELLULAR_SOURCE_IP}"
else
die "无有效数据网卡。请: cpxy rebind 或 --rebind / --iface"
fi
fi
src_ip="${CELLULAR_SOURCE_IP:-$(detect_source_ip "$cell" || true)}"
# --- binary ---
need_binary=true
if [[ "$FORCE_BINARY" != "true" && -x "$INSTALL_DIR/bin/sing-box" ]]; then
if "$INSTALL_DIR/bin/sing-box" version >/dev/null 2>&1; then
cur="$("$INSTALL_DIR/bin/sing-box" version 2>/dev/null | head -1 || true)"
info "复用已有 sing-box: $cur"
need_binary=false
fi
fi
if [[ "$need_binary" == "true" ]]; then
info "安装/更新 sing-box 二进制"
FORCE_DOWNLOAD="${FORCE_DOWNLOAD:-}"
if [[ "$FORCE_BINARY" == "true" ]]; then
export FORCE_DOWNLOAD=1
fi
# fetch 会同时拷 UI;后面还会再拷一遍 scripts/ui 没关系
export CONFIG_FILE
"$ROOT_DIR/scripts/fetch-binaries.sh"
else
info "跳过二进制下载(需要强制时加 --force-binary"
mkdir -p "$INSTALL_DIR/ui"
if [[ -f "$ROOT_DIR/ui/index.html" ]]; then
cp -a "$ROOT_DIR/ui/." "$INSTALL_DIR/ui/"
info "已更新 UI"
fi
fi
# --- scripts / cpxy / generate config ---
info "更新 scripts / 配置生成 / systemd"
# 同步新脚本到安装目录,但不要覆盖 etc/settings
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete \
--exclude 'settings.conf' \
"$ROOT_DIR/scripts/" "$INSTALL_DIR/scripts/"
else
mkdir -p "$INSTALL_DIR/scripts"
cp -a "$ROOT_DIR/scripts/." "$INSTALL_DIR/scripts/"
fi
chmod +x "$INSTALL_DIR/scripts/"*.sh "$INSTALL_DIR/scripts/"*.py 2>/dev/null || true
# 生成配置(用现有 settings,不改 secret
export CONFIG_FILE
"$ROOT_DIR/scripts/generate.sh" "$INSTALL_DIR/generated"
install -m 0644 "$INSTALL_DIR/generated/config.json" "$INSTALL_DIR/etc/config.json"
install -m 0644 "$INSTALL_DIR/generated/runtime.env" "$INSTALL_DIR/etc/runtime.env"
# 保留 settings:只确保 INSTALL_DIR 字段正确(CONFIG_FILE 已是 live 路径时勿 self-copy
if ! grep -qE '^INSTALL_DIR=' "$CONFIG_FILE" 2>/dev/null; then
printf 'INSTALL_DIR=%s\n' "$INSTALL_DIR" >> "$CONFIG_FILE"
fi
live_settings="$INSTALL_DIR/etc/settings.conf"
if [[ "$(readlink -f "$CONFIG_FILE" 2>/dev/null || echo "$CONFIG_FILE")" != "$(readlink -f "$live_settings" 2>/dev/null || echo "$live_settings")" ]]; then
install -m 0644 "$CONFIG_FILE" "$live_settings"
fi
if ! "$INSTALL_DIR/bin/sing-box" check -c "$INSTALL_DIR/etc/config.json"; then
die "sing-box 配置校验失败"
fi
# 校验 bind
if ! python3 - "$INSTALL_DIR/etc/config.json" "$cell" <<'PY'
import json,sys
c=json.load(open(sys.argv[1]))
cell=sys.argv[2]
ok=any(o.get("tag")=="cellular" and o.get("bind_interface")==cell for o in c.get("outbounds",[]))
sys.exit(0 if ok else 1)
PY
then
die "配置未正确绑定数据网卡 $cell"
fi
info "配置已确认 bind_interface=$cell"
# 安装 cpxy 包装器(与 install.sh 一致,含 upgrade 子命令)
install -m 0755 /dev/stdin "$INSTALL_DIR/bin/cpxy" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
# 经 /usr/local/bin/cpxy 符号链接调用时,$0 仍是链接路径;必须 resolve 真实路径
_self="${BASH_SOURCE[0]:-$0}"
if command -v readlink >/dev/null 2>&1; then
_resolved="$(readlink -f "$_self" 2>/dev/null || true)"
[[ -n "$_resolved" ]] && _self="$_resolved"
fi
BASE="$(cd "$(dirname "$_self")/.." && pwd)"
if [[ ! -d "$BASE/scripts" && -d /opt/cellular-proxy/scripts ]]; then
BASE="/opt/cellular-proxy"
fi
export CONFIG_FILE="${CONFIG_FILE:-$BASE/etc/settings.conf}"
cmd="${1:-help}"
shift || true
case "$cmd" in
detect) exec "$BASE/scripts/detect.sh" "$@" ;;
generate)
# shellcheck source=/dev/null
source "$BASE/scripts/lib.sh"
load_config
if [[ -z "${CELLULAR_IFACE:-}" ]] || ! iface_exists "${CELLULAR_IFACE:-}" || iface_is_usb_gadget "${CELLULAR_IFACE:-}"; then
if [[ -n "${CELLULAR_IFACE:-}" ]] && iface_is_usb_gadget "${CELLULAR_IFACE:-}"; then
warn "CELLULAR_IFACE=$CELLULAR_IFACE 是 USB gadget 网卡,重新探测"
fi
cell="$(detect_cellular_iface "" || true)"
if [[ -n "$cell" ]] && ! iface_is_usb_gadget "$cell"; then
src="$(detect_source_ip "$cell" || true)"
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src"
CELLULAR_IFACE="$cell"
CELLULAR_SOURCE_IP="${src:-}"
fi
elif [[ -n "${CELLULAR_IFACE:-}" ]]; then
src="$(detect_source_ip "$CELLULAR_IFACE" || true)"
if [[ -n "$src" && "${CELLULAR_SOURCE_IP:-}" != "$src" ]]; then
persist_cellular_to_settings "$CONFIG_FILE" "$CELLULAR_IFACE" "$src"
CELLULAR_SOURCE_IP="$src"
fi
fi
"$BASE/scripts/generate.sh" "$BASE/generated"
install -m 0644 "$BASE/generated/config.json" "$BASE/etc/config.json"
install -m 0644 "$BASE/generated/runtime.env" "$BASE/etc/runtime.env"
if [[ "$(readlink -f "$CONFIG_FILE" 2>/dev/null || echo "$CONFIG_FILE")" != "$(readlink -f "$BASE/etc/settings.conf" 2>/dev/null || echo "$BASE/etc/settings.conf")" ]]; then
install -m 0644 "$CONFIG_FILE" "$BASE/etc/settings.conf" 2>/dev/null || true
fi
"$BASE/bin/sing-box" check -c "$BASE/etc/config.json"
systemctl restart cellular-proxy 2>/dev/null || true
systemctl restart cellular-proxy-admin 2>/dev/null || true
;;
rebind)
# shellcheck source=/dev/null
source "$BASE/scripts/lib.sh"
load_config
cell="$(detect_cellular_iface "" || true)"
[[ -n "$cell" ]] || die "未能探测数据网卡"
! iface_is_usb_gadget "$cell" || die "探测到的 $cell 是 USB gadget 网卡,不能作为数据出口"
src="$(detect_source_ip "$cell" || true)"
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src"
info "已重新绑定 $cell src=${src:-}"
exec "$0" generate
;;
upgrade)
# 一条命令在线升级:拉最新 install.sh 再 --upgrade
if [[ -x "$BASE/scripts/online-upgrade.sh" ]]; then
exec "$BASE/scripts/online-upgrade.sh" "$@"
fi
url="${CELLULAR_PROXY_INSTALL_URL:-https://gitea.chickliu.fun/Hermes/cellular-proxy/raw/branch/main/install.sh}"
exec bash -c 'curl -fsSL "$1" | bash -s -- --upgrade --install-dir "$2" "${@:3}"' _ "$url" "$BASE" "$@"
;;
start) systemctl start cellular-proxy cellular-proxy-admin 2>/dev/null || systemctl start cellular-proxy ;;
stop) systemctl stop cellular-proxy-admin 2>/dev/null || true; systemctl stop cellular-proxy ;;
restart) systemctl restart cellular-proxy; systemctl restart cellular-proxy-admin 2>/dev/null || true ;;
status)
systemctl status cellular-proxy --no-pager || true
systemctl status cellular-proxy-admin --no-pager 2>/dev/null || true
;;
verify) exec "$BASE/scripts/verify.sh" "$@" ;;
auth)
exec "$BASE/scripts/apply-proxy-auth.sh" "$@"
;;
watch)
case "${1:-status}" in
once) exec "$BASE/scripts/watch-cellular.sh" --once ;;
status)
systemctl status cellular-proxy-watch.service --no-pager 2>/dev/null || true
systemctl status cellular-proxy-watch.timer --no-pager 2>/dev/null || true
;;
start|enable)
systemctl enable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
;;
stop|disable)
systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
;;
logs) journalctl -u cellular-proxy-watch -n "${2:-80}" --no-pager ;;
*) exec "$BASE/scripts/watch-cellular.sh" "$@" ;;
esac
;;
logs) journalctl -u cellular-proxy -n "${1:-80}" -f ;;
help|*)
cat <<H
cpxy — cellular-proxy
语义: 走代理的连接一律从数据网卡出口
cpxy detect | rebind | generate | start | stop | restart | status
cpxy verify | logs [N]
cpxy auth --user U --pass P | --clear | --show
cpxy watch [status|once|start|stop|logs] # WWAN 自动监控
cpxy upgrade # 一条命令在线增量升级
cpxy upgrade --ui-only | --force-binary | --verify
H
;;
esac
EOF
ln -sfn "$INSTALL_DIR/bin/cpxy" /usr/local/bin/cpxy
# systemd
MEM="${MEMORY_MAX_MB:-96}"
if [[ -f "$ROOT_DIR/systemd/cellular-proxy.service" ]]; then
install -m 0644 "$ROOT_DIR/systemd/cellular-proxy.service" /etc/systemd/system/cellular-proxy.service
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy.service
sed -i "s|@LOG_DIR@|$LOG_DIR|g" /etc/systemd/system/cellular-proxy.service
if [[ "$MEM" != "0" && -n "$MEM" ]]; then
sed -i "s|@MEMORY_MAX@|${MEM}M|g" /etc/systemd/system/cellular-proxy.service
else
sed -i '/MemoryMax=@MEMORY_MAX@/d' /etc/systemd/system/cellular-proxy.service
fi
fi
if [[ -f "$ROOT_DIR/systemd/cellular-proxy-admin.service" ]]; then
install -m 0644 "$ROOT_DIR/systemd/cellular-proxy-admin.service" /etc/systemd/system/cellular-proxy-admin.service
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy-admin.service
fi
# WWAN 自动监控
for u in cellular-proxy-watch.service cellular-proxy-watch-once.service cellular-proxy-watch.timer; do
if [[ -f "$ROOT_DIR/systemd/$u" ]]; then
install -m 0644 "$ROOT_DIR/systemd/$u" "/etc/systemd/system/$u"
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" "/etc/systemd/system/$u"
sed -i "s|@LOG_DIR@|${LOG_DIR}|g" "/etc/systemd/system/$u"
fi
done
chmod +x "$INSTALL_DIR/scripts/watch-cellular.sh" 2>/dev/null || true
systemctl daemon-reload
export SYSTEMD_PAGER=cat
export SYSTEMD_COLORS=0
if [[ "$SKIP_START" == "true" ]]; then
systemctl enable cellular-proxy.service 2>/dev/null || true
systemctl enable cellular-proxy-admin.service 2>/dev/null || true
info "已跳过 restart--skip-start"
else
systemctl enable cellular-proxy.service 2>/dev/null || true
systemctl enable cellular-proxy-admin.service 2>/dev/null || true
systemctl restart cellular-proxy.service
systemctl restart cellular-proxy-admin.service 2>/dev/null || true
ok=0
for _ in 1 2 3 4 5 6 7 8; do
if systemctl is-active --quiet cellular-proxy.service; then
ok=1
break
fi
sleep 1
done
active_state="$(systemctl is-active cellular-proxy.service 2>/dev/null || echo unknown)"
info "服务状态: $active_state"
if [[ "$ok" -ne 1 ]]; then
err "服务未 active"
journalctl -u cellular-proxy -n 30 --no-pager 2>/dev/null || true
die "升级后启动失败"
fi
fi
watch_flag="$(echo "${ENABLE_CELLULAR_WATCH:-true}" | tr '[:upper:]' '[:lower:]')"
if [[ "$watch_flag" == "false" || "$watch_flag" == "0" || "$watch_flag" == "no" ]]; then
systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
info "ENABLE_CELLULAR_WATCH=false,未启用自动监控"
else
systemctl enable cellular-proxy-watch.service 2>/dev/null || true
systemctl enable cellular-proxy-watch.timer 2>/dev/null || true
if [[ "$SKIP_START" != "true" ]]; then
systemctl restart cellular-proxy-watch.service 2>/dev/null || true
systemctl restart cellular-proxy-watch.timer 2>/dev/null || true
fi
info "已启用 cellular-proxy-watch(网卡/源 IP 变化自动 rebind"
fi
if [[ "$DO_VERIFY" == "true" && "$SKIP_START" != "true" ]]; then
info "出口验证…"
set +e
VERIFY_QUICK=1 EGRESS_TIMEOUT="${EGRESS_TIMEOUT:-8}" "$INSTALL_DIR/scripts/verify.sh"
vr=$?
set -e
if [[ "$vr" -eq 0 ]]; then
info "出口验证通过"
else
warn "验证未通过(exit=$vr),可稍后: cpxy verify"
fi
else
info "已跳过出口验证(需要时: --verify 或 cpxy verify"
fi
# shellcheck disable=SC1090
source "$INSTALL_DIR/etc/settings.conf" 2>/dev/null || true
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
========== 增量升级完成 ==========
安装目录: $INSTALL_DIR
数据网卡: ${CELLULAR_IFACE:-$cell} (${src_ip:-})
保留: PANEL_SECRET / PROXY_USER / 端口等 settings
代理: ${lan:-127.0.0.1}:${PROXY_MIXED_PORT:-7890}
面板: http://${lan:-<LAN-IP>}:${PANEL_PORT:-9090}/ui/
管理: :9091(账密配置)
下次升级:
sudo cpxy upgrade
sudo cpxy upgrade --ui-only
sudo cpxy upgrade --force-binary
EOM
exit 0