🌴 Sync 2026-10-28 19:42:05

This commit is contained in:
github-actions[bot] committed 2026-10-28 19:42:05 +08:00
1 parent 2d9a92ead9
commit c3e527335a
22 files changed
+7105

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=isp-split
PKG_VERSION:=0.2.1
PKG_RELEASE:=1
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=jihebube <jihebube@users.noreply.github.com>
include $(INCLUDE_DIR)/package.mk
define Package/isp-split
SECTION:=net
CATEGORY:=Network
SUBMENU:=Routing and Redirection
TITLE:=ISP-aware policy routing for mwan3 (nftables)
# 注意:这里故意不写 +mwan3 依赖。
# 官方 feed 的 mwan3 是 iptables 版(2.12.5),写 +mwan3 会错误地拉进它;
# Kwrt 源里 nft 版的包名是 mwan3-nft,与其他源命名不一致。
# nftables 版 mwan3 是硬性前提,由 install.sh 与 router-preflight 检查,
# 并在 description 里说明。
DEPENDS:=+nftables +curl +mwan3-nft
PKGARCH:=all
endef
define Package/isp-split/description
Route traffic by the ISP that owns the destination address.
Builds mutually exclusive nftables sets from daily-updated
Chinese ISP CIDR data and wires them into mwan3-nft rules.
REQUIRES the nftables port of mwan3 (>= 3.6.0):
- Kwrt: apk add mwan3-nft (软件源里自带)
- 上游: 用 https://github.com/dl12345/mwan3 替换官方 feed 的
iptables 版 mwan3 (2.12.5),或从 Release 下载
endef
define Build/Compile
endef
define Package/isp-split/conffiles
/etc/isp-split.conf
endef
# opkg 升级会以 prerm upgrade 调用;apk 的 prerm 只映射到 pre-deinstall。
# 升级时绝不能 uninstall:数据目录、mwan3 规则都必须保留。
define Package/isp-split/prerm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
case "$${1:-}" in
upgrade|pre-upgrade|*upgrade*) exit 0 ;;
esac
if [ -x /usr/sbin/isp-split ]; then
/usr/sbin/isp-split uninstall >/dev/null 2>&1 || true
fi
exit 0
endef
# 卸载后的兜底清理:cron、命名计数器、dnsmasq confdir、数据目录。
# 正常路径由 prerm 的 isp-split uninstall 完成,这里防止二进制先被删掉。
define Package/isp-split/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
case "$${1:-}" in
upgrade|pre-upgrade|*upgrade*) exit 0 ;;
esac
if [ -f /etc/crontabs/root ]; then
# mktemp 而不是 /tmp/.ispcron.$$: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
mv -f "$$_cron_tmp" /etc/crontabs/root 2>/dev/null || rm -f "$$_cron_tmp"
fi
/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
if [ -f "$$_state" ]; then
_orig=$$(cat "$$_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"
fi
# BUG-6:OpenClash 接管时 dnsmasq 实际加载的是 /var/etc/dnsmasq.conf.*
# 里 conf-dir= 指向的目录,必须按同一来源清理,不能只删 /tmp/dnsmasq.d。
for _f in /var/etc/dnsmasq.conf.*; do
[ -f "$$_f" ] || continue
sed -n 's/^[[:space:]]*conf-dir=//p' "$$_f" 2>/dev/null |
while IFS= read -r _line; do
for _d in $$_line; do
_d=$${_d%%,*}
while [ "$$_d" != "/" ] && [ "$${_d%/}" != "$$_d" ]; do
_d=$${_d%/}
done
[ -n "$$_d" ] && rm -f "$$_d/isp-split.conf" 2>/dev/null || true
done
done
done
rm -f /tmp/dnsmasq.d/isp-split.conf /etc/isp-split/dnsmasq.d/isp-split.conf 2>/dev/null || true
rm -rf /etc/isp-split 2>/dev/null || true
exit 0
endef
# 显式指定权限,不依赖源文件的权限位——固件里脚本不可执行会很难查
define Package/isp-split/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_DIR) $(1)/usr/lib/isp-split
$(INSTALL_DIR) $(1)/etc/init.d
$(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_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
endef
$(eval $(call BuildPackage,isp-split))
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh /etc/rc.common
# 开机时把已构建好的集合灌进 nftables。
# 注意:开机不下载数据 —— 网络可能还没通,交给 cron 每日更新。
START=95
STOP=10
start() {
(
i=0
while [ "$i" -lt 60 ]; do
if nft list table inet mwan3 >/dev/null 2>&1; then
/usr/sbin/isp-split apply
# mwan3_output 是 mwan3 重建的,我们插进去的 DNS 旁路规则
# 会随 mwan3 重启丢失。dns 子命令是幂等的,直接重放。
/usr/sbin/isp-split dns >/dev/null 2>&1
exit 0
fi
sleep 1
i=$((i + 1))
done
logger -t isp-split "开机等待 mwan3 超时,未应用运营商集合"
) &
}
stop() {
:
}
+171
View File
@@ -0,0 +1,171 @@
# /etc/isp-split.conf
# openwrt-isp-split 配置文件
# ---------------------------------------------------------------- 多线路 DNS
#
# 为什么需要:只做"按目标 IP 纠线路"治标不治本。如果 DNS 查询本身走的是
# 另一条线,CDN 会返回那条线的节点,你拿着电信节点走联通线,速度差 100 倍。
# 让"从哪条线出去,就用哪条线的 DNS",CDN 才会主动把本运营商节点给你。
# 这是商业方案(如爱快)分流效果更好的关键,原理见 docs/DESIGN.md 的
# 「与商业方案(iKuai)的对比」一节。
#
# 每条线路对应的 DNS:<mwan3策略>:<DNS服务器>
DNS_TABLE="
wan_only:223.5.5.5
wanb_only:119.29.29.29
"
# 多线路 DNS 的「出线方式」
# bind = server=/域名/IP@接口,用 dnsmasq 的 SO_BINDTODEVICE 绑定出接口
# mark = server=/域名/IP + nft 给发往该 DNS 的 53 端口打上对应策略的标
#
# 怎么选:
# · 纯 dnsmasq 环境、没装 OpenClash → bind(更直接)
# · OpenClash 接管了 DNS,或解析器不是 dnsmasq → mark
# (mark 只看目标 IP,不关心是谁发的查询,抗干扰;也不依赖 @接口特性)
DNS_MODE="bind"
# 统计未命中任何运营商集合的兜底流量(status 里显示"兜底"行)
# 规则插在本工具规则块之后,命中集合的流量不会经过它
# 口径:ct state new —— 只计每条连接的**首包**。不加这一条时,若某个流
# 最终没拿到 conntrack 标记(例如 mwan3 的 default_rule_v4 被 enabled=0
# 关闭),同一连接的每个包都会重复计数,数字会按包放大。
COUNT_FALLBACK="1"
# 兜底计数是否排除 DNS(udp/tcp 任一侧端口 53)
# 1 = 排除(默认;出向查询 dport=53 与入向响应 sport=53 都不计)
# 0 = 不排除,保持旧行为
#
# 为什么默认排除:DNS 查询/响应本身不是业务流量——到网关(如 192.168.10.1:53)
# 的查询在本地就被应答,到运营商 DNS 的也只是解析开销。它们占兜底包数
# 比例很高,计进去只会稀释"未命中运营商集合的真实业务流量"这个口径。
# 排除按 UDP/TCP 源或目的端口 53 判断,不写死网关地址,所以无论查的是
# 本地网关还是运营商 DNS,查询和响应都不会被当成业务流量。
COUNT_FALLBACK_SKIP_DNS="1"
# 域名分流:<域名后缀>:<策略>
# 这些域名的解析会被强制从对应线路发出。
# 例如 B 站:联通自有 CDN(cn-*-cu-*)需要联通 DNS 才会解析出来。
DOMAIN_SPLIT="
bilivideo.com:wanb_only
bilibili.com:wanb_only
"
# 可选:外部域名清单目录,$DOMAINDIR/<策略>.txt,每行一个域名。
# 也兼容 felixonmars/dnsmasq-china-list 的 server=/domain/1.2.3.4 格式。
DOMAIN_LIST_DIR=""
# 运营商 IP 段数据源(单源兼容模式;ISP_SOURCES 为空时才用它)
#
# 这里保留 clang 的基址和 <源名>.txt 命名,是为了让 0.1.x 老配置在
# ISP_SOURCES 留空时逐字节保持原行为。默认多源配置见下面的 ISP_SOURCES。
ISP_MIRROR="https://ispip.clang.cn"
# ---------------------------------------------------------------- 多数据源(0.2.1 默认双源)
#
# 格式:每行一条 <名字>:<角色>:<镜像基址>
# 角色 primary 出数据(必须且只能有一个)
# verify 只用来跑日 diff,不参与合成
# audit 低频审计源(同样只跑 diff)
#
# 0.2.1 默认:"主源 + 校验源"(方案 c)
# · 主源换成 gaoyifan/china-operator-ip(MIT,BGP/AS_PATH 口径)
# · clang 只做运行时校验源:默认每天下载、跑 Jaccard diff,不参与合成
# · 校验源连续失败 1-2 天只降级;达到 ISP_SOURCE_MAX_FAIL_DAYS 天则拒绝更新
# · 主源失败立即冻结(保留旧数据),绝不自动换源——换源=换口径
#
# 留空 = 单源兼容模式:仍按上面的 ISP_MIRROR/<源名>.txt 拉取,
# 行为与 0.1.x 完全一致。老配置不需要改任何东西。
#
# 注意:升级时 opkg/apk 会保留用户已有的 /etc/isp-split.conf。这里改的只是
# 新安装的默认模板;已有配置里显式声明的 ISP_SOURCES 不会被覆盖。
ISP_SOURCES="
yfgao:primary:https://china-operator-ip.yfgao.com
clang:verify:https://ispip.clang.cn
"
# 每个源的"文件名映射":<源名>:<ISP_TABLE 里的源名>=<该源上的文件名>,...
#
# 必须显式声明:clang 的文件名是 chinatelecom.txt,yfgao 的同一个运营
# 商叫 chinanet.txt。按名字猜一定会错,所以这里只认声明。
#
# 规则:某个源只要声明了任意一条映射,映射里没列出的运营商默认不下载;
# 若该运营商已启用,主源会按历史命名 <源名>.txt 再试一次(兼容 0.2.0 老配置),
# 文件不存在时 fetch 直接报错。校验源/审计源只认映射,绝不去猜。
#
# yfgao 只有 chinanet/unicom/cmcc/cernet 四类运营商数据,没有
# chinabtn/gwbn/othernet/hk/mo/tw。所以 ISP_TABLE 里后六类默认保持 0(关闭);
# 若要启用它们,需要把主源换成提供这些文件的源,或自行补映射。
ISP_SOURCE_FILES="
yfgao:chinatelecom=chinanet,unicom_cnc=unicom,cmcc=cmcc,cernet=cernet
clang:chinatelecom=chinatelecom,unicom_cnc=unicom_cnc,cmcc=cmcc,cernet=cernet
"
# 是否启用校验源(0 = 不下载不校验;0.2.1 起默认 1)
ISP_VERIFY_ENABLE=1
# 分歧阈值:某个运营商的 Jaccard 低于这个百分比就拒绝本次更新(退出码 3)
# 实测正常值 95.7%-96.9%,口径切错时 64.6%-87.9%,90 是中间带
ISP_DIFF_MAX_JACCARD_DROP=90
# 单运营商地址数日变化上限(%),只在校验源启用时生效
ISP_DIFF_MAX_DELTA_PCT=3
# 校验源连续失败多少天后,从"降级继续"升级为"拒绝更新"
ISP_SOURCE_MAX_FAIL_DAYS=3
# 运营商表,格式:<源文件名>:<nft集合名/规则名>:<mwan3策略>:<是否启用>
#
# 顺序即优先级:排在前面的运营商优先占有重叠地址。
# 同一个 IP 只会出现在一个集合里,所以调整这里的顺序会改变归属,
# 但不会出现"规则先后决定归属"的隐性行为。
#
# 集合名同时也是 mwan3 规则名,mwan3 限制规则名 <=15 字符,
# 且不能以 mwan3_ 开头(该前缀为 mwan3 内部保留)。
#
# 策略名必须已在 mwan3 中定义,可用 `mwan3 status` 或
# `uci show mwan3 | grep policy` 查看。
ISP_TABLE="
chinatelecom:isp_tel:wan_only:1
unicom_cnc:isp_uni:wanb_only:1
cmcc:isp_cmcc:wanb_only:1
cernet:isp_cernet:wan_only:1
chinabtn:isp_tietong:wanb_only:0
gwbn:isp_gwbn:balanced:0
othernet:isp_other:wanb_only:0
hk:isp_hk:wanb_only:0
mo:isp_mo:wanb_only:0
tw:isp_tw:wanb_only:0
"
# 关于 othernet 默认关闭(实测教训):
# othernet 是"三大运营商以外"的集合(华为云、腾讯云、各类 IDC、
# 以及**公共 DNS** 如 223.5.5.5 / 119.29.29.29 都可能落在这里)。
# 启用它之后,路由器自己的 DNS 查询会被分到这条线上——如果这条线
# 恰好不可用,整个域名解析就挂了(实测:启用后所有域名解析失败)。
# 要启用它,先确认你的 DNS 服务器不属于该集合,或者给策略配好备份成员。
# 关于 cmcc(移动)默认走联通的实测依据:
# Steam 移动 CDN(39.134.x)实测 联通 22ms / 电信 72ms,联通明显更好,
# 而 Steam 下载是典型大流量场景,故默认给联通。
#
# 已知例外:B 站 PCDN(*mcdn.bilivideo.cn)虽然是移动段,
# 实测 电信 12-22 MB/s 而联通只有 0.04-1 MB/s。若你常用 B 站且命中 mcdn,
# 应改用域名/网段级覆盖,而不是把 cmcc 整体改回电信——
# B 站自有联通 CDN(cn-*-cu-*)恰恰需要走联通才快。
# 规则插入位置:在第一个匹配该正则的 mwan3 rule 之前插入运营商规则。
# 默认插在按端口/兜底规则之前、按设备/IP 的规则之后。
ISP_ANCHOR='dest_port|src_port|default_rule|https|internet'
# 每日自动更新(crontab 时间)
ISP_CRON="17 4 * * *"
# 分线测速用的接口与探测目标
PROBE_IFACES="pppoe-wan1 pppoe-wan2"
PROBE_BYTES=33554432
PROBE_TIME=25
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
"
@@ -0,0 +1,26 @@
#!/bin/sh
# 首次启动初始化 —— 用于把本包直接编译进固件的情况
#
# 为什么需要:打进固件时,opkg/apk 的 postinst 在镜像构建阶段就被跳过
# (IPKG_INSTROOT 非空),所以 /etc/rc.d 的自启软链不会自动建立。
# OpenWrt 会在首次启动时执行 /etc/uci-defaults/ 下的脚本,就在这里补上。
[ -x /usr/sbin/isp-split ] || exit 0
/etc/init.d/isp-split enable >/dev/null 2>&1
# 启用每日更新(只改数据,不重写 mwan3 配置)
if [ -x /etc/init.d/cron ]; then
mkdir -p /etc/crontabs
touch /etc/crontabs/root
if ! grep -q 'isp-split' /etc/crontabs/root 2>/dev/null; then
echo "17 4 * * * /usr/sbin/isp-split refresh >/dev/null 2>&1 # isp-split" \
>> /etc/crontabs/root
fi
/etc/init.d/cron enable >/dev/null 2>&1
/etc/init.d/cron restart >/dev/null 2>&1
fi
logger -t isp-split "固件内置初始化完成:已开自启与每日更新;运行 isp-split plan 进行预演"
exit 0
@@ -0,0 +1,42 @@
# 找出不合法的 CIDR 行(校验规则与 cidr2range.awk 保持一致)
# 输出格式:<行号>: <原始内容>
{
line = $0
sub(/#.*/, "", line)
gsub(/[ \t\r]/, "", line)
if (line == "")
next
n = split(line, p, "/")
if (n != 2) {
print NR ": " $0
next
}
alen = split(p[1], q, ".")
if (alen != 4) {
print NR ": " $0
next
}
# 与 cidr2range.awk 保持一致:前缀必须是纯数字,不能靠 +0 转换
if (p[2] !~ /^[0-9]+$/) {
print NR ": " $0
next
}
len = p[2] + 0
if (len < 0 || len > 32) {
print NR ": " $0
next
}
bad = 0
for (i = 1; i <= 4; i++) {
if (q[i] !~ /^[0-9]+$/)
bad = 1
else if (q[i] + 0 > 255)
bad = 1
}
if (bad)
print NR ": " $0
}
@@ -0,0 +1,58 @@
#!/bin/sh
# 数据管线:原始 CIDR -> 归一化 -> 按优先级互斥化 -> 每运营商一个 CIDR 文件
#
# 用法: build.sh <rawdir> <outdir> <优先级列表>
# rawdir : 含 <isp>.raw 的目录,每行一个 CIDR
# 优先级 : 空格分隔的运营商名,排在前面的优先占有重叠地址
#
# 设计要点:
# 1. 每个集合内部先合并去重(RFC1918 无关,只做区间合并)
# 2. 逐级做差集:后一个集合要扣掉所有前面已占用的地址
# -> 保证同一 IP 只出现在一个集合里,规则顺序不再影响归属
# 3. 全程只依赖 awk + sort,无 Python、无 ipcalc
set -eu
RAW=$1
OUT=$2
PRIORITY=$3
LIB=${ISP_SPLIT_LIB:-$(dirname "$0")}
WORK=$OUT/.work
mkdir -p "$OUT" "$WORK"
: > "$WORK/claimed"
printf '%-16s %8s %8s %14s\n' "运营商" "原始" "互斥后" "地址数" > "$WORK/report"
for isp in $PRIORITY; do
src=$RAW/$isp.raw
[ -f "$src" ] || {
printf '%-16s %8s\n' "$isp" "缺失" >> "$WORK/report"
continue
}
# 1. CIDR -> 区间,排序,合并
awk -f "$LIB/cidr2range.awk" "$src" |
sort |
awk -f "$LIB/merge.awk" > "$WORK/$isp.ranges"
# 2. 扣掉高优先级运营商已占用的地址
awk -v claimed="$WORK/claimed" -f "$LIB/exclude.awk" "$WORK/$isp.ranges" > "$WORK/$isp.excl"
# 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"
# 统计
n_raw=$(awk -f "$LIB/cidr2range.awk" "$src" | wc -l | tr -d ' ')
n_out=$(wc -l < "$OUT/$isp.txt" | tr -d ' ')
n_addr=$(awk '{ split($0, p, "/"); t += 2 ^ (32 - (p[2] + 0)) } END { printf "%.0f", t }' "$OUT/$isp.txt")
printf '%-16s %8s %8s %14s\n' "$isp" "$n_raw" "$n_out" "$n_addr" >> "$WORK/report"
done
cat "$WORK/report" >&2
@@ -0,0 +1,59 @@
# CIDR -> 数值区间
# 输入:每行一个 CIDR(允许 # 注释、行尾空白)
# 输出:每行 "<起始> <结束>",均为零填充的 10 位定宽数字
#
# 为什么零填充:
# OpenWrt 的 busybox sort 对大数执行 sort -n 时并不按数值排序
# (实测 3232235520 会排在 134744064 之前,等于没排序)。
# 把数值零填充到 10 位后,普通字典序 == 数值序,任何平台都成立。
#
# 纯算术实现,不使用位运算,兼容 busybox awk / mawk / gawk
function pad(n, s) {
s = sprintf("%.0f", n)
while (length(s) < 10)
s = "0" s
return s
}
{
line = $0
sub(/#.*/, "", line)
gsub(/[ \t\r]/, "", line)
if (line == "")
next
n = split(line, p, "/")
if (n != 2)
next
alen = split(p[1], q, ".")
if (alen != 4)
next
# 前缀必须是纯数字;否则 "abc"/"" 会被 +0 当成 0,单行就吞掉整个 IPv4
if (p[2] !~ /^[0-9]+$/)
next
len = p[2] + 0
if (len < 0 || len > 32)
next
bad = 0
for (i = 1; i <= 4; i++) {
if (q[i] !~ /^[0-9]+$/)
bad = 1
else if (q[i] + 0 > 255)
bad = 1
}
if (bad)
next
ip = q[1] * 16777216 + q[2] * 65536 + q[3] * 256 + q[4]
if (ip < 0 || ip > 4294967295)
next
blk = 2 ^ (32 - len)
s = int(ip / blk) * blk
e = s + blk - 1
print pad(s), pad(e)
}
@@ -0,0 +1,55 @@
# 区间差集:target 减去 claimed
# 用法:awk -v claimed=<已占用区间文件> -f exclude.awk < target
# claimed:已按字典序排好的互不重叠区间 "<起始> <结束>"(零填充定宽)
# target :同样已排序去重叠的区间(stdin)
# 输出:target 中未被 claimed 覆盖的部分(零填充、升序、互不重叠)
# 目的:让同一个 IP 只归属一个运营商集合,消除归属歧义
function pad(n, s) {
s = sprintf("%.0f", n)
while (length(s) < 10)
s = "0" s
return s
}
BEGIN {
if (claimed == "") {
print "exclude.awk: 缺少参数 -v claimed=<已占用区间文件>" > "/dev/stderr"
exit 2
}
nc = 0
while ((getline l < claimed) > 0) {
n = split(l, f, " ")
if (n < 2)
continue
nc++
cs[nc] = f[1] + 0
ce[nc] = f[2] + 0
}
close(claimed)
ci = 1
}
{
s = $1 + 0
e = $2 + 0
if (s > e)
next
# 跳过完全位于 s 之前的已占用区间
while (ci <= nc && ce[ci] < s)
ci++
cur = ci
while (cur <= nc && cs[cur] <= e) {
if (cs[cur] > s)
print pad(s), pad(cs[cur] - 1)
if (ce[cur] + 1 > s)
s = ce[cur] + 1
if (s > e)
break
cur++
}
if (s <= e)
print pad(s), pad(e)
}
@@ -0,0 +1,102 @@
# 两个运营商集合的三段差异:仅主源 / 仅校验源 / 共同,并计算 Jaccard
#
# 用法:
# awk -v vf=<校验源区间文件> [-v outp=<仅主源区间输出>] [-v outv=<仅校验源区间输出>] \
# -f ispdiff.awk <主源区间文件>
#
# 输入格式:两边的区间文件都必须是「每行 <起始> <结束>,升序、互不重叠」,
# 也就是 cidr2range.awk | sort | merge.awk 的输出(零填充十位定宽)。
# 输出(stdout 一行六列):
# <主源地址数> <校验源地址数> <共同> <仅主> <仅校验> <Jaccard 百分比>
#
# 为什么用扫描线而不是逐条 CIDR 比对:
# 运营商集合动辄上千段,"每段对每段"是 O(n*m);排序后双指针是 O(n+m),
# 在 4 千行量级一次 diff 只要几十毫秒,路由器上完全无感。
#
# 注意:awk 函数里除第一个参数外多写的形参才是局部变量,这里用显式形参
# (i/j/k/s/e 都是全局的,只用一次,不影响正确性)。
BEGIN {
nv = 0
while ((getline line < vf) > 0) {
if (line == "") continue
split(line, a, " ")
if (a[1] == "" || a[2] == "") continue
nv++
vs[nv] = a[1] + 0
ve[nv] = a[2] + 0
}
close(vf)
np = 0
}
{
if ($1 == "" || $2 == "") next
np++
ps[np] = $1 + 0
pe[np] = $2 + 0
}
END {
ptot = 0
for (i = 1; i <= np; i++) ptot += pe[i] - ps[i] + 1
vtot = 0
for (i = 1; i <= nv; i++) vtot += ve[i] - vs[i] + 1
# 交集长度(双指针)
common = 0
i = 1
j = 1
while (i <= np && j <= nv) {
s = (ps[i] > vs[j]) ? ps[i] : vs[j]
e = (pe[i] < ve[j]) ? pe[i] : ve[j]
if (s <= e) common += e - s + 1
if (pe[i] < ve[j]) i++
else if (ve[j] < pe[i]) j++
else { i++; j++ }
}
# 仅主源区间(主源减去校验源)
if (outp != "") {
j = 1
for (i = 1; i <= np; i++) {
s = ps[i]
e = pe[i]
while (j <= nv && ve[j] < s) j++
k = j
while (k <= nv && vs[k] <= e) {
if (vs[k] > s) print s " " (vs[k] - 1) > outp
if (ve[k] + 1 > s) s = ve[k] + 1
if (s > e) break
k++
}
if (s <= e) print s " " e > outp
}
close(outp)
}
# 仅校验源区间(校验源减去主源)
if (outv != "") {
i = 1
for (j = 1; j <= nv; j++) {
s = vs[j]
e = ve[j]
while (i <= np && pe[i] < s) i++
k = i
while (k <= np && ps[k] <= e) {
if (ps[k] > s) print s " " (ps[k] - 1) > outv
if (pe[k] + 1 > s) s = pe[k] + 1
if (s > e) break
k++
}
if (s <= e) print s " " e > outv
}
close(outv)
}
if (ptot + vtot - common > 0)
jac = common * 100 / (ptot + vtot - common)
else
jac = 100
printf "%.0f %.0f %.0f %.0f %.0f %.2f\n", ptot, vtot, common, ptot - common, vtot - common, jac
}
@@ -0,0 +1,45 @@
# 合并重叠/相邻区间
# 输入:已按字典序排好的 "<起始> <结束>"(零填充定宽,字典序 == 数值序)
# 输出:互不重叠、已合并的区间,同样零填充
#
# 注意:不能依赖 sort -n —— busybox 的 sort -n 对大数不生效。
# 输入已经是零填充格式,普通 sort 即可得到正确数值序。
function pad(n, s) {
s = sprintf("%.0f", n)
while (length(s) < 10)
s = "0" s
return s
}
BEGIN {
have = 0
}
{
s = $1 + 0
e = $2 + 0
if (s > e)
next
if (!have) {
cs = s
ce = e
have = 1
next
}
# 与当前合并段重叠或正好相邻
if (s <= ce + 1) {
if (e > ce)
ce = e
next
}
print pad(cs), pad(ce)
cs = s
ce = e
}
END {
if (have)
print pad(cs), pad(ce)
}
@@ -0,0 +1,42 @@
# 数值区间 -> 最小 CIDR 集合
# 输入:每行 "<起始> <结束>"(升序、互不重叠)
# 输出:每行一个 CIDR
#
# 注意:awk 函数里除第一个参数外,多写的形参才是局部变量。
# int2ip(n, a,b,c,d) 中的 a,b,c,d 若不加在形参列表里就会变成全局变量,
# 直接污染外层的块大小 b,导致死循环。这是本项目踩过的一个坑。
function int2ip(n, a, b, c, d) {
a = int(n / 16777216) % 256
b = int(n / 65536) % 256
c = int(n / 256) % 256
d = n % 256
return a "." b "." c "." d
}
function log2(x, r) {
r = 0
while (x > 1) {
x = x / 2
r++
}
return r
}
{
s = $1 + 0
e = $2 + 0
if (s > e)
next
while (s <= e) {
# 找出以 s 为起点、长度不超过 e 的最大 2 的幂块
blk = 1
while (s % (blk * 2) == 0 && s + blk * 2 - 1 <= e)
blk = blk * 2
if (blk < 1)
blk = 1
printf "%s/%d\n", int2ip(s), 32 - log2(blk)
s = s + blk
}
}
@@ -0,0 +1,115 @@
# 把运营商分流规则块插入 /etc/config/mwan3 的正确位置
#
# 为什么不直接 uci add:
# mwan3 按配置文件中的顺序渲染 nft 规则链(first-match),而 uci add 只能追加到末尾。
# 追加的规则必然排在 default_rule 之后,永远匹配不到 —— 等于白装。
#
# 用法:
# awk -v insert=<规则片段文件> -v anchor=<锚点正则> -f rules-splice.awk /etc/config/mwan3
#
# 行为:
# 1. 只删掉带精确标记 option isp_split '1' 的本工具规则块
# (不再按 isp_ 前缀或注释里的 isp_split 字符串判定,避免误删用户规则)
# 2. 在第一个匹配 anchor 的 rule 块之前插入 insert 文件内容
# 3. 找不到锚点且 insert 非空时,输出警告到 stderr 并以 3 退出,
# 绝不静默追加到链尾(追加在 catch-all 之后永远不命中)
#
# 注意:awk 函数除首参外多写的形参才是局部变量,务必写全,否则污染全局。
function blocktext(i, e, j, t) {
e = (i < nb) ? bs[i + 1] - 1 : NR
t = ""
# 只拼接块内的 option/list 行。块间注释、空行不能算进前一个块,
# 否则一段含 isp_split 字样的注释就会把上面的用户规则误判成我们的。
for (j = bs[i]; j <= e; j++)
if (line[j] ~ /^[[:space:]]*(option|list)[[:space:]]/)
t = t " " line[j]
return t
}
function is_ours(i, t) {
if (bt[i] != "rule")
return 0
t = blocktext(i)
if (t ~ /option[[:space:]]+isp_split[[:space:]]+['"]1['"]/)
return 1
return 0
}
function warn_no_anchor() {
print "rules-splice.awk: 找不到锚点 '" anchor "',拒绝把规则追加到链尾(inserted=0)" > "/dev/stderr"
}
BEGIN {
if (insert == "") {
print "rules-splice.awk: 缺少参数 -v insert=<规则片段文件>" > "/dev/stderr"
exit 2
}
# 先读入待插入片段,顺便判断是否为空(卸载时 insert 为空是合法的)
ni = 0
while ((getline l < insert) > 0)
ins[++ni] = l
close(insert)
}
{
line[NR] = $0
if ($1 == "config") {
nb++
bs[nb] = NR
bt[nb] = $2
bn[nb] = $3
}
}
END {
for (i = 1; i <= nb; i++) {
gsub(/['\''"]/, "", bt[i])
gsub(/['\''"]/, "", bn[i])
}
# 找锚点:第一个不是我们的、且匹配 anchor 的 rule 块
aidx = 0
for (i = 1; i <= nb && aidx == 0; i++) {
if (bt[i] != "rule")
continue
if (is_ours(i))
continue
# blocktext 只含 option/list 行;section 名(如 default_rule_v4)
# 也要参与锚点匹配,否则默认锚点里的 default_rule 会失效。
if (blocktext(i) ~ anchor || bn[i] ~ anchor)
aidx = i
}
if (nb == 0) {
if (ni > 0) {
warn_no_anchor()
exit 3
}
exit 0
}
# 文件头(第一个 config 之前的内容)
for (j = 1; j < bs[1]; j++)
print line[j]
inserted = 0
for (i = 1; i <= nb; i++) {
if (is_ours(i))
continue
if (!inserted && i == aidx) {
for (k = 1; k <= ni; k++)
print ins[k]
inserted = 1
}
e = (i < nb) ? bs[i + 1] - 1 : NR
for (j = bs[i]; j <= e; j++)
print line[j]
}
# 没有锚点:insert 为空(卸载)时原样输出;非空时拒绝追加
if (!inserted && ni > 0) {
warn_no_anchor()
exit 3
}
}
File diff suppressed because it is too large. Load diff