- 已装机器默认自动走升级,保留 secret/网卡/账密 - 默认不重下 sing-box、不 rebind、不 verify - scripts/upgrade.sh + install.sh 入口 + README
331 lines
9.7 KiB
Bash
Executable File
331 lines
9.7 KiB
Bash
Executable File
#!/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:-auto}"
|
||
SING_BOX_VERSION="${SING_BOX_VERSION:-1.11.7}"
|
||
SING_BOX_BIN="${SING_BOX_BIN:-}"
|
||
GITEA_BASE="${GITEA_BASE:-https://gitea.chickliu.fun}"
|
||
GITEA_OWNER="${GITEA_OWNER:-Hermes}"
|
||
GITEA_REPO="${GITEA_REPO:-cellular-proxy}"
|
||
SING_BOX_RELEASE_TAG="${SING_BOX_RELEASE_TAG:-bin-v${SING_BOX_VERSION}}"
|
||
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"
|
||
}
|