Files
cellular-proxy/scripts/watch-cellular.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

194 lines
5.7 KiB
Bash
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
# 监控数据网卡: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