feat: cellular-proxy 一键安装 — 走代理一律数据出口

- sing-box 单进程 bind 数据网卡
- mixed HTTP+SOCKS + 中文静态面板
- install.sh 支持 curl | sudo bash
- 512MB 友好 MemoryMax 默认 96MB
This commit is contained in:
Hermes
2026-07-21 10:56:27 +00:00
commit 8835646d34
14 changed files with 1263 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
#!/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 "$ROOT_DIR/config/settings.conf" ]]; then
load_config
else
CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc}"
REQUIRE_CELLULAR_IFACE=false
CELLULAR_IFACE=
fi
echo "========== 主机 =========="
echo "hostname : $(hostname)"
echo "kernel : $(uname -r)"
echo "arch : $(uname -m)"
echo "mem_total: $(awk '/MemTotal/ {printf "%.0f MB", $2/1024}' /proc/meminfo 2>/dev/null || echo unknown)"
echo "mem_avail: $(awk '/MemAvailable/ {printf "%.0f MB", $2/1024}' /proc/meminfo 2>/dev/null || echo unknown)"
echo
echo "========== 接口列表 =========="
printf '%-12s %-10s %s\n' "IFACE" "STATE" "IPv4"
while IFS='|' read -r name state ip; do
printf '%-12s %-10s %s\n' "$name" "$state" "${ip:-}"
done < <(list_ifaces)
echo
echo "========== 默认路由 =========="
ip route show default 2>/dev/null || true
echo
echo "default iface: $(detect_default_iface || echo none)"
echo
echo "========== 数据网卡(代理出口) =========="
cell="$(detect_cellular_iface "${CELLULAR_IFACE:-}" 2>/dev/null || true)"
if [[ -n "$cell" ]]; then
echo "CELLULAR_IFACE=$cell ip=$(detect_source_ip "$cell" || true)"
echo "说明: 走本代理的流量将从该网卡出(数据流量)"
else
echo "未能自动探测数据网卡。请手动设置 CELLULAR_IFACE="
echo "可参考上面接口列表中的 wwan*/usb*/enx* 等。"
exit 2
fi
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# 仅下载 sing-box(轻量核心)
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=lib.sh
source "$ROOT_DIR/scripts/lib.sh"
load_config
need_root
ensure_dirs
mkdir -p /var/lib/cellular-proxy
ARCH="$(arch_go)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
download() {
local url="$1" dest="$2"
info "下载: $url"
if command -v curl >/dev/null 2>&1; then
curl -fL --retry 3 --connect-timeout 20 -o "$dest" "$url"
elif command -v wget >/dev/null 2>&1; then
wget -O "$dest" "$url"
else
die "需要 curl 或 wget"
fi
}
if [[ "$SING_BOX_SOURCE" == "local" ]]; then
[[ -n "$SING_BOX_BIN" && -x "$SING_BOX_BIN" ]] || die "SING_BOX_SOURCE=local 但 SING_BOX_BIN 无效"
install -m 0755 "$SING_BOX_BIN" "$INSTALL_DIR/bin/sing-box"
info "已安装本地 sing-box"
elif command -v sing-box >/dev/null 2>&1 && [[ -z "${FORCE_DOWNLOAD:-}" ]]; then
install -m 0755 "$(command -v sing-box)" "$INSTALL_DIR/bin/sing-box"
info "复用系统 sing-box: $(command -v sing-box)"
else
ver="$SING_BOX_VERSION"
name="sing-box-${ver}-linux-${ARCH}"
url="https://github.com/SagerNet/sing-box/releases/download/v${ver}/${name}.tar.gz"
download "$url" "$TMP/sb.tgz"
tar -xzf "$TMP/sb.tgz" -C "$TMP"
install -m 0755 "$TMP/${name}/sing-box" "$INSTALL_DIR/bin/sing-box"
info "已安装 sing-box v${ver}"
fi
# 安装中文静态 UI
mkdir -p "$INSTALL_DIR/ui"
if [[ -f "$ROOT_DIR/ui/index.html" ]]; then
cp -a "$ROOT_DIR/ui/." "$INSTALL_DIR/ui/"
info "已安装内置中文面板 -> $INSTALL_DIR/ui"
else
warn "未找到 ui/index.html"
fi
info "二进制就绪:"
ls -la "$INSTALL_DIR/bin"
du -sh "$INSTALL_DIR/bin" "$INSTALL_DIR/ui" 2>/dev/null || true
+156
View File
@@ -0,0 +1,156 @@
#!/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
src_ip="${CELLULAR_SOURCE_IP:-}"
if [[ -z "$src_ip" && -n "$cell" ]]; then
src_ip="$(detect_source_ip "$cell" || true)"
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="$src_ip"
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
# 唯一业务出口:强制绑定数据网卡
out_cell = {
"type": "direct",
"tag": "cellular",
"bind_interface": cell,
}
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"
+96
View File
@@ -0,0 +1,96 @@
#!/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 "$ROOT_DIR/config/settings.conf" ]]; then
cp "$ROOT_DIR/config/settings.conf.example" "$ROOT_DIR/config/settings.conf"
warn "已创建 config/settings.conf,请至少修改:"
warn " CELLULAR_IFACE / PANEL_SECRET /(建议)PROXY_USER+PROXY_PASS"
warn "然后重新执行: sudo $ROOT_DIR/scripts/install.sh"
warn "可先: $ROOT_DIR/scripts/detect.sh"
exit 1
fi
load_config
need_root
info "1/5 探测数据网卡"
"$ROOT_DIR/scripts/detect.sh" || true
info "2/5 下载/安装 sing-box + UI"
"$ROOT_DIR/scripts/fetch-binaries.sh"
info "3/5 生成配置(代理出口 = 数据网卡)"
"$ROOT_DIR/scripts/generate.sh" "$ROOT_DIR/generated"
ensure_dirs
mkdir -p /var/lib/cellular-proxy
install -m 0644 "$ROOT_DIR/generated/config.json" "$INSTALL_DIR/etc/config.json"
install -m 0644 "$ROOT_DIR/generated/runtime.env" "$INSTALL_DIR/etc/runtime.env"
install -m 0644 "$ROOT_DIR/config/settings.conf" "$INSTALL_DIR/etc/settings.conf"
cp -a "$ROOT_DIR/scripts" "$INSTALL_DIR/"
cp -a "$ROOT_DIR/ui" "$INSTALL_DIR/" 2>/dev/null || true
if ! "$INSTALL_DIR/bin/sing-box" check -c "$INSTALL_DIR/etc/config.json"; then
die "sing-box 配置校验失败"
fi
install -m 0755 /dev/stdin "$INSTALL_DIR/bin/cpxy" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
BASE="$(cd "$(dirname "$0")/.." && pwd)"
export CONFIG_FILE="${CONFIG_FILE:-$BASE/etc/settings.conf}"
cmd="${1:-help}"
shift || true
case "$cmd" in
detect) exec "$BASE/scripts/detect.sh" "$@" ;;
generate)
"$BASE/scripts/generate.sh" "$BASE/generated"
install -m 0644 "$BASE/generated/config.json" "$BASE/etc/config.json"
install -m 0644 "$BASE/generated/runtime.env" "$BASE/etc/runtime.env"
install -m 0644 "$CONFIG_FILE" "$BASE/etc/settings.conf" 2>/dev/null || true
"$BASE/bin/sing-box" check -c "$BASE/etc/config.json"
systemctl restart cellular-proxy 2>/dev/null || true
;;
start) systemctl start cellular-proxy ;;
stop) systemctl stop cellular-proxy ;;
restart) systemctl restart cellular-proxy ;;
status) systemctl status cellular-proxy --no-pager || true ;;
verify) exec "$BASE/scripts/verify.sh" "$@" ;;
logs) journalctl -u cellular-proxy -n "${1:-80}" -f ;;
help|*)
cat <<H
cpxy — cellular-proxy
语义: 走代理的连接一律从数据网卡出口
cpxy detect | generate | start | stop | restart | status
cpxy verify | logs [N]
H
;;
esac
EOF
ln -sfn "$INSTALL_DIR/bin/cpxy" /usr/local/bin/cpxy
info "4/5 安装 systemd"
MEM="${MEMORY_MAX_MB:-96}"
install -m 0644 "$ROOT_DIR/systemd/cellular-proxy.service" /etc/systemd/system/cellular-proxy.service
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy.service
sed -i "s|@LOG_DIR@|$LOG_DIR|g" /etc/systemd/system/cellular-proxy.service
if [[ "$MEM" != "0" && -n "$MEM" ]]; then
sed -i "s|@MEMORY_MAX@|${MEM}M|g" /etc/systemd/system/cellular-proxy.service
else
sed -i '/MemoryMax=@MEMORY_MAX@/d' /etc/systemd/system/cellular-proxy.service
fi
systemctl daemon-reload
info "5/5 启动"
systemctl enable cellular-proxy.service
systemctl restart cellular-proxy.service
sleep 1
systemctl --no-pager --full status cellular-proxy.service || true
echo
info "安装完成"
echo " 语义: 不走代理 → 系统默认(WiFi);走代理 → 数据流量"
echo " 代理: HTTP/SOCKS ${PROXY_LISTEN_HOST}:${PROXY_MIXED_PORT}"
echo " 面板: http://<LAN-IP>:${PANEL_PORT}/ 密钥: PANEL_SECRET"
echo " 验证: cpxy verify"
Executable
+159
View File
@@ -0,0 +1,159 @@
#!/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"
}
+64
View File
@@ -0,0 +1,64 @@
#!/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 "$ROOT_DIR/generated/runtime.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$ROOT_DIR/generated/runtime.env"
set +a
else
load_config
fi
URL="${EGRESS_CHECK_URL:-https://ifconfig.me}"
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}"
echo "========== 接口 =========="
"$ROOT_DIR/scripts/detect.sh" || true
echo
echo "========== 默认出口(不走代理,通常是 WiFi =========="
direct_ip="$(curl -fsS --max-time 15 "$URL" || echo FAIL)"
echo "direct: $direct_ip"
echo
echo "========== 代理出口(应是数据流量公网 IP =========="
auth=()
if [[ -n "${PROXY_USER:-}" ]]; then
auth=(--proxy-user "${PROXY_USER}:${PROXY_PASS}")
fi
socks_ip="$(curl -fsS --max-time 20 "${auth[@]}" --socks5-hostname "${HOST}:${PORT}" "$URL" || echo FAIL)"
echo "socks : $socks_ip"
http_ip="$(curl -fsS --max-time 20 "${auth[@]}" -x "http://${HOST}:${PORT}" "$URL" || echo 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}/"