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.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
# 监控数据网卡:IP/链路变化时自动 regenerate(避免 stale bind 或网卡改名导致 rep=0x01)
|
||||
# 策略:
|
||||
# 1) 默认配置只 bind_interface(不写死源 IP)—— 多数场景无需本脚本也能自愈
|
||||
# 2) 本脚本仍负责:iface 丢失→重探、可选 BIND_SOURCE_IP 时同步源 IP、链路 down/up 后拉起
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT_DIR/scripts/lib.sh"
|
||||
|
||||
CONFIG_FILE="${CONFIG_FILE:-$ROOT_DIR/etc/settings.conf}"
|
||||
if [[ ! -f "$CONFIG_FILE" && -f /opt/cellular-proxy/etc/settings.conf ]]; then
|
||||
CONFIG_FILE=/opt/cellular-proxy/etc/settings.conf
|
||||
ROOT_DIR=/opt/cellular-proxy
|
||||
fi
|
||||
export CONFIG_FILE
|
||||
|
||||
load_config
|
||||
|
||||
INTERVAL="${WATCH_INTERVAL_SEC:-60}"
|
||||
DEBOUNCE_SEC="${WATCH_DEBOUNCE_SEC:-3}"
|
||||
LOCK_DIR="${LOG_DIR:-/var/log/cellular-proxy}"
|
||||
mkdir -p "$LOCK_DIR" 2>/dev/null || true
|
||||
STATE_FILE="${LOCK_DIR}/watch.state"
|
||||
LOG_TAG="cellular-proxy-watch"
|
||||
|
||||
logw() { printf '[%s] %s %s\n' "$(date '+%F %T')" "$LOG_TAG" "$*" >&2; }
|
||||
|
||||
current_cfg_iface() {
|
||||
python3 - <<'PY' 2>/dev/null || true
|
||||
import json
|
||||
from pathlib import Path
|
||||
for p in ("/opt/cellular-proxy/etc/config.json",):
|
||||
try:
|
||||
c=json.loads(Path(p).read_text())
|
||||
for o in c.get("outbounds") or []:
|
||||
if o.get("tag")=="cellular":
|
||||
print(o.get("bind_interface") or "")
|
||||
raise SystemExit
|
||||
except Exception:
|
||||
pass
|
||||
print("")
|
||||
PY
|
||||
}
|
||||
|
||||
current_cfg_src() {
|
||||
python3 - <<'PY' 2>/dev/null || true
|
||||
import json
|
||||
from pathlib import Path
|
||||
try:
|
||||
c=json.loads(Path("/opt/cellular-proxy/etc/config.json").read_text())
|
||||
for o in c.get("outbounds") or []:
|
||||
if o.get("tag")=="cellular":
|
||||
print(o.get("inet4_bind_address") or "")
|
||||
raise SystemExit
|
||||
except Exception:
|
||||
pass
|
||||
print("")
|
||||
PY
|
||||
}
|
||||
|
||||
need_refresh() {
|
||||
load_config
|
||||
local cell="${CELLULAR_IFACE:-}"
|
||||
local live_src="" cfg_iface cfg_src
|
||||
cfg_iface="$(current_cfg_iface)"
|
||||
cfg_src="$(current_cfg_src)"
|
||||
|
||||
if [[ -z "$cell" ]] || ! iface_exists "$cell"; then
|
||||
logw "iface missing or empty (CELLULAR_IFACE=${cell:-<空>}) → rebind"
|
||||
return 0
|
||||
fi
|
||||
|
||||
live_src="$(detect_source_ip "$cell" || true)"
|
||||
if [[ -z "$live_src" ]]; then
|
||||
# 网卡在但无 IPv4:可能刚重拨;不立刻 die,等下一轮
|
||||
logw "wwan up but no IPv4 yet on $cell"
|
||||
# 若配置里仍绑旧源 IP,清掉/重生更安全
|
||||
if [[ -n "$cfg_src" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$cfg_iface" && "$cfg_iface" != "$cell" ]]; then
|
||||
logw "cfg iface $cfg_iface != settings $cell"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 仅当配置写了 inet4_bind_address 且与 live 不一致时才需要
|
||||
if [[ -n "$cfg_src" && "$cfg_src" != "$live_src" ]]; then
|
||||
logw "stale source IP cfg=$cfg_src live=$live_src"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# settings 里的 SOURCE_IP 过期也同步(即使 config 未 pin)
|
||||
if [[ -n "${CELLULAR_SOURCE_IP:-}" && "${CELLULAR_SOURCE_IP}" != "$live_src" ]]; then
|
||||
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$live_src" || true
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
do_refresh() {
|
||||
load_config
|
||||
local cell="${CELLULAR_IFACE:-}"
|
||||
local src=""
|
||||
|
||||
if [[ -z "$cell" ]] || ! iface_exists "$cell"; then
|
||||
cell="$(detect_cellular_iface "" || true)"
|
||||
if [[ -z "$cell" ]]; then
|
||||
logw "detect failed; skip"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
src="$(detect_source_ip "$cell" || true)"
|
||||
if [[ -z "$src" ]]; then
|
||||
logw "no IPv4 on $cell yet; skip generate"
|
||||
return 1
|
||||
fi
|
||||
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" || true
|
||||
logw "refresh: iface=$cell src=$src"
|
||||
# 走 cpxy generate(含 check + restart);失败则直接 scripts
|
||||
if command -v cpxy >/dev/null 2>&1; then
|
||||
cpxy generate >/dev/null 2>&1 || {
|
||||
# shellcheck source=/dev/null
|
||||
source "$ROOT_DIR/scripts/lib.sh"
|
||||
load_config
|
||||
"$ROOT_DIR/scripts/generate.sh" "$ROOT_DIR/generated"
|
||||
install -m 0644 "$ROOT_DIR/generated/config.json" "$ROOT_DIR/etc/config.json"
|
||||
install -m 0644 "$ROOT_DIR/generated/runtime.env" "$ROOT_DIR/etc/runtime.env" 2>/dev/null || true
|
||||
systemctl restart cellular-proxy 2>/dev/null || true
|
||||
}
|
||||
else
|
||||
"$ROOT_DIR/scripts/generate.sh" "$ROOT_DIR/generated"
|
||||
install -m 0644 "$ROOT_DIR/generated/config.json" "$ROOT_DIR/etc/config.json"
|
||||
systemctl restart cellular-proxy 2>/dev/null || true
|
||||
fi
|
||||
printf '%s iface=%s src=%s\n' "$(date -Iseconds)" "$cell" "$src" >"$STATE_FILE" 2>/dev/null || true
|
||||
logw "refresh done"
|
||||
}
|
||||
|
||||
# oneshot mode (systemd timer)
|
||||
if [[ "${1:-}" == "--once" ]]; then
|
||||
if need_refresh; then
|
||||
do_refresh || true
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# long-running: periodic + optional ip monitor events
|
||||
logw "start interval=${INTERVAL}s (debounce=${DEBOUNCE_SEC}s)"
|
||||
last_run=0
|
||||
trigger() {
|
||||
local now
|
||||
now="$(date +%s)"
|
||||
if (( now - last_run < DEBOUNCE_SEC )); then
|
||||
return 0
|
||||
fi
|
||||
last_run=$now
|
||||
if need_refresh; then
|
||||
do_refresh || true
|
||||
fi
|
||||
}
|
||||
|
||||
# initial
|
||||
trigger
|
||||
|
||||
# Prefer event-driven if available; always keep poll as safety net
|
||||
if command -v ip >/dev/null 2>&1; then
|
||||
(
|
||||
# address/link changes on any iface — cheap filter in shell
|
||||
ip -o monitor address link 2>/dev/null | while read -r line; do
|
||||
cell="${CELLULAR_IFACE:-}"
|
||||
# reload cell from settings occasionally
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
cell="$(awk -F= '/^CELLULAR_IFACE=/{print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -n "$cell" ]] && [[ "$line" == *"$cell"* || "$line" == *Deleted* || "$line" == *wwan* || "$line" == *usb* || "$line" == *enx* ]]; then
|
||||
sleep "$DEBOUNCE_SEC"
|
||||
trigger
|
||||
fi
|
||||
done
|
||||
) &
|
||||
mon_pid=$!
|
||||
trap 'kill $mon_pid 2>/dev/null || true; exit 0' TERM INT
|
||||
fi
|
||||
|
||||
while true; do
|
||||
sleep "$INTERVAL"
|
||||
trigger
|
||||
done
|
||||
Reference in New Issue
Block a user