Files
cellular-proxy/scripts/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

337 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
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=lib.sh
source "$ROOT_DIR/scripts/lib.sh"
AUTO_DETECT="${AUTO_DETECT:-true}"
AUTO_VERIFY="${AUTO_VERIFY:-true}"
SKIP_START="${SKIP_START:-false}"
if [[ ! -f "$ROOT_DIR/config/settings.conf" ]]; then
cp "$ROOT_DIR/config/settings.conf.example" "$ROOT_DIR/config/settings.conf"
warn "已创建 config/settings.conf(将自动探测数据网卡并生成密钥)"
fi
load_config
need_root
info "1/6 自动探测并绑定数据网卡"
"$ROOT_DIR/scripts/detect.sh" || true
cell=""
if [[ -n "${CELLULAR_IFACE:-}" ]] && iface_is_usb_gadget "$CELLULAR_IFACE"; then
warn "配置的 CELLULAR_IFACE=$CELLULAR_IFACE 是 USB gadget 网卡(面向上游主机),忽略并重新探测"
cell=""
elif [[ -n "${CELLULAR_IFACE:-}" ]] && iface_exists "$CELLULAR_IFACE"; then
cell="$CELLULAR_IFACE"
info "使用配置的数据网卡: $cell"
fi
if [[ -z "$cell" && "$AUTO_DETECT" == "true" ]]; then
cell="$(detect_cellular_iface "" || true)"
fi
if [[ -n "$cell" ]] && iface_is_usb_gadget "$cell"; then
die "拒绝把 USB gadget 网卡 $cell 写成数据出口,请手动设置 CELLULAR_IFACE=wwanX"
fi
if [[ -z "$cell" ]]; then
die "未能自动探测数据网卡。
请插入/拨通数据模块后重试,或手动:
1) ./scripts/detect.sh
2) 编辑 config/settings.conf 设置 CELLULAR_IFACE=网卡名
3) sudo ./scripts/install.sh"
fi
src_ip="$(detect_source_ip "$cell" || true)"
persist_cellular_to_settings "$ROOT_DIR/config/settings.conf" "$cell" "$src_ip"
# 重新加载
CELLULAR_IFACE="$cell"
CELLULAR_SOURCE_IP="$src_ip"
info "已绑定 CELLULAR_IFACE=$cell source_ip=${src_ip:-auto}"
# 随机面板密钥(若仍是默认)
if [[ "$PANEL_SECRET" == "please-change-me" || "$PANEL_SECRET" == "change-this-secret" || -z "$PANEL_SECRET" ]]; then
gen="$(openssl rand -hex 12 2>/dev/null || head -c 16 /dev/urandom | xxd -p | tr -d '\n')"
tmp="$(mktemp)"
awk -v k="PANEL_SECRET" -v v="$gen" '
BEGIN { done=0 }
index($0, k "=")==1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "$ROOT_DIR/config/settings.conf" > "$tmp"
mv "$tmp" "$ROOT_DIR/config/settings.conf"
PANEL_SECRET="$gen"
warn "已自动生成 PANEL_SECRET(见 settings.conf"
fi
# 再 load 一次保证变量一致
load_config
CELLULAR_IFACE="$cell"
CELLULAR_SOURCE_IP="${src_ip:-$CELLULAR_SOURCE_IP}"
info "2/6 下载/安装 sing-box + UI"
"$ROOT_DIR/scripts/fetch-binaries.sh"
info "3/6 生成配置(代理出口 bind = $cell"
"$ROOT_DIR/scripts/generate.sh" "$ROOT_DIR/generated"
ensure_dirs
mkdir -p /var/lib/cellular-proxy
install -m 0644 "$ROOT_DIR/generated/config.json" "$INSTALL_DIR/etc/config.json"
install -m 0644 "$ROOT_DIR/generated/runtime.env" "$INSTALL_DIR/etc/runtime.env"
install -m 0644 "$ROOT_DIR/config/settings.conf" "$INSTALL_DIR/etc/settings.conf"
cp -a "$ROOT_DIR/scripts" "$INSTALL_DIR/"
cp -a "$ROOT_DIR/ui" "$INSTALL_DIR/" 2>/dev/null || true
if ! "$INSTALL_DIR/bin/sing-box" check -c "$INSTALL_DIR/etc/config.json"; then
die "sing-box 配置校验失败"
fi
# 确认 bind_interface 写进配置
if ! grep -q "\"bind_interface\": \"$cell\"" "$INSTALL_DIR/etc/config.json" \
&& ! grep -q "\"bind_interface\": \"$cell\"" "$INSTALL_DIR/etc/config.json" 2>/dev/null; then
# JSON 可能无空格差异
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
fi
info "配置已确认 bind_interface=$cell"
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)"
# 兜底:避免 symlink 解析失败时 BASE 变成 /usr/local
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)
# 重新探测:若 settings 里网卡丢失则自动补;源 IP 漂移也刷新
# 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
;;
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
;;
start) systemctl start cellular-proxy ;;
stop) systemctl stop cellular-proxy ;;
restart) systemctl restart cellular-proxy ;;
status) systemctl status cellular-proxy --no-pager || 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
;;
upgrade)
# 一条命令在线增量升级(保留密钥/网卡)
if [[ -x "$BASE/scripts/online-upgrade.sh" ]]; then
exec "$BASE/scripts/online-upgrade.sh" "$@"
fi
# 兜底:直接 curl 入口
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" "$@"
;;
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 # 设置 7890 代理账号密码
cpxy auth --clear | --show
cpxy watch [status|once|start|stop|logs] # WWAN 自动监控
cpxy upgrade # 一条命令在线增量升级
cpxy upgrade --ui-only # 只更新面板
cpxy upgrade --force-binary # 强制重下 sing-box
cpxy upgrade --verify # 升级后验证出口
H
;;
esac
EOF
chmod +x "$INSTALL_DIR/scripts/"*.sh "$INSTALL_DIR/scripts/"*.py 2>/dev/null || true
ln -sfn "$INSTALL_DIR/bin/cpxy" /usr/local/bin/cpxy
info "4/6 安装 systemd"
MEM="${MEMORY_MAX_MB:-96}"
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
# 管理 APIUI 配置代理账密)
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 自动监控:事件驱动 + 定时兜底
install_watch_units() {
local u
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
}
install_watch_units
systemctl daemon-reload
info "5/6 启动服务"
export SYSTEMD_PAGER=cat
export SYSTEMD_COLORS=0
if [[ "$SKIP_START" == "true" ]]; then
systemctl enable cellular-proxy.service
systemctl enable cellular-proxy-admin.service 2>/dev/null || true
systemctl stop cellular-proxy.service 2>/dev/null || true
systemctl stop cellular-proxy-admin.service 2>/dev/null || true
info "已按 SKIP_START 跳过启动"
else
systemctl enable cellular-proxy.service
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
# 等待 active,最多约 8 秒(避免 status 卡住)
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"
systemctl show cellular-proxy.service -p ActiveState -p SubState -p Result -p ExecMainStatus --no-pager 2>/dev/null || true
journalctl -u cellular-proxy -n 40 --no-pager 2>/dev/null || true
die "启动失败,请检查数据网卡 $cell 是否 up 且有 IP"
fi
fi
# 默认开启 watchENABLE_CELLULAR_WATCH!=false
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
info "6/6 自动验证出口(短超时,失败不阻塞安装结束)"
if [[ "$SKIP_START" != "true" && "$AUTO_VERIFY" == "true" ]]; then
set +e
# 安装阶段用短超时;VERIFY_QUICK=1 跳过 detect 长输出
VERIFY_QUICK=1 EGRESS_TIMEOUT="${EGRESS_TIMEOUT:-8}" \
"$ROOT_DIR/scripts/verify.sh"
vr=$?
set -e
if [[ "$vr" -eq 0 ]]; then
info "出口验证通过:代理已从数据网卡出"
else
warn "自动验证未通过(exit=$vr)。安装仍算完成,稍后可: cpxy verify"
warn "当前绑定: CELLULAR_IFACE=$cell ip=${src_ip:-}"
fi
else
info "已跳过自动验证"
fi
echo
info "安装完成(全自动探测 + 绑定)"
echo " 数据网卡: $cell (${src_ip:-no-ipv4})"
echo " 语义: 不走代理 → 系统默认(WiFi);走代理 → 数据流量"
echo " 代理: HTTP/SOCKS ${PROXY_LISTEN_HOST}:${PROXY_MIXED_PORT}"
if [[ -n "${PROXY_USER:-}" ]]; then
echo " 代理鉴权: 用户 ${PROXY_USER}(密码已设置)"
else
echo " 代理鉴权: 无(建议: cpxy auth --user u --pass p 或面板配置)"
fi
echo " 面板: http://<LAN-IP>:${PANEL_PORT}/ui/ 密钥: $PANEL_SECRET"
echo " 管理API: http://<LAN-IP>:9091 UI 里改代理账密用,密钥同上)"
echo " 配置: $INSTALL_DIR/etc/settings.conf"
echo " 管理: cpxy rebind | verify | logs | status"
echo " 账密: cpxy auth --user U --pass P | --clear | --show"
echo " 若刚才像卡住:多半在测公网出口,现已改为短超时并保证退出"
# 安装脚本始终以 0 结束(服务已 active);验证失败只告警
exit 0