🛸 Sync 2026-10-29 02:04:12

This commit is contained in:
github-actions[bot] committed 2026-10-29 02:04:12 +08:00
1 parent b21929fceb
commit bfd56813e0
37 files changed
+3918 -4346

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=isp-split
PKG_VERSION:=0.2.1
PKG_RELEASE:=2
PKG_VERSION:=0.2.2
PKG_RELEASE:=3
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=jihebube <jihebube@users.noreply.github.com>
+186 -23
View File
@@ -15,7 +15,14 @@
# install 注册开机自启与每日更新
# uninstall 卸载所有改动
set -u
ISP_VERSION="0.2.1-logo"
ISP_VERSION="0.2.2"
# 配置表里的注释行(# 开头,允许前导空白)必须在按空白展开之前整行删掉
# (B-1)。否则 `for row in $ISP_TABLE` 会把 "#" 与后面的文字拆成两个
# token,注释片段被当成数据行。只改内存,不写回配置文件。
_strip_comments() {
printf '%s\n' "$1" | sed 's/^[[:space:]]*#.*$//' | sed '/^[[:space:]]*$/d'
}
CONF=${ISP_SPLIT_CONF:-/etc/isp-split.conf}
BASE=${ISP_SPLIT_BASE:-/etc/isp-split}
@@ -47,9 +54,10 @@ DNSMASQ_SERVICE=${DNSMASQ_SERVICE:-/etc/init.d/dnsmasq}
# 但 ISP_TABLE 的最后一列会变成 "1\r",`[ "$4" = "1" ]` 永远不成立:
# plan 只剩一张空表、status 只剩兜底行、apply 报"没有启用的运营商",
# 而真正的原因(行尾多了一个 \r)完全看不出来。这里在入口统一去掉。
ISP_TABLE=$(printf '%s' "${ISP_TABLE:-}" | tr -d '\r')
DNS_TABLE=$(printf '%s' "${DNS_TABLE:-}" | tr -d '\r')
DOMAIN_SPLIT=$(printf '%s' "${DOMAIN_SPLIT:-}" | tr -d '\r')
# 注释行也在同一处删掉(B-1):删除必须发生在任何按空白展开之前。
ISP_TABLE=$(_strip_comments "$(printf '%s' "${ISP_TABLE:-}" | tr -d '\r')")
DNS_TABLE=$(_strip_comments "$(printf '%s' "${DNS_TABLE:-}" | tr -d '\r')")
DOMAIN_SPLIT=$(_strip_comments "$(printf '%s' "${DOMAIN_SPLIT:-}" | tr -d '\r')")
# ---- 多数据源(0.2.1 默认双源)----
# ISP_SOURCES 为空 = 单源兼容模式:仍按 ISP_MIRROR/<源名>.txt 拉取,
@@ -1453,7 +1461,7 @@ cmd_apply() {
# 取第一个 enabled=1 的行,而不是无条件取第一行:用户禁用第一个
# 运营商后,wire 不会为它建集合,旧写法会让每日 refresh 永久失败。
_fs=""
for _row in $(_conf_val ISP_TABLE 2>/dev/null); do
for _row in $(_conf_table ISP_TABLE 2>/dev/null); do
_old_ifs=$IFS
IFS=':'
set -- $_row
@@ -1596,6 +1604,112 @@ _write_rule() {
} >> "$WORK/rules.txt"
}
# ---- 孤儿 ipset 段收敛(B-4)----
# 段名可以改名/删除/禁用,但 mwan3 的 config ipset 段不会自己消失:
# 旧段仍然 enabled=1、loadfile 指向旧数据文件,mwan3 下次 reload 照样
# 建集合、灌元素,永久驻留。旧版 uninstall 也只看"当前表里的名字",
# 连卸载都清不掉。
#
# 识别方式只有一个:isp_split=1 标记(_wire_ipset 写入)。绝不按 isp_
# 前缀猜 —— 用户自己的 mwan3 ipset 段可能也叫 isp_xxx。没有标记的段
# (0.2.1-happy 之前的旧版本写的)一律不动,只在日志里列出来。
#
# 当前仍有效的集合名:ISP_TABLE 里第 4 列=1 的行。DNS_TABLE 不引用
# ipset,但把它的字段也纳入保护集合,避免同名段被误删(保守兜底)。
_keep_set_names() {
_ks=" "
for _row in $ISP_TABLE; do
[ -n "$_row" ] || continue
_old_ifs=$IFS
IFS=':'
set -- $_row
IFS=$_old_ifs
[ $# -ge 4 ] || continue
[ "$4" = "1" ] || continue
_ks="$_ks$2 "
done
for _row in ${DNS_TABLE:-}; do
[ -n "$_row" ] || continue
_old_ifs=$IFS
IFS=':'
set -- $_row
IFS=$_old_ifs
for _f in "$@"; do
case "$_f" in "" | *[!A-Za-z0-9_]*) continue ;; esac
_ks="$_ks$_f "
done
done
printf '%s\n' "$_ks"
}
# 所有 config rule 引用的 ipset 名(空格分隔,带前后空格便于整词匹配)
_rule_ipset_refs() {
_ri=" "
for _ri_sec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=rule$/\1/p'); do
_ri_v=$(uci -q get "mwan3.$_ri_sec.ipset" 2>/dev/null) || continue
for _ri_x in $_ri_v; do
case "$_ri_x" in "" | *[!A-Za-z0-9_]*) continue ;; esac
_ri="$_ri$_ri_x "
done
done
printf '%s\n' "$_ri"
}
_prune_orphan_ipsets() {
[ -f "$CFG_MWAN3" ] || return 0
_po_keep=$(_keep_set_names)
_po_refs=$(_rule_ipset_refs)
_po_changed=0
for _po_sec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=ipset$/\1/p'); do
_po_name=$(uci -q get "mwan3.$_po_sec.name" 2>/dev/null) || continue
[ -n "$_po_name" ] || continue
# 仍被当前启用的行引用:保留
case "$_po_keep" in *" $_po_name "*) continue ;; esac
# 没有标记的段不是本工具写的(或旧版本写的):绝不删
if [ "$(uci -q get "mwan3.$_po_sec.isp_split" 2>/dev/null)" != "1" ]; then
case "$_po_name" in
isp_*)
log "提示:ipset 段 $_po_sec($_po_name)没有 isp_split 标记,保留不动(旧版本或用户自建)"
;;
esac
continue
fi
# 仍被任何 mwan3 规则引用:删了会让 mwan3 渲染失败,只告警
case "$_po_refs" in
*" $_po_name "*)
log "警告:孤儿 ipset 段 $_po_sec($_po_name)仍被 mwan3 规则引用,保留"
continue
;;
esac
if uci -q delete "mwan3.$_po_sec" 2>/dev/null; then
log "已清理孤儿 ipset 段 $_po_sec($_po_name)"
_po_changed=1
else
log "警告:清理 ipset 段 $_po_sec 失败,保留"
fi
done
[ "$_po_changed" = "1" ] && uci -q commit mwan3 2>/dev/null
return 0
}
# 卸载:按 isp_split=1 标记删除所有本工具的 ipset 段。
# 不能按"当前 ISP_TABLE 的名字"删:改名/删除的旧段会漏掉,永远清不掉。
# 无标记的段一律不碰。
_uninstall_ipsets() {
_ui_n=0
for _ui_sec in $(uci -q show mwan3 2>/dev/null |
sed -n 's/^mwan3\.\([^=]*\)=ipset$/\1/p'); do
[ "$(uci -q get "mwan3.$_ui_sec.isp_split" 2>/dev/null)" = "1" ] || continue
uci -q delete "mwan3.$_ui_sec" 2>/dev/null && _ui_n=$((_ui_n + 1))
done
uci -q commit mwan3 2>/dev/null
[ "$_ui_n" -gt 0 ] &&
log "已删除 $_ui_n 个带 isp_split 标记的 ipset 段(含改名/删除的孤儿段)"
return 0
}
cmd_wire() {
_openclash_fakeip_warn
need_nft
@@ -1651,6 +1765,9 @@ cmd_wire() {
cat "$WORK/mwan3.new" > "$CFG_MWAN3"
log "规则已注入(备份: $WORK/mwan3.bak)"
# 5. 收敛孤儿 ipset 段:必须放在规则块重写之后 —— 改名时旧规则
# 这一刻才消失;若在注入前做,旧规则仍引用旧段,会被误判成"保留"。
_prune_orphan_ipsets
/etc/init.d/mwan3 reload
_fallback_counter ||
log "警告:兜底计数规则未安装成功(status 的兜底行会缺失或为旧值),可稍后重跑 isp-split apply"
@@ -1748,8 +1865,9 @@ cmd_verify() {
echo "⚠ DNS 旁路规则缺失(mwan3 重启会清掉运行时插入的规则)"
echo " 恢复:isp-split dns (或 isp-split wire)"
fi
if [ "$(nft list chain inet mwan3 mwan3_rules 2>/dev/null | grep -c "counter name")" -eq 0 ] &&
[ -n "${DNS_TABLE:-}" ]; then
# 兜底计数器由 COUNT_FALLBACK 控制,与 DNS_TABLE 无关(B-2)。
if [ "${COUNT_FALLBACK:-1}" = "1" ] &&
[ "$(nft list chain inet mwan3 mwan3_rules 2>/dev/null | grep -c "counter name")" -eq 0 ]; then
echo "⚠ 兜底计数规则缺失(同上)"
echo " 恢复:isp-split apply"
fi
@@ -1761,6 +1879,23 @@ cmd_verify() {
# ---------------------------------------------------------------- plan (dry-run)
# dnsmasq 是否编译了 nftset(域名→nftset 钉线的前置条件,F-5)。
# 返回:0=支持,1=不支持,2=没有 dnsmasq。
# OpenWrt 基础版 dnsmasq 的编译选项是 no-ipset no-nftset,必须装
# dnsmasq-full 才有。这里只检测并提示,不阻断、不改变任何行为。
_dnsmasq_nftset() {
command -v dnsmasq >/dev/null 2>&1 || return 2
_dn_opts=$(dnsmasq --version 2>/dev/null |
sed -n 's/^Compile time options:[[:space:]]*//p' | head -1)
# 没有 Compile time options 行的老版本无法证明支持,按不支持处理
[ -n "$_dn_opts" ] || return 1
case " $_dn_opts " in
*" no-nftset "*) return 1 ;;
*" nftset "* | *" +nftset "*) return 0 ;;
esac
return 1
}
# 只读预演:不写任何文件、不发任何 uci/nft 改动。
cmd_plan() {
_openclash_fakeip_warn
@@ -1813,7 +1948,7 @@ cmd_plan() {
done
[ "$_dp_any" = "1" ] || echo " (无)"
echo " 说明: 未启用项不会被 apply 写入;残留的 nft 规则会在下次 wire 重写规则块时移除,"
echo " mwan3 ipset 配置段与对应 nft 集合会在 uninstall 时清理。"
echo " mwan3 ipset 配置段(带 isp_split=1 标记)会在下次 wire 与 uninstall 时清理。"
echo
echo "dnsmasq confdir(多线路 DNS 片段写入位置):"
@@ -1853,6 +1988,24 @@ cmd_plan() {
;;
esac
echo
echo "dnsmasq nftset 能力(域名钉线的前置条件;本版本只检测,不启用钉线):"
_dnsmasq_nftset
_nf=$?
case "$_nf" in
0)
echo " ✓ 支持 nftset(dnsmasq 编译选项含 nftset)"
;;
1)
echo " ⚠ 不支持 nftset(dnsmasq 编译选项为 no-nftset 或缺少该选项)"
echo " 域名钉线不可用;多线路 DNS 的 server=/域名/IP@接口 不受影响。"
echo " 需要域名→nftset 时请安装 dnsmasq-full。"
;;
*)
echo " ⚠ 未找到 dnsmasq,无法检测 nftset 能力"
;;
esac
echo
echo "将会写入的 nft 集合声明(每个启用的运营商一个):"
_p3() {
@@ -2173,8 +2326,10 @@ cmd_status() {
grep -oE "packets [0-9]+ bytes [0-9]+" | head -1)
_fp=$(echo "$_fb" | awk '{print $2}')
_fb_b=$(echo "$_fb" | awk '{print $4}')
# 与其余行走同一个 _human 换算,否则这一列是裸字节数、
# 既不对齐也没有单位(B-3)。--json 里的 bytes 保持纯数字。
printf '%-14s %-10s %10s %12s %10s %s\n' \
"兜底" "-" "-" "${_fp:-0}" "${_fb_b:-0}" "未命中集合"
"兜底" "-" "-" "${_fp:-0}" "$(_human "${_fb_b:-0}")" "未命中集合"
fi
echo
echo "注: 命中包/流量为连接首包统计(ct state new;后续包由 conntrack 恢复标记,不再经过规则链)。"
@@ -3035,6 +3190,12 @@ _conf_val() {
' "${2:-$CONF}"
}
# 取配置表的值并删掉注释行。conf_get / apply 直接读文件,不能只依赖入口
# 那次清理(B-1)。必须先整行删注释、再按空白展开,顺序不能反。
_conf_table() {
_strip_comments "$(_conf_val "$1")"
}
conf_get() {
[ -f "$CONF" ] || die "找不到 $CONF"
printf 'dns_mode\t%s\n' "$(_conf_val DNS_MODE)"
@@ -3042,11 +3203,11 @@ conf_get() {
"$(uci -q show mwan3 2>/dev/null | sed -n 's/^mwan3\.\([^=]*\)=policy$/\1/p' | tr '\n' ' ')"
# 值支持两种书写格式:多行(每行一条)或单行空格分隔。
# 先把所有空白折叠成换行,再按冒号拆字段——两种格式统一处理。
_conf_val ISP_TABLE | tr -s ' \t' '\n' |
_conf_table ISP_TABLE | tr -s ' \t' '\n' |
awk -F: 'NF >= 4 { printf "isp\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4 }'
_conf_val DNS_TABLE | tr -s ' \t' '\n' |
_conf_table DNS_TABLE | tr -s ' \t' '\n' |
awk -F: 'NF >= 2 { printf "dns\t%s\t%s\n", $1, $2 }'
_conf_val DOMAIN_SPLIT | tr -s ' \t' '\n' |
_conf_table DOMAIN_SPLIT | tr -s ' \t' '\n' |
awk -F: 'NF >= 2 { printf "domain\t%s\t%s\n", $1, $2 }'
}
@@ -3103,6 +3264,11 @@ _row_check() {
# 直接对一整行做校验(不拆字段的调用点用)
_row_check_row() {
# 双保险(B-1):入口已删注释行,这里再挡一次带前导空白的 # 行,
# 避免将来新增调用点绕过入口清理时又把注释当数据。
case "$1" in
'#'* | [[:space:]]*'#'*) return 0 ;;
esac
_rr_ifs=$IFS
IFS=':'
set -- $1
@@ -3134,6 +3300,12 @@ _valid_ipv4() {
_conf_validate() {
_key=$1
_val=$2
# 注释行不参与校验(B-1):整行删掉后再按空白拆字段。清理必须发生在
# 空白展开之前,否则 "# 说明:xxx" 会被拆成 "#" 和 "说明:xxx" 两段。
# 这里只改局部副本,写回文件的内容仍保留用户注释。
case "$_key" in
ISP_TABLE | DNS_TABLE | DOMAIN_SPLIT) _val=$(_strip_comments "$_val") ;;
esac
case "$_val" in
*'$'* | *'`'* | *'"'* | *"'"* | *'\'* | *';'* | *'|'*)
die "配置值含不允许的字符(\$ \` \" ' \\ ; |)"
@@ -3684,17 +3856,8 @@ cmd_uninstall() {
cat "$WORK/mwan3.new" > "$CFG_MWAN3"
fi
for row in $ISP_TABLE; do
_old_ifs=$IFS
IFS=':'
set -- $row
IFS=$_old_ifs
case "$row" in \#*) continue ;; esac
[ $# -ge 2 ] || continue
_valid_name "$2" || continue
uci -q delete "mwan3.ipset_$2" 2>/dev/null
done
uci -q commit mwan3
# 按标记删,而不是按当前表名:改名/删除的孤儿段也要清掉(B-4)。
_uninstall_ipsets
/etc/init.d/mwan3 reload >/dev/null 2>&1
# 恢复 dnsmasq confdir 原值(状态文件在 $BASE 里,必须先恢复再删目录)