设备把自己当 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 条离线断言,无需真机
125 lines
3.9 KiB
Bash
Executable File
125 lines
3.9 KiB
Bash
Executable File
#!/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"
|
||
|
||
if [[ -f "${CONFIG_FILE:-$ROOT_DIR/config/settings.conf}" ]]; then
|
||
load_config
|
||
elif [[ -f "$INSTALL_DIR/etc/settings.conf" ]]; then
|
||
CONFIG_FILE="$INSTALL_DIR/etc/settings.conf"
|
||
load_config
|
||
elif [[ -f "$ROOT_DIR/generated/runtime.env" ]]; then
|
||
set -a
|
||
# shellcheck disable=SC1091
|
||
source "$ROOT_DIR/generated/runtime.env"
|
||
set +a
|
||
else
|
||
# 尝试已安装路径
|
||
if [[ -f /opt/cellular-proxy/etc/settings.conf ]]; then
|
||
CONFIG_FILE=/opt/cellular-proxy/etc/settings.conf
|
||
load_config
|
||
else
|
||
load_config
|
||
fi
|
||
fi
|
||
|
||
URL="${EGRESS_CHECK_URL:-https://ifconfig.me}"
|
||
# 安装阶段可用 EGRESS_TIMEOUT=8 缩短;手动 verify 默认 12
|
||
TIMEOUT="${EGRESS_TIMEOUT:-12}"
|
||
HOST="$PROXY_LISTEN_HOST"
|
||
if [[ "$HOST" == "0.0.0.0" || "$HOST" == "::" || -z "$HOST" ]]; then
|
||
HOST="127.0.0.1"
|
||
fi
|
||
PORT="${PROXY_MIXED_PORT:-7890}"
|
||
QUICK="${VERIFY_QUICK:-false}"
|
||
case "${QUICK,,}" in
|
||
1|true|yes|on) QUICK=true ;;
|
||
*) QUICK=false ;;
|
||
esac
|
||
|
||
# 出口一旦绑成 USB gadget 网卡,所有代理请求都会超时(客户端看到 SOCKS rep=0x01)。
|
||
# 在打网络探测之前先点名,免得只剩一个含糊的 FAIL。
|
||
cfg_json="${INSTALL_DIR:-/opt/cellular-proxy}/etc/config.json"
|
||
cfg_bind=""
|
||
if command -v python3 >/dev/null 2>&1 && [[ -f "$cfg_json" ]]; then
|
||
cfg_bind="$(python3 -c '
|
||
import json, sys
|
||
try:
|
||
cfg = json.load(open(sys.argv[1]))
|
||
for o in cfg.get("outbounds") or []:
|
||
if o.get("tag") == "cellular":
|
||
print(o.get("bind_interface") or "")
|
||
break
|
||
except Exception:
|
||
pass
|
||
' "$cfg_json" 2>/dev/null || true)"
|
||
fi
|
||
for _ifc in "${CELLULAR_IFACE:-}" "$cfg_bind"; do
|
||
[[ -n "$_ifc" ]] || continue
|
||
if iface_is_usb_gadget "$_ifc"; then
|
||
err "出口绑在 USB gadget 网卡 $_ifc 上(那是本机 -> 上游主机的下行链路),代理必定超时"
|
||
err "修复: sudo cpxy rebind && sudo cpxy generate"
|
||
exit 4
|
||
fi
|
||
done
|
||
|
||
curl_ip() {
|
||
# 单次探测,严格超时,绝不无限挂起
|
||
local extra=("$@")
|
||
# --max-time 总时长;--connect-timeout 连接
|
||
curl -fsS --connect-timeout 3 --max-time "$TIMEOUT" "${extra[@]}" "$URL" 2>/dev/null \
|
||
| tr -d '\r\n' \
|
||
| head -c 64
|
||
}
|
||
|
||
if [[ "$QUICK" != "true" ]]; then
|
||
echo "========== 接口 =========="
|
||
"$ROOT_DIR/scripts/detect.sh" || true
|
||
echo
|
||
fi
|
||
|
||
echo "========== 默认出口(不走代理,通常是 WiFi) =========="
|
||
info "探测中(最多 ${TIMEOUT}s): $URL"
|
||
direct_ip="$(curl_ip || true)"
|
||
[[ -n "$direct_ip" ]] || direct_ip=FAIL
|
||
echo "direct: $direct_ip"
|
||
echo
|
||
|
||
echo "========== 代理出口(应是数据流量公网 IP) =========="
|
||
auth=()
|
||
if [[ -n "${PROXY_USER:-}" ]]; then
|
||
auth=(--proxy-user "${PROXY_USER}:${PROXY_PASS}")
|
||
fi
|
||
info "SOCKS 探测中(最多 ${TIMEOUT}s): ${HOST}:${PORT}"
|
||
socks_ip="$(curl_ip "${auth[@]}" --socks5-hostname "${HOST}:${PORT}" || true)"
|
||
[[ -n "$socks_ip" ]] || socks_ip=FAIL
|
||
echo "socks : $socks_ip"
|
||
|
||
info "HTTP 探测中(最多 ${TIMEOUT}s): ${HOST}:${PORT}"
|
||
http_ip="$(curl_ip "${auth[@]}" -x "http://${HOST}:${PORT}" || true)"
|
||
[[ -n "$http_ip" ]] || http_ip=FAIL
|
||
echo "http : $http_ip"
|
||
echo
|
||
|
||
if [[ "$direct_ip" == "FAIL" || "$socks_ip" == "FAIL" ]]; then
|
||
err "探测失败(服务/数据网是否在线? journalctl -u cellular-proxy -e)"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ "$direct_ip" == "$socks_ip" ]]; then
|
||
warn "代理与直连公网 IP 相同 — 可能未绑到数据网卡,或数据与 WiFi 同出口"
|
||
warn "请检查 CELLULAR_IFACE / 数据网是否 up / bind_interface"
|
||
exit 2
|
||
fi
|
||
|
||
if [[ -n "${EXPECTED_CELLULAR_PUBLIC_IP:-}" && "$socks_ip" != "$EXPECTED_CELLULAR_PUBLIC_IP" ]]; then
|
||
err "代理出口 $socks_ip 与 EXPECTED_CELLULAR_PUBLIC_IP=$EXPECTED_CELLULAR_PUBLIC_IP 不一致"
|
||
exit 3
|
||
fi
|
||
|
||
info "OK: 直连(WiFi)=$direct_ip 代理(数据)=$socks_ip"
|
||
echo
|
||
echo "局域网: curl --socks5-hostname <LAN-IP>:${PORT} $URL"
|
||
echo "面板: http://<LAN-IP>:${PANEL_PORT}/"
|