fix: 安装启动后不再卡住,保证脚本退出

- 去掉可能阻塞的 systemctl status 全量输出
- verify 使用 connect/max 超时;安装阶段 8s 快速验证
- 验证失败只告警,不阻塞安装结束
This commit is contained in:
Hermes
2026-07-21 11:26:30 +00:00
commit aec388682b
14 changed files with 1697 additions and 0 deletions
Executable
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env bash
# 公共库
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$ROOT_DIR/config/settings.conf}"
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }
info() { log "INFO $*"; }
warn() { log "WARN $*"; }
err() { log "ERROR $*"; }
die() { err "$*"; exit 1; }
need_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
die "请用 root 运行(sudo"
fi
}
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
die "缺少配置: $CONFIG_FILE
请先: cp $ROOT_DIR/config/settings.conf.example $ROOT_DIR/config/settings.conf"
fi
set -a
# shellcheck disable=SC1090
source "$CONFIG_FILE"
set +a
INSTALL_DIR="${INSTALL_DIR:-/opt/cellular-proxy}"
LOG_DIR="${LOG_DIR:-/var/log/cellular-proxy}"
CELLULAR_IFACE="${CELLULAR_IFACE:-}"
CELLULAR_SOURCE_IP="${CELLULAR_SOURCE_IP:-}"
REQUIRE_CELLULAR_IFACE="${REQUIRE_CELLULAR_IFACE:-true}"
CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}"
PROXY_LISTEN_HOST="${PROXY_LISTEN_HOST:-0.0.0.0}"
PROXY_MIXED_PORT="${PROXY_MIXED_PORT:-7890}"
PROXY_USER="${PROXY_USER:-}"
PROXY_PASS="${PROXY_PASS:-}"
PANEL_LISTEN_HOST="${PANEL_LISTEN_HOST:-0.0.0.0}"
PANEL_PORT="${PANEL_PORT:-9090}"
PANEL_SECRET="${PANEL_SECRET:-please-change-me}"
ENABLE_DNS="${ENABLE_DNS:-true}"
LOG_LEVEL="${LOG_LEVEL:-warn}"
SING_BOX_SOURCE="${SING_BOX_SOURCE:-release}"
SING_BOX_VERSION="${SING_BOX_VERSION:-1.11.7}"
SING_BOX_BIN="${SING_BOX_BIN:-}"
GITHUB_PROXY="${GITHUB_PROXY:-https://git.86482425.xyz}"
MEMORY_MAX_MB="${MEMORY_MAX_MB:-96}"
EGRESS_CHECK_URL="${EGRESS_CHECK_URL:-https://ifconfig.me}"
EXPECTED_CELLULAR_PUBLIC_IP="${EXPECTED_CELLULAR_PUBLIC_IP:-}"
for v in REQUIRE_CELLULAR_IFACE ENABLE_DNS; do
val="${!v}"
case "${val,,}" in
1|true|yes|on) printf -v "$v" '%s' true ;;
*) printf -v "$v" '%s' false ;;
esac
done
}
iface_exists() {
local ifc="$1"
[[ -n "$ifc" ]] && [[ -d "/sys/class/net/$ifc" ]]
}
is_virtual_or_skip_iface() {
local name="$1"
case "$name" in
lo|docker*|br-*|veth*|virbr*|cni*|flannel*|tun*|tap*|wg*|zt*|tailscale*|easy*|et*|nlmon*|dummy*|ifb*|bond*|team*|macvlan*|ipvlan*)
return 0
;;
esac
# bridge that is not a physical device
if [[ -d "/sys/class/net/$name/bridge" ]]; then
return 0
fi
return 1
}
list_ifaces() {
local name state ip
for name in $(ls /sys/class/net 2>/dev/null | sort); do
[[ "$name" == "lo" ]] && continue
state="$(cat "/sys/class/net/$name/operstate" 2>/dev/null || echo unknown)"
ip="$(ip -4 -o addr show dev "$name" 2>/dev/null | awk '{print $4}' | head -1)"
printf '%s|%s|%s\n' "$name" "$state" "${ip:-}"
done
}
detect_default_iface() {
ip route show default 2>/dev/null | awk '/default/ {print $5; exit}'
}
iface_has_carrier() {
local ifc="$1" c
c="$(cat "/sys/class/net/$ifc/carrier" 2>/dev/null || echo 0)"
[[ "$c" == "1" ]]
}
iface_driver() {
local ifc="$1" link
link="$(readlink -f "/sys/class/net/$ifc/device/driver" 2>/dev/null || true)"
if [[ -n "$link" ]]; then
basename "$link"
return
fi
# some USB eth put driver under device
if [[ -f "/sys/class/net/$ifc/device/uevent" ]]; then
awk -F= '/^DRIVER=/{print $2; exit}' "/sys/class/net/$ifc/device/uevent" 2>/dev/null || true
fi
}
iface_looks_cellular_by_driver() {
local ifc="$1" drv
drv="$(iface_driver "$ifc" || true)"
case "${drv,,}" in
qmi_wwan|cdc_mbim|cdc_ncm|cdc_ether|cdc_wdm|option|huawei_cdc_ncm|rndis_host|GobiNet|GobiSerial|simcom*|rmnet*|mhi_net|ipa)
return 0
;;
esac
return 1
}
iface_matches_patterns() {
local name="$1" p
local patterns="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}"
IFS=',' read -r -a arr <<< "$patterns"
for p in "${arr[@]}"; do
p="${p// /}"
[[ -z "$p" ]] && continue
if [[ "$name" == *"$p"* ]]; then
return 0
fi
done
return 1
}
# 多默认路由时,metric 更大的往往是数据网(WiFi metric 更小优先)
detect_secondary_default_iface() {
local primary secondary
primary="$(detect_default_iface || true)"
secondary="$(
ip route show default 2>/dev/null | awk -v p="$primary" '
/default/ {
iface=""; metric=0
for (i=1;i<=NF;i++) {
if ($i=="dev") iface=$(i+1)
if ($i=="metric") metric=$(i+1)+0
}
if (iface!="" && iface!=p) {
print metric, iface
}
}
' | sort -n | awk 'END {print $2}'
)"
if [[ -n "$secondary" && "$secondary" != "$primary" ]]; then
echo "$secondary"
return 0
fi
return 1
}
# 打分选数据网卡(stdout 仅输出网卡名)
# 更高分优先
score_iface_as_cellular() {
local name="$1"
local default_if="$2"
local state ip score=0 drv
is_virtual_or_skip_iface "$name" && { echo 0; return; }
state="$(cat "/sys/class/net/$name/operstate" 2>/dev/null || echo unknown)"
ip="$(ip -4 -o addr show dev "$name" 2>/dev/null | awk '{print $4}' | head -1 | cut -d/ -f1)"
drv="$(iface_driver "$name" || true)"
# 名称
if iface_matches_patterns "$name"; then score=$((score + 80)); fi
# 驱动
if iface_looks_cellular_by_driver "$name"; then score=$((score + 100)); fi
# 非默认网卡(关键:系统默认走 WiFi)
if [[ -n "$default_if" && "$name" != "$default_if" ]]; then score=$((score + 40)); fi
# 有 IPv4
if [[ -n "$ip" ]]; then score=$((score + 30)); fi
# up/carrier
if [[ "$state" == "up" || "$state" == "unknown" ]]; then score=$((score + 15)); fi
if iface_has_carrier "$name"; then score=$((score + 10)); fi
# 默认网卡通常是 WiFi/有线,大幅降权
if [[ -n "$default_if" && "$name" == "$default_if" ]]; then score=$((score - 60)); fi
# docker 等已在 skip;再防 enp/eth 当默认时
case "$name" in
eth*|enp*|eno*|ens*|wlan*|wlp*|wl*)
if [[ "$name" == "$default_if" ]]; then score=$((score - 20)); fi
;;
esac
echo "$score"
}
# 自动探测数据网卡:配置 > 名称/驱动 > 次默认路由 > 非默认有 IP 物理口
detect_cellular_iface() {
local configured="${1:-}"
local default_if name state ip best_name="" best_score=0 score
if [[ -n "$configured" ]]; then
if [[ "$configured" == "lo" ]]; then
warn "CELLULAR_IFACE=lo 无效,将尝试自动探测"
elif iface_exists "$configured"; then
echo "$configured"
return 0
else
warn "配置的 CELLULAR_IFACE=$configured 不存在,将尝试自动探测"
if [[ "${REQUIRE_CELLULAR_IFACE:-true}" == "true" && -n "${FORCE_CONFIGURED_ONLY:-}" ]]; then
return 1
fi
fi
fi
default_if="$(detect_default_iface || true)"
# 1) 按评分扫描全部接口
while IFS='|' read -r name state ip; do
[[ -z "$name" ]] && continue
is_virtual_or_skip_iface "$name" && continue
score="$(score_iface_as_cellular "$name" "$default_if")"
if [[ "$score" -gt "$best_score" ]]; then
best_score="$score"
best_name="$name"
fi
done < <(list_ifaces)
# 需要足够置信度(避免把唯一的 WiFi 当数据)
if [[ -n "$best_name" && "$best_score" -ge 70 ]]; then
info "自动探测数据网卡: $best_name (score=$best_score, default=$default_if, driver=$(iface_driver "$best_name" || true))"
echo "$best_name"
return 0
fi
# 2) 多默认路由的次要口
if name="$(detect_secondary_default_iface 2>/dev/null || true)"; then
if [[ -n "$name" ]] && iface_exists "$name"; then
info "根据次要默认路由探测数据网卡: $name"
echo "$name"
return 0
fi
fi
# 3) 任意非默认、有 IPv4、非虚拟
while IFS='|' read -r name state ip; do
[[ -z "$name" ]] && continue
is_virtual_or_skip_iface "$name" && continue
[[ -z "$ip" ]] && continue
if [[ -n "$default_if" && "$name" == "$default_if" ]]; then
continue
fi
if [[ "$state" == "up" || "$state" == "unknown" ]]; then
info "回退选择非默认网卡: $name"
echo "$name"
return 0
fi
done < <(list_ifaces)
if [[ -n "$best_name" && "$best_score" -gt 0 ]]; then
warn "置信度较低,选用: $best_name (score=$best_score)"
echo "$best_name"
return 0
fi
return 1
}
detect_source_ip() {
local ifc="$1"
ip -4 -o addr show dev "$ifc" 2>/dev/null | awk '{print $4}' | head -1 | cut -d/ -f1
}
# 把探测结果写回 settings.conf
persist_cellular_to_settings() {
local conf="${1:-$CONFIG_FILE}"
local cell="$2"
local src_ip="${3:-}"
[[ -f "$conf" ]] || return 1
local tmp
tmp="$(mktemp)"
awk -v k="CELLULAR_IFACE" -v v="$cell" '
BEGIN { done=0 }
index($0, k "=")==1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "$conf" > "$tmp"
mv "$tmp" "$conf"
if [[ -n "$src_ip" ]]; then
tmp="$(mktemp)"
awk -v k="CELLULAR_SOURCE_IP" -v v="$src_ip" '
BEGIN { done=0 }
index($0, k "=")==1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "$conf" > "$tmp"
mv "$tmp" "$conf"
fi
}
arch_go() {
case "$(uname -m)" in
x86_64|amd64) echo amd64 ;;
aarch64|arm64) echo arm64 ;;
armv7l|armhf) echo armv7 ;;
*) die "不支持的架构: $(uname -m)" ;;
esac
}
ensure_dirs() {
mkdir -p "$INSTALL_DIR"/{bin,etc,ui,generated} "$LOG_DIR"
}
resolve_cellular() {
local cell
if ! cell="$(detect_cellular_iface "$CELLULAR_IFACE")"; then
if [[ "$REQUIRE_CELLULAR_IFACE" == "true" ]]; then
die "无法确定数据网卡。请设置 CELLULAR_IFACE= 或运行 scripts/detect.sh / cpxy detect"
fi
warn "未找到数据网卡"
cell=""
fi
echo "$cell"
}