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:
@@ -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 相同)
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ LOG_DIR=/var/log/cellular-proxy
|
|||||||
# ---- 数据网卡(代理出口强制绑定)----
|
# ---- 数据网卡(代理出口强制绑定)----
|
||||||
# 留空则自动探测 wwan/usb/enx 等;建议手动填准
|
# 留空则自动探测 wwan/usb/enx 等;建议手动填准
|
||||||
CELLULAR_IFACE=
|
CELLULAR_IFACE=
|
||||||
# 可选:数据侧源 IPv4(一般留空)
|
# 元数据:数据侧当前源 IPv4(generate/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
@@ -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_src(settings 仅作元数据)"
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
# 默认开启 watch(ENABLE_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
|
||||||
|
|||||||
@@ -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:-}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user