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:
Hermes Agent
2026-07-23 09:02:46 +00:00
parent 062a3eee90
commit 06f941c519
10 changed files with 381 additions and 7 deletions
+14
View File
@@ -72,8 +72,22 @@ cpxy rebind
cpxy status | logs cpxy status | logs
cpxy auth --user u --pass p cpxy auth --user u --pass p
cpxy auth --show | --clear cpxy auth --show | --clear
cpxy watch status # WWAN 自动监控
cpxy watch once # 手动跑一轮检查
``` ```
### 重拨 / `rep=0x01` 自愈
| 层级 | 做法 |
|------|------|
| **绑定(推荐)** | 默认 `BIND_SOURCE_IP=false`:只 `bind_interface=wwanX`**不 pin 私网源 IP**,重拨换地址通常不用 regenerate |
| **监控** | `cellular-proxy-watch``ip monitor` 事件 + 60s 轮询;网卡改名/掉线/仍 pin 了 stale IP 时自动 `generate` |
| **兜底 timer** | 每 2 分钟 oneshot 再检查一次 |
| **手动** | `cpxy rebind && cpxy verify` |
关监控:`ENABLE_CELLULAR_WATCH=false``cpxy upgrade`,或 `cpxy watch stop`
若必须 pin 源 IP`BIND_SOURCE_IP=true`(依赖 watch 同步,否则会再次 stale)。
- 代理:`LAN-IP:7890`HTTP + SOCKS5 - 代理:`LAN-IP:7890`HTTP + SOCKS5
- 面板:`http://LAN-IP:9090/ui/`(密钥在安装结束输出 / `settings.conf` - 面板:`http://LAN-IP:9090/ui/`(密钥在安装结束输出 / `settings.conf`
- **UI 可直接配置代理账号密码**(管理 API 默认 `9091`,与面板 secret 相同) - **UI 可直接配置代理账号密码**(管理 API 默认 `9091`,与面板 secret 相同)
+9 -1
View File
@@ -17,12 +17,20 @@ LOG_DIR=/var/log/cellular-proxy
# ---- 数据网卡(代理出口强制绑定)---- # ---- 数据网卡(代理出口强制绑定)----
# 留空则自动探测 wwan/usb/enx 等;建议手动填准 # 留空则自动探测 wwan/usb/enx 等;建议手动填准
CELLULAR_IFACE= CELLULAR_IFACE=
# 可选:数据侧源 IPv4一般留空 # 元数据:数据侧当前源 IPv4generate/watch 会更新;默认不写入 sing-box
CELLULAR_SOURCE_IP= CELLULAR_SOURCE_IP=
# 是否把源 IP pin 进 sing-box inet4_bind_address
# false(默认,推荐):只 bind_interface=网卡名 → WWAN 重拨换私网 IP 时通常无需 regenerate
# true:额外写 inet4_bind_address(需 watch/rebind 同步,否则易 stale → SOCKS rep=0x01
BIND_SOURCE_IP=false
# 数据网卡不存在则不启动(推荐 true,避免代理静默从 WiFi 出) # 数据网卡不存在则不启动(推荐 true,避免代理静默从 WiFi 出)
REQUIRE_CELLULAR_IFACE=true REQUIRE_CELLULAR_IFACE=true
# 自动探测关键词(逗号分隔) # 自动探测关键词(逗号分隔)
CELLULAR_IFACE_PATTERNS=wwan,wwp,usb,enx,ppp,cdc CELLULAR_IFACE_PATTERNS=wwan,wwp,usb,enx,ppp,cdc
# 后台监控(推荐开):网卡改名/掉线/无 IPv4 时自动 rebind;默认开启 long-running watch
ENABLE_CELLULAR_WATCH=true
# watch 轮询兜底间隔秒(事件驱动 ip monitor 为主)
WATCH_INTERVAL_SEC=60
# ---- 代理监听(局域网其它设备用 0.0.0.0)---- # ---- 代理监听(局域网其它设备用 0.0.0.0)----
# mixed = 同一端口同时支持 HTTP 代理 与 SOCKS5 # mixed = 同一端口同时支持 HTTP 代理 与 SOCKS5
+18 -6
View File
@@ -14,22 +14,33 @@ if [[ -z "$cell" ]]; then
resolve_cellular >/dev/null || true resolve_cellular >/dev/null || true
fi fi
# 始终以当前网卡实 IP 为准:WWAN 重拨后 iface 名常不变,但 CELLULAR_SOURCE_IP 会过期。 # 绑定策略(优先更好的实现,而不是只靠定时器修 stale IP):
# 过期的 inet4_bind_address 会导致 sing-box 出站失败 → 客户端 SOCKS rep=0x01 # 默认 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="" live_src=""
if [[ -n "$cell" ]]; then if [[ -n "$cell" ]]; then
live_src="$(detect_source_ip "$cell" || true)" live_src="$(detect_source_ip "$cell" || true)"
fi fi
src_ip="${live_src:-${CELLULAR_SOURCE_IP:-}}" 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 if [[ -n "$cell" && -n "$live_src" && "${CELLULAR_SOURCE_IP:-}" != "$live_src" ]]; then
info "源 IP 已漂移: ${CELLULAR_SOURCE_IP:-<空>}$live_src,写回 settings" info "源 IP 记录更新: ${CELLULAR_SOURCE_IP:-<空>}$live_srcsettings 仅作元数据)"
persist_cellular_to_settings "${CONFIG_FILE:-$ROOT_DIR/etc/settings.conf}" "$cell" "$live_src" || true persist_cellular_to_settings "${CONFIG_FILE:-$ROOT_DIR/etc/settings.conf}" "$cell" "$live_src" || true
# 兼容 live 安装布局:settings 在 etc/
if [[ -f /opt/cellular-proxy/etc/settings.conf ]]; then if [[ -f /opt/cellular-proxy/etc/settings.conf ]]; then
persist_cellular_to_settings /opt/cellular-proxy/etc/settings.conf "$cell" "$live_src" || true persist_cellular_to_settings /opt/cellular-proxy/etc/settings.conf "$cell" "$live_src" || true
fi fi
CELLULAR_SOURCE_IP="$live_src" CELLULAR_SOURCE_IP="$live_src"
fi 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 if [[ -z "$cell" ]]; then
warn "CELLULAR_IFACE 为空,临时用 lo 生成配置(启动前务必修正)" warn "CELLULAR_IFACE 为空,临时用 lo 生成配置(启动前务必修正)"
@@ -47,7 +58,7 @@ fi
export GEN_OUT="$OUT_DIR/config.json" export GEN_OUT="$OUT_DIR/config.json"
export GEN_CELL="$cell_for_cfg" export GEN_CELL="$cell_for_cfg"
export GEN_SRC="$src_ip" export GEN_SRC="${GEN_SRC_FOR_CFG:-}"
export GEN_PROXY_HOST="$PROXY_LISTEN_HOST" export GEN_PROXY_HOST="$PROXY_LISTEN_HOST"
export GEN_PROXY_PORT="$PROXY_MIXED_PORT" export GEN_PROXY_PORT="$PROXY_MIXED_PORT"
export GEN_PROXY_USER="$PROXY_USER" export GEN_PROXY_USER="$PROXY_USER"
@@ -83,12 +94,13 @@ inbound = {
if users: if users:
inbound["users"] = users inbound["users"] = users
# 唯一业务出口:强制绑定数据网卡 # 唯一业务出口:强制绑定数据网卡(默认仅 bind_interface
out_cell = { out_cell = {
"type": "direct", "type": "direct",
"tag": "cellular", "tag": "cellular",
"bind_interface": cell, "bind_interface": cell,
} }
# 仅当 GEN_SRC 非空(BIND_SOURCE_IP=true)时 pin 源 IP
if src: if src:
out_cell["inet4_bind_address"] = src out_cell["inet4_bind_address"] = src
+46
View File
@@ -160,6 +160,23 @@ case "$cmd" in
auth) auth)
exec "$BASE/scripts/apply-proxy-auth.sh" "$@" exec "$BASE/scripts/apply-proxy-auth.sh" "$@"
;; ;;
watch)
case "${1:-status}" in
once) exec "$BASE/scripts/watch-cellular.sh" --once ;;
status)
systemctl status cellular-proxy-watch.service --no-pager 2>/dev/null || true
systemctl status cellular-proxy-watch.timer --no-pager 2>/dev/null || true
;;
start|enable)
systemctl enable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
;;
stop|disable)
systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
;;
logs) journalctl -u cellular-proxy-watch -n "${2:-80}" --no-pager ;;
*) exec "$BASE/scripts/watch-cellular.sh" "$@" ;;
esac
;;
upgrade) upgrade)
# 一条命令在线增量升级(保留密钥/网卡) # 一条命令在线增量升级(保留密钥/网卡)
if [[ -x "$BASE/scripts/online-upgrade.sh" ]]; then if [[ -x "$BASE/scripts/online-upgrade.sh" ]]; then
@@ -178,6 +195,7 @@ cpxy — cellular-proxy
cpxy verify | logs [N] cpxy verify | logs [N]
cpxy auth --user U --pass P # 设置 7890 代理账号密码 cpxy auth --user U --pass P # 设置 7890 代理账号密码
cpxy auth --clear | --show cpxy auth --clear | --show
cpxy watch [status|once|start|stop|logs] # WWAN 自动监控
cpxy upgrade # 一条命令在线增量升级 cpxy upgrade # 一条命令在线增量升级
cpxy upgrade --ui-only # 只更新面板 cpxy upgrade --ui-only # 只更新面板
cpxy upgrade --force-binary # 强制重下 sing-box cpxy upgrade --force-binary # 强制重下 sing-box
@@ -204,6 +222,19 @@ if [[ -f "$ROOT_DIR/systemd/cellular-proxy-admin.service" ]]; then
install -m 0644 "$ROOT_DIR/systemd/cellular-proxy-admin.service" /etc/systemd/system/cellular-proxy-admin.service install -m 0644 "$ROOT_DIR/systemd/cellular-proxy-admin.service" /etc/systemd/system/cellular-proxy-admin.service
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy-admin.service sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy-admin.service
fi fi
# WWAN 自动监控:事件驱动 + 定时兜底
install_watch_units() {
local u
for u in cellular-proxy-watch.service cellular-proxy-watch-once.service cellular-proxy-watch.timer; do
if [[ -f "$ROOT_DIR/systemd/$u" ]]; then
install -m 0644 "$ROOT_DIR/systemd/$u" "/etc/systemd/system/$u"
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" "/etc/systemd/system/$u"
sed -i "s|@LOG_DIR@|${LOG_DIR}|g" "/etc/systemd/system/$u"
fi
done
chmod +x "$INSTALL_DIR/scripts/watch-cellular.sh" 2>/dev/null || true
}
install_watch_units
systemctl daemon-reload systemctl daemon-reload
info "5/6 启动服务" info "5/6 启动服务"
@@ -239,6 +270,21 @@ else
fi fi
fi fi
# 默认开启 watchENABLE_CELLULAR_WATCH!=false
watch_flag="$(echo "${ENABLE_CELLULAR_WATCH:-true}" | tr '[:upper:]' '[:lower:]')"
if [[ "$watch_flag" == "false" || "$watch_flag" == "0" || "$watch_flag" == "no" ]]; then
systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
info "ENABLE_CELLULAR_WATCH=false,未启用自动监控"
else
systemctl enable cellular-proxy-watch.service 2>/dev/null || true
systemctl enable cellular-proxy-watch.timer 2>/dev/null || true
if [[ "$SKIP_START" != "true" ]]; then
systemctl restart cellular-proxy-watch.service 2>/dev/null || true
systemctl restart cellular-proxy-watch.timer 2>/dev/null || true
fi
info "已启用 cellular-proxy-watch(网卡/源 IP 变化自动 rebind"
fi
info "6/6 自动验证出口(短超时,失败不阻塞安装结束)" info "6/6 自动验证出口(短超时,失败不阻塞安装结束)"
if [[ "$SKIP_START" != "true" && "$AUTO_VERIFY" == "true" ]]; then if [[ "$SKIP_START" != "true" && "$AUTO_VERIFY" == "true" ]]; then
set +e set +e
+3
View File
@@ -31,8 +31,11 @@ load_config() {
LOG_DIR="${LOG_DIR:-/var/log/cellular-proxy}" LOG_DIR="${LOG_DIR:-/var/log/cellular-proxy}"
CELLULAR_IFACE="${CELLULAR_IFACE:-}" CELLULAR_IFACE="${CELLULAR_IFACE:-}"
CELLULAR_SOURCE_IP="${CELLULAR_SOURCE_IP:-}" CELLULAR_SOURCE_IP="${CELLULAR_SOURCE_IP:-}"
BIND_SOURCE_IP="${BIND_SOURCE_IP:-false}"
REQUIRE_CELLULAR_IFACE="${REQUIRE_CELLULAR_IFACE:-true}" REQUIRE_CELLULAR_IFACE="${REQUIRE_CELLULAR_IFACE:-true}"
CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}" CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}"
ENABLE_CELLULAR_WATCH="${ENABLE_CELLULAR_WATCH:-true}"
WATCH_INTERVAL_SEC="${WATCH_INTERVAL_SEC:-60}"
PROXY_LISTEN_HOST="${PROXY_LISTEN_HOST:-0.0.0.0}" PROXY_LISTEN_HOST="${PROXY_LISTEN_HOST:-0.0.0.0}"
PROXY_MIXED_PORT="${PROXY_MIXED_PORT:-7890}" PROXY_MIXED_PORT="${PROXY_MIXED_PORT:-7890}"
PROXY_USER="${PROXY_USER:-}" PROXY_USER="${PROXY_USER:-}"
+41
View File
@@ -273,6 +273,23 @@ case "$cmd" in
auth) auth)
exec "$BASE/scripts/apply-proxy-auth.sh" "$@" exec "$BASE/scripts/apply-proxy-auth.sh" "$@"
;; ;;
watch)
case "${1:-status}" in
once) exec "$BASE/scripts/watch-cellular.sh" --once ;;
status)
systemctl status cellular-proxy-watch.service --no-pager 2>/dev/null || true
systemctl status cellular-proxy-watch.timer --no-pager 2>/dev/null || true
;;
start|enable)
systemctl enable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
;;
stop|disable)
systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
;;
logs) journalctl -u cellular-proxy-watch -n "${2:-80}" --no-pager ;;
*) exec "$BASE/scripts/watch-cellular.sh" "$@" ;;
esac
;;
logs) journalctl -u cellular-proxy -n "${1:-80}" -f ;; logs) journalctl -u cellular-proxy -n "${1:-80}" -f ;;
help|*) help|*)
cat <<H cat <<H
@@ -281,6 +298,7 @@ cpxy — cellular-proxy
cpxy detect | rebind | generate | start | stop | restart | status cpxy detect | rebind | generate | start | stop | restart | status
cpxy verify | logs [N] cpxy verify | logs [N]
cpxy auth --user U --pass P | --clear | --show cpxy auth --user U --pass P | --clear | --show
cpxy watch [status|once|start|stop|logs] # WWAN 自动监控
cpxy upgrade # 一条命令在线增量升级 cpxy upgrade # 一条命令在线增量升级
cpxy upgrade --ui-only | --force-binary | --verify cpxy upgrade --ui-only | --force-binary | --verify
H H
@@ -305,6 +323,15 @@ if [[ -f "$ROOT_DIR/systemd/cellular-proxy-admin.service" ]]; then
install -m 0644 "$ROOT_DIR/systemd/cellular-proxy-admin.service" /etc/systemd/system/cellular-proxy-admin.service install -m 0644 "$ROOT_DIR/systemd/cellular-proxy-admin.service" /etc/systemd/system/cellular-proxy-admin.service
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy-admin.service sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" /etc/systemd/system/cellular-proxy-admin.service
fi fi
# WWAN 自动监控
for u in cellular-proxy-watch.service cellular-proxy-watch-once.service cellular-proxy-watch.timer; do
if [[ -f "$ROOT_DIR/systemd/$u" ]]; then
install -m 0644 "$ROOT_DIR/systemd/$u" "/etc/systemd/system/$u"
sed -i "s|@INSTALL_DIR@|$INSTALL_DIR|g" "/etc/systemd/system/$u"
sed -i "s|@LOG_DIR@|${LOG_DIR}|g" "/etc/systemd/system/$u"
fi
done
chmod +x "$INSTALL_DIR/scripts/watch-cellular.sh" 2>/dev/null || true
systemctl daemon-reload systemctl daemon-reload
export SYSTEMD_PAGER=cat export SYSTEMD_PAGER=cat
@@ -335,6 +362,20 @@ else
fi fi
fi fi
watch_flag="$(echo "${ENABLE_CELLULAR_WATCH:-true}" | tr '[:upper:]' '[:lower:]')"
if [[ "$watch_flag" == "false" || "$watch_flag" == "0" || "$watch_flag" == "no" ]]; then
systemctl disable --now cellular-proxy-watch.service cellular-proxy-watch.timer 2>/dev/null || true
info "ENABLE_CELLULAR_WATCH=false,未启用自动监控"
else
systemctl enable cellular-proxy-watch.service 2>/dev/null || true
systemctl enable cellular-proxy-watch.timer 2>/dev/null || true
if [[ "$SKIP_START" != "true" ]]; then
systemctl restart cellular-proxy-watch.service 2>/dev/null || true
systemctl restart cellular-proxy-watch.timer 2>/dev/null || true
fi
info "已启用 cellular-proxy-watch(网卡/源 IP 变化自动 rebind"
fi
if [[ "$DO_VERIFY" == "true" && "$SKIP_START" != "true" ]]; then if [[ "$DO_VERIFY" == "true" && "$SKIP_START" != "true" ]]; then
info "出口验证…" info "出口验证…"
set +e set +e
+193
View File
@@ -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
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=cellular-proxy WWAN watch once (timer)
After=network-online.target
ConditionPathExists=@INSTALL_DIR@/etc/settings.conf
[Service]
Type=oneshot
EnvironmentFile=-@INSTALL_DIR@/etc/runtime.env
EnvironmentFile=-@INSTALL_DIR@/etc/settings.conf
ExecStart=@INSTALL_DIR@/scripts/watch-cellular.sh --once
Nice=10
MemoryMax=16M
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
StandardOutput=append:@LOG_DIR@/watch.log
StandardError=append:@LOG_DIR@/watch.log
+25
View File
@@ -0,0 +1,25 @@
[Unit]
Description=cellular-proxy WWAN watch (auto rebind on IP/link change)
After=network-online.target cellular-proxy.service
Wants=network-online.target
PartOf=cellular-proxy.service
[Service]
Type=simple
EnvironmentFile=-@INSTALL_DIR@/etc/runtime.env
EnvironmentFile=-@INSTALL_DIR@/etc/settings.conf
# 低内存:watch 脚本本身极轻;间隔可在 settings WATCH_INTERVAL_SEC 覆盖
ExecStart=@INSTALL_DIR@/scripts/watch-cellular.sh
Restart=always
RestartSec=5
MemoryMax=16M
Nice=10
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
StandardOutput=append:@LOG_DIR@/watch.log
StandardError=append:@LOG_DIR@/watch.log
[Install]
WantedBy=multi-user.target
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=cellular-proxy WWAN watch timer (fallback oneshot)
Requires=cellular-proxy-watch-once.service
[Timer]
# 兜底:即使 long-running watch 挂了,也每 2 分钟 oneshot 检查
OnBootSec=1min
OnUnitActiveSec=2min
AccuracySec=30s
Persistent=true
Unit=cellular-proxy-watch-once.service
[Install]
WantedBy=timers.target