🎨 Sync 2026-10-29 20:07:12

This commit is contained in:
github-actions[bot] committed 2026-10-29 20:07:12 +08:00
1 parent 0546d1d8e4
commit 737acd5a18
182 files changed
+10470 -1649

No files matched your search

+63 -21
View File
@@ -1,14 +1,21 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=isp-split
PKG_VERSION:=0.2.2
PKG_RELEASE:=3
PKG_VERSION:=0.3.0
PKG_RELEASE:=4
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=jihebube <jihebube@users.noreply.github.com>
include $(INCLUDE_DIR)/package.mk
# 线路体检(linecheck)自 0.3.0 起随包发货,默认 1。
# 1 = 完整功能(默认,正式发行版走这条路径);
# 0 = 构建期物理剥离:实现、CLI、conf 键、LuCI 页/菜单、socat 依赖全部不进包。
# 小闪存 / 特殊渠道的可选剥离版,必须显式指定 FEATURE_LINECHECK=0。
# 这是构建期开关,不是运行时开关:0 的产物里没有可被重新打开的代码。
FEATURE_LINECHECK?=1
define Package/isp-split
SECTION:=net
CATEGORY:=Network
@@ -19,7 +26,7 @@ define Package/isp-split
# Kwrt 源里 nft 版的包名是 mwan3-nft,与其他源命名不一致。
# nftables 版 mwan3 是硬性前提,由 install.sh 与 router-preflight 检查,
# 并在 description 里说明。
DEPENDS:=+nftables +curl +mwan3-nft
DEPENDS:=+nftables +curl +mwan3-nft $(if $(filter 1,$(FEATURE_LINECHECK)),+socat)
PKGARCH:=all
endef
@@ -34,6 +41,18 @@ define Package/isp-split/description
iptables 版 mwan3 (2.12.5),或从 Release 下载
endef
define Build/Prepare
rm -rf $(PKG_BUILD_DIR)
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./files $(PKG_BUILD_DIR)/files
# 只调用仓库里那一份 strip-linecheck.sh(随包内 strip-linecheck.sh 逐字节
# 一致),不再手抄 sed:F7 的泄漏就是 SDK 侧另抄一份、覆盖不全造成的。
# 脚本自己会做标记配对、零残留、CSS `.lc-*` 与 sh -n 断言。
if [ "$(FEATURE_LINECHECK)" != "1" ]; then \
sh $(CURDIR)/strip-linecheck.sh $(PKG_BUILD_DIR)/files; \
fi
endef
define Build/Compile
endef
@@ -55,16 +74,25 @@ fi
exit 0
endef
# 卸载后的兜底清理:cron、命名计数器、dnsmasq confdir、数据目录。
# 正常路径由 prerm 的 isp-split uninstall 完成,这里防止二进制先被删掉。
# 卸载后的兜底清理。正常路径是 prerm 里的 `isp-split uninstall`(它调
# /usr/lib/isp-split/teardown.sh,与 scripts/install.sh --uninstall 同一份实现)。
# 但 opkg/apk 会先删包文件再跑 postrm,那时 teardown.sh 已经不在盘上,
# 所以这里必须自带一份等价的内联清单 —— 这是"三条路径同源"的唯一例外,
# 由 tools/selftest.sh 第 21 节做锚点双向核对:teardown.sh 的 runtime 锚点
# 必须逐条出现在本块里,本块也不许出现清单外的清理动作。
define Package/isp-split/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
case "$${1:-}" in
upgrade|pre-upgrade|*upgrade*) exit 0 ;;
esac
# 共享实现在盘上就用它(少数包管理器/强制删除会在删文件前跑 postrm)。
if [ -f /usr/lib/isp-split/teardown.sh ]; then
sh /usr/lib/isp-split/teardown.sh all
exit $$?
fi
if [ -f /etc/crontabs/root ]; then
# mktemp 而不是 /tmp/.ispcron.$$:PID 可预测,符号链接能抢占(SEC-1)
# mktemp 而不是固定 /tmp 名:PID 可预测,符号链接能抢占(SEC-1)
_cron_tmp=$$(mktemp /etc/.isp-split-cron.XXXXXX) || _cron_tmp=""
if [ -n "$$_cron_tmp" ]; then
grep -v 'isp-split' /etc/crontabs/root > "$$_cron_tmp" 2>/dev/null || true
@@ -73,16 +101,20 @@ if [ -f /etc/crontabs/root ]; then
/etc/init.d/cron restart >/dev/null 2>&1 || true
fi
nft delete counter inet mwan3 isp_fallback >/dev/null 2>&1 || true
_state=/etc/isp-split/state/dnsmasq.confdir.orig
# B-5:状态文件已改名 dnsmasq.confdir.migrated(记的是迁移后的清理值)。
# 旧名 dnsmasq.confdir.orig 兼容读一次。
_state=/etc/isp-split/state/dnsmasq.confdir.migrated
[ -f "$$_state" ] || _state=/etc/isp-split/state/dnsmasq.confdir.orig
if [ -f "$$_state" ]; then
_orig=$$(cat "$$_state" 2>/dev/null)
_orig=$$(head -1 "$$_state" 2>/dev/null)
if [ -n "$$_orig" ]; then
uci -q set "dhcp.@dnsmasq[0].confdir=$$_orig" 2>/dev/null || true
else
uci -q delete dhcp.@dnsmasq[0].confdir 2>/dev/null || true
fi
uci -q commit dhcp 2>/dev/null || true
rm -f "$$_state"
rm -f /etc/isp-split/state/dnsmasq.confdir.migrated \
/etc/isp-split/state/dnsmasq.confdir.orig 2>/dev/null || true
fi
# BUG-6:OpenClash 接管时 dnsmasq 实际加载的是 /var/etc/dnsmasq.conf.*
# 里 conf-dir= 指向的目录,必须按同一来源清理,不能只删 /tmp/dnsmasq.d。
@@ -100,6 +132,9 @@ for _f in /var/etc/dnsmasq.conf.*; do
done
done
rm -f /tmp/dnsmasq.d/isp-split.conf /etc/isp-split/dnsmasq.d/isp-split.conf 2>/dev/null || true
# F-14:install.sh 留下的 /etc/isp-split.conf.bak.* 与主配置一并清掉(与 teardown.sh
# 同策略:默认删,逐条打印由 teardown.sh 负责;这里只兜底)。
rm -f /etc/isp-split.conf /etc/isp-split.conf.bak.* 2>/dev/null || true
rm -rf /etc/isp-split 2>/dev/null || true
exit 0
endef
@@ -112,19 +147,26 @@ define Package/isp-split/install
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_DIR) $(1)/etc/isp-split/data
$(INSTALL_BIN) ./files/usr/sbin/isp-split $(1)/usr/sbin/isp-split
$(INSTALL_BIN) ./files/etc/init.d/isp-split $(1)/etc/init.d/isp-split
$(INSTALL_BIN) ./files/usr/lib/isp-split/build.sh $(1)/usr/lib/isp-split/build.sh
$(INSTALL_BIN) ./files/etc/uci-defaults/99-isp-split $(1)/etc/uci-defaults/99-isp-split
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/usr/sbin/isp-split $(1)/usr/sbin/isp-split
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/etc/init.d/isp-split $(1)/etc/init.d/isp-split
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/build.sh $(1)/usr/lib/isp-split/build.sh
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/etc/uci-defaults/99-isp-split $(1)/etc/uci-defaults/99-isp-split
# F-14:卸载清理的唯一实现。prerm(isp-split uninstall)与
# scripts/install.sh --uninstall 都调它;postrm 在二进制先被删掉时
# 用内联等价清单兜底(锚点由 selftest 第 21 节双向核对)。
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/teardown.sh $(1)/usr/lib/isp-split/teardown.sh
$(INSTALL_DATA) ./files/usr/lib/isp-split/cidr2range.awk $(1)/usr/lib/isp-split/cidr2range.awk
$(INSTALL_DATA) ./files/usr/lib/isp-split/merge.awk $(1)/usr/lib/isp-split/merge.awk
$(INSTALL_DATA) ./files/usr/lib/isp-split/exclude.awk $(1)/usr/lib/isp-split/exclude.awk
$(INSTALL_DATA) ./files/usr/lib/isp-split/range2cidr.awk $(1)/usr/lib/isp-split/range2cidr.awk
$(INSTALL_DATA) ./files/usr/lib/isp-split/rules-splice.awk $(1)/usr/lib/isp-split/rules-splice.awk
$(INSTALL_DATA) ./files/usr/lib/isp-split/badcidr.awk $(1)/usr/lib/isp-split/badcidr.awk
$(INSTALL_DATA) ./files/usr/lib/isp-split/ispdiff.awk $(1)/usr/lib/isp-split/ispdiff.awk
$(INSTALL_CONF) ./files/etc/isp-split.conf $(1)/etc/isp-split.conf
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/cidr2range.awk $(1)/usr/lib/isp-split/cidr2range.awk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/merge.awk $(1)/usr/lib/isp-split/merge.awk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/exclude.awk $(1)/usr/lib/isp-split/exclude.awk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/range2cidr.awk $(1)/usr/lib/isp-split/range2cidr.awk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/rules-splice.awk $(1)/usr/lib/isp-split/rules-splice.awk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/badcidr.awk $(1)/usr/lib/isp-split/badcidr.awk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/ispdiff.awk $(1)/usr/lib/isp-split/ispdiff.awk
if [ -f $(PKG_BUILD_DIR)/files/usr/lib/isp-split/lc-run ]; then \
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/usr/lib/isp-split/lc-run $(1)/usr/lib/isp-split/lc-run; \
fi
$(INSTALL_CONF) $(PKG_BUILD_DIR)/files/etc/isp-split.conf $(1)/etc/isp-split.conf
endef
$(eval $(call BuildPackage,isp-split))
+81
View File
@@ -169,3 +169,84 @@ PROBE_URLS="
https://mirrors.huaweicloud.com/ubuntu-releases/24.04/ubuntu-24.04.3-live-server-amd64.iso
https://mirrors.aliyun.com/ubuntu-releases/24.04/ubuntu-24.04.3-live-server-amd64.iso
"
# lc:begin
# ---------------------------------------------------------------- 线路体检(0.3.0)
#
# `isp-split linecheck` 对每条 WAN 做:本机口 IP 分类(公网/CGNAT/私网)、
# curl --interface 出口 IP、同运营商目标四段延迟、IP 归属地、双 STUN NAT 判定、
# 评分与一句话建议。结果缓存 LINE_CHECK_MIN_INTERVAL 秒,--force 可绕过。
#
# 注意:NAT 只给「公网 / 锥形 / 对称或多出口 / CGNAT / 未知」五态,
# 不做 NAT1-4 细分——公网 STUN 服务器基本不支持 CHANGE-REQUEST(见可行性报告)。
# 体检不会修改 mwan3/nft 规则;失败只降级单项,不影响上网。
# 总开关:0=完全不执行(LuCI 页面也会提示已关闭)
LINE_CHECK_ENABLE=1
# 归属地隐私开关:1=允许向第三方查询出口 IP 归属地;0=只关闭归属地查询
#
# 开启后,体检会把**每条 WAN 的出口公网 IP**发送给第三方(默认百度,
# 失败时 ip-api.com)查询归属地。只发送出口 IP,不发送内网地址、设备名、
# MAC、流量内容。ip-api 使用 HTTP,传输不加密;介意时请设为 0,或把
# LINE_CHECK_GEO_PROVIDER 固定为 baidu。
# 关闭后不发起任何归属地请求,卡片显示「归属地:未查询(隐私开关关闭)」。
# 省/市不可知,但仍可用本地 @isp_* 集合判断运营商大类,不需要外发。
# 注意:此开关只关归属地;出口 IP 回显、STUN 与延迟探测仍会外发。
# 要完全停止体检,请设 LINE_CHECK_ENABLE=0。
LINE_CHECK_GEO=1
# 归属地服务商:auto | baidu | ip-api | off
# auto 先百度(HTTPS、国内可达),失败转 ip-api(HTTP)
# baidu 只用百度;失败显示「归属地未知」,不会自动转 HTTP 源
# ip-api 只用 ip-api(HTTP,明文)
# off 等同 LINE_CHECK_GEO=0
LINE_CHECK_GEO_PROVIDER="auto"
# 归属地缓存有效期(秒)。同一个出口 IP 在有效期内只查一次。
LINE_CHECK_GEO_TTL=86400
# 手动触发的最小间隔(秒)。间隔内重复执行直接返回上次结果(缓存),
# LuCI 的「立即体检」按钮也按这个值节流。范围 15-3600。
LINE_CHECK_MIN_INTERVAL=60
# 单次探测量(1-10 秒):curl --connect-timeout 与 socat -T 都用它。
# 宽带正常时 3 秒足够;线路很差可调到 5,但整次体检会更慢。
LINE_CHECK_TIMEOUT=3
# 单条 WAN 的探测预算(秒)。超过后跳过剩余的 STUN/归属地,
# 只保留已完成的项目并在明细里标注,避免 LuCI/rpcd 超时(建议 15-25)。
LINE_CHECK_BUDGET=20
# 每条 WAN 的同运营商探测目标:<运营商源名>:<域名1>,<域名2>
# 源名必须与 ISP_TABLE 第一列一致;域名运行时解析,再用对应 @isp_* 集合
# 校验归属(不随机抽集合 IP、不扫端口)。全部失败时用下面的跨网基线兜底,
# 并在卡片上标注「跨网基线」,延迟项最多 15 分。
LINE_CHECK_TARGETS="
chinatelecom:www.chinatelecom.com.cn,www.21cn.com
unicom_cnc:www.10010.com,www.chinaunicom.com.cn
cmcc:www.migu.cn,www.10086.cn
cernet:www.tsinghua.edu.cn,www.pku.edu.cn
"
# 跨网基线目标(仅用于判断“链路是否通”,不计入同网延迟分)
LINE_CHECK_BASELINE="www.baidu.com,www.qq.com"
# STUN 服务器:<域名或 IP>:<端口>,按顺序解析,取两个不同 IP 各探一次。
# 同一本地端口 + so-bindtodevice 锁线;失败自动换下一个/重试一次。
# 公网 STUN 可达性会波动,不要只写一个;UDP 3478 被阻断时只降级为“未知”。
# 可选:stun.chat.bilibili.com:3478(实测有时无 A 记录,按需加回列表)
LINE_CHECK_STUN_SERVERS="
stun.miwifi.com:3478
stun.douyucdn.cn:18000
stun.freeswitch.org:3478
stun.cloudflare.com:3478
"
# 出口 IP 回显服务(逗号分隔,按顺序尝试)。只用于拿公网出口 IP。
LINE_CHECK_ECHO_URLS="https://api.ipify.org,https://ipv4.icanhazip.com"
# 体检请求的 User-Agent。部分运营商站点对默认 curl UA 返回 412/418,
# 必须带浏览器 UA;不要改成空值。
LINE_CHECK_UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
# lc:end
+20 -6
View File
@@ -1,9 +1,12 @@
#!/bin/sh
# 数据管线:原始 CIDR -> 归一化 -> 按优先级互斥化 -> 每运营商一个 CIDR 文件
#
# 用法: build.sh <rawdir> <outdir> <优先级列表>
# 用法: build.sh <rawdir> <outdir> <优先级列表> [<占位列表>]
# rawdir : 含 <isp>.raw 的目录,每行一个 CIDR
# 优先级 : 空格分隔的运营商名,排在前面的优先占有重叠地址
# 占位列表: 可选,只有列在这里的运营商会把地址记入 claimed(差集占位)。
# 缺省=全部优先级行都占位(保持旧行为)。cmd_fetch 传
# enabled=1 的行:禁用行仍按原顺序生成自己的 .txt,但不抢地址。
#
# 设计要点:
# 1. 每个集合内部先合并去重(RFC1918 无关,只做区间合并)
@@ -15,6 +18,11 @@ set -eu
RAW=$1
OUT=$2
PRIORITY=$3
if [ $# -ge 4 ]; then
CLAIM=$4
else
CLAIM=$PRIORITY
fi
LIB=${ISP_SPLIT_LIB:-$(dirname "$0")}
WORK=$OUT/.work
@@ -42,11 +50,17 @@ for isp in $PRIORITY; do
# 3. 区间 -> 最小 CIDR 集合
awk -f "$LIB/range2cidr.awk" "$WORK/$isp.excl" > "$OUT/$isp.txt"
# 4. 已占用集合累加(需重新合并以保持有序且互不重叠)
cat "$WORK/claimed" "$WORK/$isp.excl" |
sort |
awk -f "$LIB/merge.awk" > "$WORK/claimed.new"
mv "$WORK/claimed.new" "$WORK/claimed"
# 4. 已占用集合累加(需重新合并以保持有序且互不重叠)。
# F-27:只有占位列表里的行才累加——禁用行不再抢低优先级启用集合
# 的地址;但它的 .txt 仍按原优先级序号生成,方便重新启用/预演。
case " $CLAIM " in
*" $isp "*)
cat "$WORK/claimed" "$WORK/$isp.excl" |
sort |
awk -f "$LIB/merge.awk" > "$WORK/claimed.new"
mv "$WORK/claimed.new" "$WORK/claimed"
;;
esac
# 统计
n_raw=$(awk -f "$LIB/cidr2range.awk" "$src" | wc -l | tr -d ' ')
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# 后台探测引擎入口(仅 --background 模式使用)。
#
# ash 的命令替换 / 管道会 fork 出继承父进程 argv 的子 shell。如果探测直接
# 在持锁 worker 里跑,进程表会把那些子 shell 也数成额外 worker(fix29 实测
# 1 个真 worker + 3 个子 shell = 4)。这里用独立 argv 的入口运行主脚本,
# 进程表里带 "isp-split 子命令" 的只剩唯一那个持锁监督者。
#
# 参数原样透传:lc-run <子命令> <参数...>
# 被加载的主脚本路径由监督者通过 ISP_SPLIT_LC_SELF 传入(argv 里不带路径,
# 否则又会命中 "isp-split <子命令>" 的进程计数)。
. "${ISP_SPLIT_LC_SELF:-/usr/sbin/isp-split}"
@@ -0,0 +1,420 @@
#!/bin/sh
# 卸载清理的唯一实现(F-14)
#
# 背景:卸载有三条路径,以前各写各的清单:
# 1. isp-split uninstall(cmd_uninstall)——摘规则、删策略、删数据目录,
# 但不删命名计数器 isp_fallback,也不删 /usr/sbin/isp-split 等文件
# 2. scripts/install.sh --uninstall——只删 LuCI 资源,脚本与配置全留着
# 3. 包 postrm——删 cron/计数器/confdir,包文件则由包管理器删
# 结果:Hermes 动态复现 install.sh --uninstall rc=0,而
# /usr/sbin/isp-split、/etc/init.d/isp-split、/usr/lib/isp-split/、
# /etc/isp-split.conf、isp_fallback 计数器、.conf.bak.* 全留着。
#
# 现在三条路径都走这里,清单只有一份:
# isp-split uninstall -> 先摘 mwan3 规则(要 rules-splice.awk),再 all
# scripts/install.sh --uninstall -> 先调 isp-split uninstall(若在),再 all
# 包 postrm -> 二进制还在就调本脚本 runtime;包文件已被删时
# 才走内联兜底(清单锚点由 selftest 第 21 节双向核对)
#
# 用法:
# sh teardown.sh all # mwan3 段 + runtime 残留 + 文件
# sh teardown.sh mwan3 # 只删 mwan3 里 isp_split=1 的 ipset/policy/member
# sh teardown.sh runtime # cron 行 / isp_fallback 计数器 / dnsmasq confdir 与片段 / LuCI 缓存
# sh teardown.sh files # 本工具安装的文件(含 /etc/isp-split 数据目录)
# sh teardown.sh --list # 打印清单(selftest 与 postrm 漂移断言用),不动作
# 选项:
# --keep-conf-bak # 保留 /etc/isp-split.conf.bak.*(默认删除并逐条打印)
#
# 为什么默认删 .conf.bak.*:它们是安装脚本每次覆盖安装都会新生成的副本,
# 不是用户手写的备份。留着等于"卸载"留下一批与工具同名同前缀的孤儿文件,
# 重装时再叠一层;删除前会逐条打印路径,想留档可以照路径先 cp 走。要长期
# 留档就显式 --keep-conf-bak,行为可选且可见。
#
# 环境变量(与 isp-split 同名同义;自检/打包用):
# ISP_SPLIT_DESTDIR 根前缀(默认空)。设置后所有绝对路径都加上它,
# 用于在沙箱里演练卸载,不碰宿主 /usr /etc
# ISP_SPLIT_BASE /etc/isp-split
# ISP_SPLIT_CONF /etc/isp-split.conf
# ISP_SPLIT_CRONTAB /etc/crontabs/root
# ISP_SPLIT_LIB /usr/lib/isp-split
# DNSDIR 旧版片段目录(默认 <BASE>/dnsmasq.d)
# DNSMASQ_DEFAULT_DIR dnsmasq 默认 conf-dir(默认 /tmp/dnsmasq.d)
# DNSMASQ_CONF_GLOB 运行配置 glob(默认 /var/etc/dnsmasq.conf.*)
# MWAN3_SERVICE / CRON_SERVICE / RPCD_SERVICE 服务脚本(自检可换 stub)
set -u
DESTDIR=${ISP_SPLIT_DESTDIR:-}
case "$DESTDIR" in
"" | "/") DESTDIR="" ;;
/*) DESTDIR=${DESTDIR%/} ;;
*)
echo "ISP_SPLIT_DESTDIR 必须是绝对路径: '$DESTDIR'" >&2
exit 1
;;
esac
BASE=${ISP_SPLIT_BASE:-$DESTDIR/etc/isp-split}
CONF=${ISP_SPLIT_CONF:-$DESTDIR/etc/isp-split.conf}
CRONTAB=${ISP_SPLIT_CRONTAB:-$DESTDIR/etc/crontabs/root}
LIB=${ISP_SPLIT_LIB:-$DESTDIR/usr/lib/isp-split}
DNSDIR=${DNSDIR:-$BASE/dnsmasq.d}
DNSMASQ_DEFAULT_DIR=${DNSMASQ_DEFAULT_DIR:-$DESTDIR/tmp/dnsmasq.d}
DNSMASQ_CONF_GLOB=${DNSMASQ_CONF_GLOB:-$DESTDIR/var/etc/dnsmasq.conf.*}
STATE_DIR=$BASE/state
# B-5:状态文件名与语义对齐——它记的是"迁移后写进 UCI 的清理值",不是原值。
STATE_NEW=$STATE_DIR/dnsmasq.confdir.migrated
STATE_OLD=$STATE_DIR/dnsmasq.confdir.orig
MWAN3_SERVICE=${MWAN3_SERVICE:-$DESTDIR/etc/init.d/mwan3}
CRON_SERVICE=${CRON_SERVICE:-$DESTDIR/etc/init.d/cron}
RPCD_SERVICE=${RPCD_SERVICE:-$DESTDIR/etc/init.d/rpcd}
KEEP_CONF_BAK=0
SCOPE=""
say() { printf '[teardown] %s\n' "$*"; }
have() { command -v "$1" >/dev/null 2>&1; }
# 只有真实根(DESTDIR 为空)才动服务:沙箱演练里重启服务既没意义,也会误伤宿主。
svc_restart() {
[ -z "$DESTDIR" ] || return 0
[ -x "$1" ] || return 0
"$1" restart >/dev/null 2>&1 || true
return 0
}
# ---------------------------------------------------------------- 清单
# 文件/目录清单(相对 DESTDIR)。--list 与 files 段共用,杜绝两处漂移。
files_list() {
cat <<'EOF'
path usr/sbin/isp-split
path etc/init.d/isp-split
dir usr/lib/isp-split
path etc/isp-split.conf
glob etc/isp-split.conf.bak.*
dir etc/isp-split
path etc/uci-defaults/99-isp-split
dir www/luci-static/resources/view/isp-split
dir www/luci-static/resources/isp-split
path usr/share/luci/menu.d/luci-app-isp-split.json
# lc:begin
# 体检页的 menu 节点只在完整版里存在;0 版(FEATURE_LINECHECK=0)连这一行
# 都不该留在卸载清单里(strip-linecheck.sh 的零残留断言会抓,19b.2 覆盖)。
path usr/share/luci/menu.d/luci-app-isp-split-linecheck.json
# lc:end
path usr/share/rpcd/acl.d/luci-app-isp-split.json
EOF
}
# runtime 的锚点:每一条都必须能在 Makefile postrm 的内联兜底里找到
# (selftest 第 21 节双向核对,防"三条路径又各写一份")。
runtime_anchors() {
cat <<'EOF'
anchor nft delete counter inet mwan3 isp_fallback
anchor /etc/isp-split/state/dnsmasq.confdir.migrated
anchor /etc/isp-split/state/dnsmasq.confdir.orig
anchor conf-dir=
anchor /tmp/dnsmasq.d/isp-split.conf
anchor /etc/isp-split/dnsmasq.d/isp-split.conf
anchor .isp-split-cron.XXXXXX
anchor rm -rf /etc/isp-split
EOF
}
print_list() {
echo "== 卸载清单(唯一实现:teardown.sh)=="
echo "-- mwan3(UCI 段 + 服务重载)--"
echo " 带 isp_split=1 标记的 ipset 段(含改名/删除的孤儿段)"
echo " 带 isp_split=1 标记的 policy 段"
echo " 带 isp_split=1 标记的 member 段(仍被其它策略引用的保留)"
echo " nft 规则块由 isp-split uninstall 的 rules-splice 摘除"
echo "-- runtime 残留 --"
echo " crontab 里的 isp-split 行"
echo " nft 命名计数器 isp_fallback"
echo " dnsmasq confdir 状态文件(恢复 UCI 后删除)"
echo " dnsmasq 片段 isp-split.conf(探测目录 + 默认目录 + 旧版目录)"
echo " LuCI 缓存目录"
echo "-- 文件 --"
files_list | while read -r _k _v; do
case "$_k" in
path) echo " $_v" ;;
dir) echo " $_v/(目录)" ;;
glob) echo " $_v(历史备份,默认删除;--keep-conf-bak 保留)" ;;
esac
done
echo "-- 机器可读 --"
files_list
runtime_anchors
}
# ---------------------------------------------------------------- mwan3
# 按 isp_split=1 标记删,而不是按"当前 ISP_TABLE 的名字"删:改名/删除的
# 旧段会漏掉,永远清不掉(B-4)。无标记的段一律不碰。
cmd_mwan3() {
if ! have uci; then
say "没有 uci,跳过 mwan3 段清理"
return 0
fi
_mw_n=0
for _mw_sec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=ipset$/\1/p'); do
[ "$(uci -q get "mwan3.$_mw_sec.isp_split" 2>/dev/null)" = "1" ] || continue
uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_n=$((_mw_n + 1))
done
_mw_np=0
for _mw_sec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=policy$/\1/p'); do
[ "$(uci -q get "mwan3.$_mw_sec.isp_split" 2>/dev/null)" = "1" ] || continue
uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_np=$((_mw_np + 1))
done
# member 段:仍被任何策略引用的不能删(删了 mwan3 会渲染失败)
_mw_refs=" "
for _mw_psec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=policy$/\1/p'); do
for _mw_x in $(uci -q get "mwan3.$_mw_psec.use_member" 2>/dev/null); do
_mw_refs="$_mw_refs$_mw_x "
done
done
_mw_nm=0
for _mw_sec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=member$/\1/p'); do
[ "$(uci -q get "mwan3.$_mw_sec.isp_split" 2>/dev/null)" = "1" ] || continue
case "$_mw_refs" in
*" $_mw_sec "*)
say "警告:成员段 $_mw_sec 仍被其它策略引用,保留"
continue
;;
esac
uci -q delete "mwan3.$_mw_sec" 2>/dev/null && _mw_nm=$((_mw_nm + 1))
done
uci -q commit mwan3 2>/dev/null || true
[ "$_mw_n" -gt 0 ] &&
say "已删除 $_mw_n 个带 isp_split 标记的 ipset 段(含改名/删除的孤儿段)"
[ "$_mw_np" -gt 0 ] || [ "$_mw_nm" -gt 0 ] &&
say "已删除 $_mw_np 个带 isp_split 标记的策略段、$_mw_nm 个成员段"
# 规则已由 rules-splice 摘掉,这里 reload 让集合/策略随之消失。
if [ -x "$MWAN3_SERVICE" ]; then
"$MWAN3_SERVICE" reload >/dev/null 2>&1 || true
fi
return 0
}
# ---------------------------------------------------------------- runtime
# B-5:旧名里存的是"迁移后的清理值",名字与语义不符(PROPOSAL-0.2.2 §B-5)。
# 升级上来的设备可能还留着旧名:迁到新名,之后只认新名。迁移失败也不阻断,
# 因为读取侧两个名字都认。
_state_adopt() {
[ -f "$STATE_NEW" ] && return 0
[ -f "$STATE_OLD" ] || return 0
if mv -f "$STATE_OLD" "$STATE_NEW" 2>/dev/null; then
say "旧状态文件 dnsmasq.confdir.orig 已迁移为 dnsmasq.confdir.migrated(B-5)"
else
cp -f "$STATE_OLD" "$STATE_NEW" 2>/dev/null && rm -f "$STATE_OLD"
fi
return 0
}
_state_read() {
if [ -f "$STATE_NEW" ]; then
head -1 "$STATE_NEW" 2>/dev/null | tr -d '\r'
elif [ -f "$STATE_OLD" ]; then
head -1 "$STATE_OLD" 2>/dev/null | tr -d '\r'
fi
return 0
}
# 片段可能残留的目录:运行配置里的 conf-dir=(权威源)+ UCI 当前值 +
# 状态文件记录值 + 默认目录 + 旧版目录,去重后逐行输出。
_frag_dirs() {
{
for _fd_f in $DNSMASQ_CONF_GLOB; do
[ -f "$_fd_f" ] || continue
sed -n 's/^[[:space:]]*conf-dir=//p' "$_fd_f" 2>/dev/null
done
printf '%s\n' "${_frag_uci:-}" "${_frag_state:-}" \
"$DNSMASQ_DEFAULT_DIR" "$DNSDIR"
} | tr ' ' '\n' |
sed -e 's/,.*$//' -e 's/^"//' -e 's/"$//' -e 's:/*$::' |
awk 'NF && !seen[$0]++'
}
cmd_runtime() {
_state_adopt
_orig=$(_state_read)
_frag_state=$_orig
_frag_uci=""
have uci && _frag_uci=$(uci -q get dhcp.@dnsmasq[0].confdir 2>/dev/null || true)
# 1) 恢复 UCI confdir:只有真的改过(有状态文件)才碰 UCI。
if [ -f "$STATE_NEW" ] || [ -f "$STATE_OLD" ]; then
if [ -n "$_orig" ]; then
if [ "$_frag_uci" != "$_orig" ]; then
if uci -q set "dhcp.@dnsmasq[0].confdir=$_orig" 2>/dev/null &&
uci -q commit dhcp 2>/dev/null; then
say "已恢复 dnsmasq confdir=$_orig"
fi
fi
elif [ -n "$_frag_uci" ]; then
if uci -q delete dhcp.@dnsmasq[0].confdir 2>/dev/null &&
uci -q commit dhcp 2>/dev/null; then
say "已恢复 dnsmasq confdir(回落到默认 $DNSMASQ_DEFAULT_DIR)"
fi
fi
fi
rm -f "$STATE_NEW" "$STATE_OLD" 2>/dev/null || true
# 2) 清片段。探测来源优先,但不依赖探测是否成功——目录列表是并集。
_n=0
for _d in $(_frag_dirs); do
[ -f "$_d/isp-split.conf" ] || continue
rm -f "$_d/isp-split.conf" 2>/dev/null && _n=$((_n + 1))
done
[ "$_n" -gt 0 ] && say "已清理 $_n 处 dnsmasq 片段 isp-split.conf"
# 3) 命名计数器(旧版只有包 postrm 删它,脚本卸载路径漏了)
if have nft && nft list table inet mwan3 >/dev/null 2>&1; then
if nft list counter inet mwan3 isp_fallback >/dev/null 2>&1; then
nft delete counter inet mwan3 isp_fallback >/dev/null 2>&1 &&
say "已删除 nft 计数器 inet mwan3 isp_fallback"
fi
fi
# 4) crontab 行。mktemp 而不是固定 /tmp 名:/tmp 世界可写,符号链接能抢占(SEC-1)。
if [ -f "$CRONTAB" ] && grep -q 'isp-split' "$CRONTAB" 2>/dev/null; then
_cron_dir=$(dirname "$CRONTAB")
_cron_tmp=$(mktemp "$_cron_dir/.isp-split-cron.XXXXXX" 2>/dev/null) ||
_cron_tmp=$(mktemp "$CRONTAB.XXXXXX" 2>/dev/null) || _cron_tmp=""
if [ -n "$_cron_tmp" ]; then
grep -v 'isp-split' "$CRONTAB" > "$_cron_tmp" 2>/dev/null || true
if mv -f "$_cron_tmp" "$CRONTAB"; then
say "已移除 crontab 里的 isp-split 行"
svc_restart "$CRON_SERVICE"
else
rm -f "$_cron_tmp"
fi
fi
fi
# 5) LuCI 缓存(页面文件在 files 段删)
rm -rf "$DESTDIR"/tmp/luci-indexcache* "$DESTDIR"/tmp/luci-modulecache* 2>/dev/null || true
return 0
}
# ---------------------------------------------------------------- files
cmd_files() {
_del=0
_err=0
while read -r _k _p; do
case "$_k" in
path)
[ -e "$DESTDIR/$_p" ] || [ -L "$DESTDIR/$_p" ] || continue
if rm -f "$DESTDIR/$_p"; then
_del=$((_del + 1))
else
say "错误:删不掉 $DESTDIR/$_p"
_err=1
fi
;;
dir)
[ -d "$DESTDIR/$_p" ] || continue
if rm -rf "$DESTDIR/$_p"; then
_del=$((_del + 1))
else
say "错误:删不掉 $DESTDIR/$_p"
_err=1
fi
;;
glob)
# 这里必须真的做通配:把目录与文件名分开,目录加引号(容忍空格),
# 文件名部分不加引号交给 shell 展开。整段引号会把 `*` 当成字面量,
# 结果一个 .bak 都删不掉(自检 21.2/21.4 抓到的第一版 bug)。
_gdir=$(dirname "$DESTDIR/$_p")
_gbase=$(basename "$_p")
for _g in "$_gdir"/$_gbase; do
[ -f "$_g" ] || continue
if [ "$KEEP_CONF_BAK" = "1" ]; then
say "保留历史配置备份: $_g(--keep-conf-bak)"
continue
fi
# 删除前逐条打印:想留档可以在同一台机器上先照路径 cp 走。
say "删除历史配置备份: $_g"
if rm -f "$_g"; then
_del=$((_del + 1))
else
say "错误:删不掉 $_g"
_err=1
fi
done
;;
esac
done <<EOF
$(files_list)
EOF
# BASE 可被 ISP_SPLIT_BASE 改名(自检沙箱),清单里的是默认路径,这里补上
# 真实值。ISP_SPLIT_LIB 故意不参与删除:它只用于离线自检,可能指到源码树,
# 删它会把仓库自己的 usr/lib/isp-split 抹掉。
if [ "$BASE" != "$DESTDIR/etc/isp-split" ] && [ -d "$BASE" ]; then
if rm -rf "$BASE"; then
_del=$((_del + 1))
else
say "错误:删不掉 $BASE"
_err=1
fi
fi
if [ "$_del" -gt 0 ]; then
say "已删除 $_del 项文件/目录"
else
say "没有需要删除的文件"
fi
if [ "$_err" -ne 0 ]; then
say "有文件删不掉(只读挂载或权限问题),卸载不完整"
return 1
fi
return 0
}
# ---------------------------------------------------------------- main
while [ $# -gt 0 ]; do
case "$1" in
--keep-conf-bak) KEEP_CONF_BAK=1 ;;
--list) SCOPE=list ;;
all | mwan3 | runtime | files) SCOPE=$1 ;;
-h | --help)
sed -n '2,38p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "未知参数: $1(可用:all|mwan3|runtime|files|--list|--keep-conf-bak)" >&2
exit 2
;;
esac
shift
done
if [ -z "$SCOPE" ]; then
echo "用法: sh teardown.sh all|mwan3|runtime|files|--list [--keep-conf-bak]" >&2
exit 2
fi
case "$SCOPE" in
list) print_list ;;
mwan3) cmd_mwan3 ;;
runtime) cmd_runtime ;;
files) cmd_files ;;
all)
cmd_mwan3
cmd_runtime
svc_restart "$RPCD_SERVICE"
cmd_files
;;
esac
File diff suppressed because it is too large. Load diff
+125
View File
@@ -0,0 +1,125 @@
#!/bin/sh
# 构建期剥离线路体检(FEATURE_LINECHECK=0)。
#
# 用法:sh strip-linecheck.sh <staging-root>
#
# 这是**唯一**的剥离实现。手工打包(scripts/build-ipk.sh)与 SDK/buildroot
# (主包 / LuCI 包两份 Makefile 的 Build/Prepare)都必须调用它,不许再手抄
# sed。F7 的根因就是 SDK 侧另抄了三行 sed:只覆盖 config.js,漏了 htdocs 的
# isp-split.css,于是本地(走本脚本,全树扫)、VM 全绿,24.10 SDK 产物泄漏。
#
# 源文件里用成对标记包住 feature 专属内容(shell/conf/js/css 用 `lc:begin` /
# `lc:end` 注释,Markdown 用 `<!-- lc:begin -->` / `<!-- lc:end -->`)。
# 标记必须包住内容本体,不能只包一行标题注释:CSS 曾经只包了标题,`.lc-*`
# 样式体裸奔进包,而“grep linecheck 字符串”的断言看不见它——所以本脚本除了
# 字符串零残留,还断言标记配对、剥离后标记清零、CSS 里不留 `.lc-*` 样式类。
#
# 这不是运行时开关:删掉的内容不会进入包、不会进入安装目录,也不存在
# 一个环境变量或 UCI 键能把它打开。
set -eu
ROOT=${1:-}
[ -n "$ROOT" ] || { echo "用法: sh strip-linecheck.sh <staging-root>" >&2; exit 1; }
[ -d "$ROOT" ] || { echo "目录不存在: $ROOT" >&2; exit 1; }
WORK=$(mktemp -d 2>/dev/null) || WORK=$(mktemp -d /tmp/lcstrip.XXXXXX) ||
{ echo "无法创建临时目录" >&2; exit 1; }
trap 'rm -rf "$WORK"' EXIT
# 0) 标记配对自检:缺 lc:end 时 sed 会静默删到文件末尾,必须在剥离前拦住。
cat > "$WORK/check-marks.sh" <<'EOS'
#!/bin/sh
rc=0
for f do
grep -Iq 'lc:begin\|lc:end' "$f" 2>/dev/null || continue
awk '
FNR == 1 { d = 0 }
/lc:begin/ { d++ }
/lc:end/ {
d--
if (d < 0) {
printf "lc:end 出现在 lc:begin 之前: %s\n", FILENAME > "/dev/stderr"
bad = 1
exit 1
}
}
END {
if (!bad && d != 0) {
printf "lc:begin 缺 lc:end(%d 层未闭合): %s\n", d, FILENAME > "/dev/stderr"
exit 1
}
}
' "$f" || { echo "标记不配对: $f" >&2; rc=1; }
done
exit $rc
EOS
find "$ROOT" -type f -exec sh "$WORK/check-marks.sh" {} \; || exit 1
del_blocks() {
_f=$1
grep -Iq 'lc:begin' "$_f" 2>/dev/null || return 0
_tmp=$_f.lcstrip.$$
sed '/lc:begin/,/lc:end/d' "$_f" > "$_tmp" || { rm -f "$_tmp"; return 1; }
# 保留原权限位(isp-split 必须仍可执行)
chmod --reference="$_f" "$_tmp" 2>/dev/null || chmod 644 "$_tmp"
mv -f "$_tmp" "$_f"
}
# 1) 文本文件里的标记区
find "$ROOT" -type f | while IFS= read -r f; do
del_blocks "$f" || touch "$WORK/del-failed"
done
[ ! -e "$WORK/del-failed" ] || { echo "剥离时有文件写入失败" >&2; exit 1; }
# 2) feature=1 专属文件
find "$ROOT" -type f \
\( -name 'linecheck.js' -o -name 'lc-run' -o -name 'luci-app-isp-split-linecheck.json' \) \
-exec rm -f {} +
# 3) 空目录顺手收掉(LuCI view 目录只有 linecheck.js 时)
find "$ROOT" -type d -empty -delete 2>/dev/null || true
# 4) 零残留断言(白名单为空)
_leak=$(grep -rIl 'linecheck\|LINE_CHECK\|线路体检' "$ROOT" 2>/dev/null || true)
if [ -n "$_leak" ]; then
echo "剥离失败,以下文件仍有 linecheck 残留:" >&2
printf '%s\n' "$_leak" | sed 's/^/ /' >&2
exit 1
fi
# 4b) 标记必须被吃干净(配对自检通过 + sed 正常时必然成立;写死断言防回归)
_marks=$(grep -rIl 'lc:begin\|lc:end' "$ROOT" 2>/dev/null || true)
if [ -n "$_marks" ]; then
echo "剥离失败,以下文件仍残留 lc:begin / lc:end 标记:" >&2
printf '%s\n' "$_marks" | sed 's/^/ /' >&2
exit 1
fi
# 4c) CSS:标记必须包住样式体。`.lc-*` 不含 linecheck 字样,字符串断言抓不到,
# 所以单独断言;顺手断言花括号平衡,防止把规则体切成半截。
find "$ROOT" -type f -name '*.css' | while IFS= read -r _css; do
if grep -q '\.lc-' "$_css"; then
echo "CSS 仍有 .lc-* 样式(标记没有包住样式体): $_css" >&2
touch "$WORK/fail"
fi
# 用 tr 数花括号:老 busybox awk 对 gsub(/{/) 会报 "bad regex"
_o=$(tr -cd '{' <"$_css" | wc -c | tr -d ' \n')
_c=$(tr -cd '}' <"$_css" | wc -c | tr -d ' \n')
if [ "$_o" != "$_c" ]; then
echo "CSS 花括号不平衡(剥离切坏了规则体): $_css({=$_o }=$_c)" >&2
touch "$WORK/fail"
fi
done
[ ! -e "$WORK/fail" ] || exit 1
# 5) 语法自检(脚本仍能被 shell 解析)
_main=$ROOT/usr/sbin/isp-split
if [ -f "$_main" ]; then
sh -n "$_main" || { echo "剥离后 isp-split 语法错误" >&2; exit 1; }
fi
_conf=$ROOT/etc/isp-split.conf
if [ -f "$_conf" ]; then
sh -n "$_conf" || { echo "剥离后 isp-split.conf 语法错误" >&2; exit 1; }
fi
echo "已剥离线路体检: $ROOT"