设备把自己当 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 条离线断言,无需真机
195 lines
6.6 KiB
Bash
Executable File
195 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 生成 sing-box:代理流量全部 bind 数据网卡出口
|
||
set -euo pipefail
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
# shellcheck source=lib.sh
|
||
source "$ROOT_DIR/scripts/lib.sh"
|
||
load_config
|
||
|
||
OUT_DIR="${1:-$ROOT_DIR/generated}"
|
||
mkdir -p "$OUT_DIR"
|
||
|
||
cell="$(resolve_cellular 2>/dev/null || true)"
|
||
if [[ -z "$cell" ]]; then
|
||
resolve_cellular >/dev/null || true
|
||
fi
|
||
|
||
# 最后一道闸:绝不把 USB gadget 网卡(本机 -> 上游主机的 RNDIS 链路)写成代理出口。
|
||
# 一旦写错,所有出站连接都会绑到只通向 PC 的接口上,SOCKS 返回 rep=0x01。
|
||
if [[ -n "$cell" ]] && iface_is_usb_gadget "$cell"; then
|
||
warn "拒绝使用 USB gadget 网卡 $cell 作为数据出口,重新探测"
|
||
cell="$(detect_cellular_iface_via_mm 2>/dev/null || true)"
|
||
if [[ -z "$cell" ]]; then
|
||
die "无法确定数据网卡(唯一候选是 USB gadget 网卡)。请设置 CELLULAR_IFACE=wwanX"
|
||
fi
|
||
info "改用 $cell"
|
||
persist_cellular_to_settings "${CONFIG_FILE:-$ROOT_DIR/etc/settings.conf}" "$cell" "$(detect_source_ip "$cell" || true)" || true
|
||
CELLULAR_IFACE="$cell"
|
||
fi
|
||
|
||
# 绑定策略(优先更好的实现,而不是只靠定时器修 stale IP):
|
||
# 默认 BIND_SOURCE_IP=false → 只写 bind_interface,不写 inet4_bind_address。
|
||
# sing-box 1.11+ 在 wwan 重拨后仍能按网卡出口,无需锁定私网源 IP。
|
||
# 实测:去掉 inet4_bind_address 后 SOCKS 仍从数据出口出(公网 IP ≠ WiFi)。
|
||
# BIND_SOURCE_IP=true 时才 pin 源 IP(兼容特殊内核/多地址场景),并始终用 live IP。
|
||
live_src=""
|
||
if [[ -n "$cell" ]]; then
|
||
live_src="$(detect_source_ip "$cell" || true)"
|
||
fi
|
||
src_ip="${live_src:-${CELLULAR_SOURCE_IP:-}}"
|
||
BIND_SOURCE_IP="$(echo "${BIND_SOURCE_IP:-false}" | tr '[:upper:]' '[:lower:]')"
|
||
if [[ -n "$cell" && -n "$live_src" && "${CELLULAR_SOURCE_IP:-}" != "$live_src" ]]; then
|
||
info "源 IP 记录更新: ${CELLULAR_SOURCE_IP:-<空>} → $live_src(settings 仅作元数据)"
|
||
persist_cellular_to_settings "${CONFIG_FILE:-$ROOT_DIR/etc/settings.conf}" "$cell" "$live_src" || true
|
||
if [[ -f /opt/cellular-proxy/etc/settings.conf ]]; then
|
||
persist_cellular_to_settings /opt/cellular-proxy/etc/settings.conf "$cell" "$live_src" || true
|
||
fi
|
||
CELLULAR_SOURCE_IP="$live_src"
|
||
fi
|
||
# 默认不把源 IP 写进 sing-box,避免下次重拨 stale bind
|
||
GEN_SRC_FOR_CFG=""
|
||
if [[ "$BIND_SOURCE_IP" == "true" || "$BIND_SOURCE_IP" == "1" || "$BIND_SOURCE_IP" == "yes" ]]; then
|
||
GEN_SRC_FOR_CFG="$src_ip"
|
||
info "BIND_SOURCE_IP=true → pin inet4_bind_address=${GEN_SRC_FOR_CFG:-<空>}"
|
||
else
|
||
info "绑定模式: bind_interface only(不 pin 源 IP,重拨更稳)"
|
||
fi
|
||
|
||
if [[ -z "$cell" ]]; then
|
||
warn "CELLULAR_IFACE 为空,临时用 lo 生成配置(启动前务必修正)"
|
||
cell_for_cfg=lo
|
||
else
|
||
cell_for_cfg="$cell"
|
||
fi
|
||
|
||
if [[ "$PANEL_SECRET" == "please-change-me" || "$PANEL_SECRET" == "change-this-secret" ]]; then
|
||
warn "PANEL_SECRET 仍是默认值,请修改 settings.conf"
|
||
fi
|
||
if [[ "$PROXY_LISTEN_HOST" != "127.0.0.1" && "$PROXY_LISTEN_HOST" != "::1" && -z "$PROXY_USER" ]]; then
|
||
warn "代理监听 $PROXY_LISTEN_HOST 且未设置 PROXY_USER/PASS(局域网建议加鉴权)"
|
||
fi
|
||
|
||
export GEN_OUT="$OUT_DIR/config.json"
|
||
export GEN_CELL="$cell_for_cfg"
|
||
export GEN_SRC="${GEN_SRC_FOR_CFG:-}"
|
||
export GEN_PROXY_HOST="$PROXY_LISTEN_HOST"
|
||
export GEN_PROXY_PORT="$PROXY_MIXED_PORT"
|
||
export GEN_PROXY_USER="$PROXY_USER"
|
||
export GEN_PROXY_PASS="$PROXY_PASS"
|
||
export GEN_PANEL_HOST="$PANEL_LISTEN_HOST"
|
||
export GEN_PANEL_PORT="$PANEL_PORT"
|
||
export GEN_PANEL_SECRET="$PANEL_SECRET"
|
||
export GEN_DNS="$ENABLE_DNS"
|
||
export GEN_LOG="$LOG_LEVEL"
|
||
export GEN_UI_DIR="$INSTALL_DIR/ui"
|
||
|
||
python3 <<'PY'
|
||
import json, os
|
||
from pathlib import Path
|
||
|
||
cell = os.environ["GEN_CELL"]
|
||
src = os.environ.get("GEN_SRC") or ""
|
||
log_level = os.environ.get("GEN_LOG") or "warn"
|
||
ui_dir = os.environ.get("GEN_UI_DIR") or "/opt/cellular-proxy/ui"
|
||
|
||
users = []
|
||
u = os.environ.get("GEN_PROXY_USER") or ""
|
||
p = os.environ.get("GEN_PROXY_PASS") or ""
|
||
if u:
|
||
users.append({"username": u, "password": p})
|
||
|
||
inbound = {
|
||
"type": "mixed",
|
||
"tag": "mixed-in",
|
||
"listen": os.environ["GEN_PROXY_HOST"],
|
||
"listen_port": int(os.environ["GEN_PROXY_PORT"]),
|
||
}
|
||
if users:
|
||
inbound["users"] = users
|
||
|
||
# 唯一业务出口:强制绑定数据网卡(默认仅 bind_interface)
|
||
out_cell = {
|
||
"type": "direct",
|
||
"tag": "cellular",
|
||
"bind_interface": cell,
|
||
}
|
||
# 仅当 GEN_SRC 非空(BIND_SOURCE_IP=true)时 pin 源 IP
|
||
if src:
|
||
out_cell["inet4_bind_address"] = src
|
||
|
||
cfg = {
|
||
"log": {"level": log_level, "timestamp": True},
|
||
"inbounds": [inbound],
|
||
"outbounds": [
|
||
out_cell,
|
||
{"type": "block", "tag": "block"},
|
||
],
|
||
# 所有代理流量最终都走 cellular(数据)
|
||
"route": {
|
||
"rules": [],
|
||
"final": "cellular",
|
||
"auto_detect_interface": False,
|
||
},
|
||
"experimental": {
|
||
"clash_api": {
|
||
"external_controller": f"{os.environ['GEN_PANEL_HOST']}:{os.environ['GEN_PANEL_PORT']}",
|
||
"secret": os.environ["GEN_PANEL_SECRET"],
|
||
"default_mode": "rule",
|
||
"external_ui": ui_dir,
|
||
},
|
||
"cache_file": {
|
||
"enabled": True,
|
||
"path": "/var/lib/cellular-proxy/cache.db",
|
||
"store_fakeip": False,
|
||
},
|
||
},
|
||
}
|
||
|
||
if (os.environ.get("GEN_DNS") or "true").lower() == "true":
|
||
# DNS 也走数据出口,避免解析从 WiFi 出去
|
||
cfg["dns"] = {
|
||
"servers": [
|
||
{
|
||
"tag": "remote",
|
||
"address": "1.1.1.1",
|
||
"detour": "cellular",
|
||
},
|
||
],
|
||
"final": "remote",
|
||
"strategy": "ipv4_only",
|
||
}
|
||
|
||
Path(os.environ["GEN_OUT"]).write_text(
|
||
json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||
)
|
||
print("wrote", os.environ["GEN_OUT"])
|
||
PY
|
||
|
||
cat > "$OUT_DIR/runtime.env" <<EOF
|
||
INSTALL_DIR=${INSTALL_DIR}
|
||
LOG_DIR=${LOG_DIR}
|
||
CELLULAR_IFACE=${cell}
|
||
CELLULAR_SOURCE_IP=${src_ip}
|
||
PROXY_LISTEN_HOST=${PROXY_LISTEN_HOST}
|
||
PROXY_MIXED_PORT=${PROXY_MIXED_PORT}
|
||
PANEL_LISTEN_HOST=${PANEL_LISTEN_HOST}
|
||
PANEL_PORT=${PANEL_PORT}
|
||
EGRESS_CHECK_URL=${EGRESS_CHECK_URL}
|
||
EXPECTED_CELLULAR_PUBLIC_IP=${EXPECTED_CELLULAR_PUBLIC_IP}
|
||
MEMORY_MAX_MB=${MEMORY_MAX_MB}
|
||
REQUIRE_CELLULAR_IFACE=${REQUIRE_CELLULAR_IFACE}
|
||
EOF
|
||
|
||
cat > "$OUT_DIR/SUMMARY.txt" <<EOF
|
||
generated_at=$(date -Iseconds)
|
||
语义=走代理的连接一律从数据网卡出口
|
||
cellular_iface=${cell}
|
||
cellular_source_ip=${src_ip}
|
||
proxy=${PROXY_LISTEN_HOST}:${PROXY_MIXED_PORT} (HTTP+SOCKS mixed)
|
||
panel=http://<LAN-IP>:${PANEL_PORT}/ secret=见 settings PANEL_SECRET
|
||
memory_max_mb=${MEMORY_MAX_MB}
|
||
EOF
|
||
|
||
info "生成完成: $OUT_DIR"
|
||
cat "$OUT_DIR/SUMMARY.txt"
|