#!/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}" 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:-}" 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*) return 0 ;; esac 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}' } detect_cellular_iface() { local configured="$1" 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" ]]; then return 1 fi fi fi local patterns name state ip p cand="" IFS=',' read -r -a patterns <<< "$CELLULAR_IFACE_PATTERNS" while IFS='|' read -r name state ip; do [[ -z "$name" ]] && continue is_virtual_or_skip_iface "$name" && continue for p in "${patterns[@]}"; do p="${p// /}" [[ -z "$p" ]] && continue if [[ "$name" == *"$p"* ]]; then if [[ "$state" == "up" || "$state" == "unknown" ]] && [[ -n "$ip" ]]; then echo "$name" return 0 fi cand="${cand:-$name}" fi done done < <(list_ifaces) if [[ -n "$cand" ]]; then echo "$cand" 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 } 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" fi warn "未找到数据网卡" cell="" fi echo "$cell" }