设备把自己当 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 条离线断言,无需真机
188 lines
6.9 KiB
Bash
Executable File
188 lines
6.9 KiB
Bash
Executable File
#!/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 X;ip 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 ]]
|