Files
cellular-proxy/scripts/generate.sh
T
Hermes Agent 06f941c519 feat: bind_interface-only by default + WWAN watch auto-rebind
- Default BIND_SOURCE_IP=false: do not pin inet4_bind_address (stale
  source IP after WWAN re-dial was the main rep=0x01 cause).
- Add scripts/watch-cellular.sh: ip monitor events + poll, regenerate
  when iface missing or pinned source IP drifts.
- systemd: cellular-proxy-watch.service + 2min timer oneshot fallback.
- cpxy watch {status|once|start|stop|logs}
- Install/upgrade enable watch when ENABLE_CELLULAR_WATCH=true.
2026-07-23 09:02:46 +00:00

182 lines
5.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
# 绑定策略(优先更好的实现,而不是只靠定时器修 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_srcsettings 仅作元数据)"
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"