Files
cellular-proxy/tests/detect-gadget.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

188 lines
6.9 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 的探测链路永不选中 USB gadget 网卡。
# 用法: bash tests/detect-gadget.sh [repo路径]
# 不依赖真实 /sys,靠覆盖探针函数模拟设备现场(usb0=configfs-gadget.g1, wwan0=bam-dmux)。
REPO="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
LIB="$REPO/scripts/lib.sh"
[[ -f "$LIB" ]] || { echo "找不到 $LIB"; exit 1; }
PASS=0
FAIL=0
check_eq() {
local name="$1" want="$2" got="$3"
if [[ "$want" == "$got" ]]; then
PASS=$((PASS+1)); printf 'ok %s\n' "$name"
else
FAIL=$((FAIL+1)); printf 'FAIL %s (want=%s got=%s)\n' "$name" "$want" "$got"
fi
}
# scenario <ifaces> <drivers> <ips> <default_if> <mm_iface> <have_mmcli> <shell代码>
# 在子 shell 里 source lib.sh,替换所有触碰真实系统的探针,然后 eval 代码。
scenario() {
(
FAKE_IFACES="$1"
FAKE_DRIVERS="$2"
FAKE_IPS="$3"
FAKE_DEFAULT="$4"
FAKE_MM_IFACE="$5"
FAKE_HAVE_MMCLI="$6"
CODE="$7"
# shellcheck disable=SC1090
source "$LIB" >/dev/null 2>&1
set +eu
lookup() {
local table="$1" key="$2" pair
for pair in $table; do
if [[ "${pair%%=*}" == "$key" ]]; then printf '%s' "${pair#*=}"; return 0; fi
done
return 1
}
iface_exists() {
local i
for i in $FAKE_IFACES; do [[ "$i" == "$1" ]] && return 0; done
return 1
}
iface_driver() { lookup "$FAKE_DRIVERS" "$1" || true; }
iface_has_carrier() { iface_exists "$1"; }
detect_default_iface() { printf '%s' "$FAKE_DEFAULT"; }
list_ifaces() {
local i ip4
for i in $FAKE_IFACES; do
ip4="$(lookup "$FAKE_IPS" "$i" || true)"
printf '%s|up|%s\n' "$i" "$ip4"
done
}
# 只需支持 ip -4 -o addr show dev Xip route show default 一律返回空
ip() {
local a prev="" dev="" v
for a in "$@"; do
[[ "$prev" == "dev" ]] && dev="$a"
prev="$a"
done
[[ -n "$dev" ]] || return 0
v="$(lookup "$FAKE_IPS" "$dev" || true)"
[[ -n "$v" ]] && printf '1: %s inet %s scope global %s\n' "$dev" "$v" "$dev"
return 0
}
if [[ "$FAKE_HAVE_MMCLI" == "yes" ]]; then
mmcli() {
case "$1" in
-L) echo " /org/freedesktop/ModemManager1/Modem/0 [QUECTEL] EC20" ;;
-m) echo " Bearer | paths: /org/freedesktop/ModemManager1/Bearer/0" ;;
-b) printf ' IPv4 configuration | interface: %s\n' "$FAKE_MM_IFACE" ;;
esac
return 0
}
else
command() {
if [[ "$1" == "-v" && "$2" == "mmcli" ]]; then return 1; fi
builtin command "$@"
}
fi
eval "$CODE"
) 2>/dev/null
}
IF_REAL="usb0 wlan0 wwan0"
DRV_REAL="usb0=configfs-gadget.g1 wwan0=bam-dmux wlan0=brcmfmac"
IPS_REAL="usb0=192.168.68.1/24 wwan0=10.2.8.250/30 wlan0=192.168.1.23/24"
real() { scenario "$IF_REAL" "$DRV_REAL" "$IPS_REAL" wlan0 "${2:-}" "${3:-no}" "$1"; }
check_eq "usb0 识别为 USB gadget" gadget \
"$(real 'iface_is_usb_gadget usb0 && echo gadget || echo no')"
check_eq "wwan0 不被误判为 gadget" no \
"$(real 'iface_is_usb_gadget wwan0 && echo gadget || echo no')"
check_eq "usb0 进入 skip 列表" skip \
"$(real 'is_virtual_or_skip_iface usb0 && echo skip || echo keep')"
check_eq "bam-dmux 命中蜂窝驱动白名单" cell \
"$(real 'iface_looks_cellular_by_driver wwan0 && echo cell || echo no')"
check_eq "gadget 驱动不算蜂窝" no \
"$(real 'iface_looks_cellular_by_driver usb0 && echo cell || echo no')"
check_eq "usb0 评分为 0" 0 \
"$(real 'score_iface_as_cellular usb0 wlan0')"
check_eq "wwan0 评分达到置信阈值" high \
"$(real 's=$(score_iface_as_cellular wwan0 wlan0); [[ $s -ge 70 ]] && echo high || echo low:$s')"
check_eq "无 mmcli 时自动探测得 wwan0" wwan0 \
"$(real 'detect_cellular_iface ""')"
check_eq "配置写死 usb0 被纠回 wwan0" wwan0 \
"$(real 'detect_cellular_iface usb0')"
check_eq "旧 patterns 含 usb/enx 仍选 wwan0" wwan0 \
"$(real 'CELLULAR_IFACE_PATTERNS="wwan,usb,enx,cdc"; detect_cellular_iface ""')"
check_eq "ModemManager bearer 优先采用" wwan0 \
"$(real 'detect_cellular_iface ""' wwan0 yes)"
check_eq "ModemManager 报 gadget 时被忽略" wwan0 \
"$(real 'detect_cellular_iface ""' usb0 yes)"
check_eq "resolve_cellular 拦截 usb0" wwan0 \
"$(real 'CELLULAR_IFACE=usb0; REQUIRE_CELLULAR_IFACE=true; resolve_cellular')"
# 只有 gadget 可选时必须失败,而不是把 usb0 绑成出口
check_eq "仅 gadget 时 resolve_cellular 拒绝返回 usb0" die \
"$(scenario "usb0 wlan0" "usb0=configfs-gadget.g1 wlan0=brcmfmac" \
"usb0=192.168.68.1/24 wlan0=192.168.1.23/24" wlan0 "" no \
'CELLULAR_IFACE=usb0; REQUIRE_CELLULAR_IFACE=true; out=$(resolve_cellular 2>/dev/null); rc=$?; if [[ $rc -ne 0 || -z $out ]]; then echo die; else echo "$out"; fi')"
# verify.sh 的前置体检:出口绑在 gadget 上时必须 exit 4,而不是跑一堆 curl 再给含糊 FAIL
verify_guard() {
local sandbox conf rc
sandbox="$(mktemp -d)"
cp -r "$REPO"/. "$sandbox/" 2>/dev/null
rm -rf "$sandbox/.git"
mkdir -p "$sandbox/config" "$sandbox/etc"
cat >"$sandbox/config/settings.conf" <<EOF
INSTALL_DIR=$sandbox
CELLULAR_IFACE=$1
REQUIRE_CELLULAR_IFACE=false
EOF
# 让 usb0 在这个沙箱里被判定为 gadget,且不真的去打网络
cat >>"$sandbox/scripts/lib.sh" <<'EOF'
iface_is_usb_gadget() { [[ "$1" == usb0 || "$1" == rndis0 ]]; }
EOF
( cd "$sandbox" && CONFIG_FILE="$sandbox/config/settings.conf" \
EGRESS_CHECK_URL="http://127.0.0.1:1/" EGRESS_TIMEOUT=1 VERIFY_QUICK=true \
bash scripts/verify.sh >/dev/null 2>&1 )
rc=$?
rm -rf "$sandbox"
printf '%s' "$rc"
}
check_eq "verify.sh 对 gadget 出口直接 exit 4" 4 "$(verify_guard usb0)"
check_eq "verify.sh 对正常出口不报 exit 4" not4 \
"$(rc="$(verify_guard wwan0)"; [[ "$rc" == 4 ]] && echo 4 || echo not4)"
# upgrade.sh 的迁移函数:旧 settings.conf 里的 usb/enx 必须被就地剔除
migrate_case() {
local conf fn out
conf="$(mktemp)"
fn="$(mktemp)"
printf 'CELLULAR_IFACE=wwan0\nCELLULAR_IFACE_PATTERNS=%s\nPROXY_MIXED_PORT=7890\n' "$1" >"$conf"
# 从 upgrade.sh 里抽出 migrate_iface_patterns 函数体,避免执行整个升级脚本
awk '/^migrate_iface_patterns\(\) \{/,/^\}/' "$REPO/scripts/upgrade.sh" >"$fn"
out="$(
set +eu
warn() { :; }
# shellcheck disable=SC1090
source "$fn"
migrate_iface_patterns "$conf" >/dev/null 2>&1
awk -F= '/^CELLULAR_IFACE_PATTERNS=/{print substr($0,index($0,"=")+1)}' "$conf"
# 顺带确认其它行没被吃掉
grep -c '^PROXY_MIXED_PORT=7890$' "$conf"
)"
rm -f "$conf" "$fn"
printf '%s' "$(printf '%s' "$out" | tr '\n' '/')"
}
check_eq "升级迁移剔除 usb/enx 且保留其它配置" "wwan,wwp,ppp,cdc/1" \
"$(migrate_case 'wwan,wwp,usb,enx,ppp,cdc')"
check_eq "已干净的 patterns 不被改动" "wwan,wwp,ppp,cdc/1" \
"$(migrate_case 'wwan,wwp,ppp,cdc')"
printf '\n通过 %d / 失败 %d\n' "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]