- 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.
194 lines
5.7 KiB
Bash
194 lines
5.7 KiB
Bash
#!/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
|