1.41.1
- 【修复·严重】**包在官方安装路径上装不上**：插件市场列表里看不到、也不会设开机自启。
  真因：插件市场的安装器把整条SQL当**一个命令行参数**交给sqlite3，而Linux对单个参数
  有硬上限（单参数字符串131072字节，超了execve直接失败）。我们的readme+changelog
  经base64后有145KB，于是sqlite3根本没跑起来，安装器只报
  「failed to write appmarket.db」——包解开、文件全就位、就是不注册，极具迷惑性。
  实测判据：同一台设备上，140000字节的单参数报「Argument list too long」，100000字节正常。
  修法：**包内只带最近的版本记录**（完整历史留在仓库），并加打包闸门核对SQL payload
  长度（base64后的readme+changelog+描述 必须 < 120000字节，留安全余量）。
- 【修复】`.pkg/manifest.json` 是安装时写入的注册副本，此前所有安装都是手工拷贝目录，
  **绕过了注册这一步**：设备上它一直停在旧值，插件市场显示的还是旧名字，插件也不在
  applist里。走官方安装器（naixi install）时它才会按包内manifest正确写入「应用层防火墙」。
- 【教训】"文件都在"不等于"装上了"。这个故障只有走官方安装器才暴露 ——
  此前每一轮验证都是手工拷贝，于是它一路漏到了现在。
- 【更正】本条目曾把原因记为"changelog里的ASCII单引号破坏SQL拼接"，那是错的：
  readme/changelog入库前会base64，引号进不了语句。真因是参数长度。
  保留这条更正，是因为"错的结论比没有结论更危险"——它会让人去改不相干的地方。

1.41.0
- 【新增·核心】**SNI与IP一致性校验**（用户第4点）。客户端在握手时自报域名（SNI），而它
  实际连的是自己解析到的地址 —— 两者本该一致。借域名握手那一类隧道（VLESS Reality的
  典型形态：借真站域名完成握手、却把连接指向自己的服务器）会在这里露出马脚。
  实现要点：
  · **旁路**，绝不进流量路径：观察只入队（永不阻塞，队列满就丢计数），解析由后台按限速
    发出。查DNS会阻塞且是外发流量，放在转发路径上就是断网。
  · 结果带TTL缓存（含失败缓存：查不到的域名不反复重试，每次重试都是一次外发流量）；
    缓存有上限并按最久未更新淘汰 —— 只写不淘汰会被历史域名占满，新流量全部查不到。
  · 结论有三种，且 **unknown必须与match分开**：解析还没回来时如实报"尚未校验"。
    把"还没查"显示成"正常"，等于把判据变成一张永远通过的检查表。
  · **默认关闭**：这是本插件唯一会主动外发DNS查询的功能，插件不替操作者决定要不要
    产生这类流量。开关走设置这条唯一路径落盘（否则重启后开关丢了、面板却显示开着）。
  · 内网/保留地址不查（在这些目标上的"不一致"没有意义，且白白外发查询），并单独计数，
    便于看出这条判据对多少流量根本用不上。
  · **把判据的边界随数据一起显示**：抓不到用真证书的代理（经典Trojan用自己的域名+真证书，
    解析结果与实际目标一致）；CDN/Anycast/多线解析会让正常站点也出现不一致。所以它输出的
    是**线索**，不是结论，落在面板上是"待人工复核"的一列。
  测试11条：一致/不一致/未查到不等于一致/内网不查/未启用不发查询/失败缓存不重试/TTL到期
  重查/并发去重/绝不阻塞/地址归一化/状态可解释。
- 【新增】面板：实时流表格加「SNI」列（不一致时给出两边地址与判据说明，供人工判断）；
  系统设置加「SNI与IP一致性校验」卡片（开关、状态、判据边界、不一致明细、清空）。
- 【新增】`/api/v1/sni-mismatch`（GET状态与明细 / POST开关 / DELETE清空）。
- 【新增】`/api/v1/metrics`：效率与性能指标的**接口**（按产品要求本轮只给接口、不设计页面）。
  引擎状态与运行时长、速率（每秒裁决数/拦截数，由后台定时采样算，不由接口现算 —— 接口是
  被人轮询的，间隔不固定，算出来的"每秒"没有统一口径）、拦截率（**分母是已裁决流数**，
  不是总包数：总包数含SYN/FIN/重传，用它会重复计数同一连接，这个口径写在返回值里）、
  资源占用（CPU滴答、RSS、协程数、打开的文件数）、协议命中分布、**规则命中排行**
  （哪条规则真在起作用；引擎侧计数有上限，规则名由操作者提供、可以很多，无限增长的map
  在常驻进程里就是泄漏）。
- 【修复】规则命中计数补上了：此前只有"按协议"的计数，无法回答"我写的这条规则到底有没有
  在命中"。

1.40.0
- 【新增·核心】**Shadowsocks与VMess的家族判定**（用户要的"尽可能区分加密代理"）。
  判据来自协议结构本身，不是拍脑袋的阈值：SS-AEAD首包 = salt(16或32) + 2字节长度 +
  密文 + 16字节tag，首块明文是地址头（域名目标5~257字节）→ 首包 ∈ [39,307]；
  VMess legacy请求头明文 = 79 + 地址 + padding，线上再套16字节IV → 首包 ∈ [99,415]。
  于是**两段区间的两端干净可分**：首包 ≤98只能是SS家族，≥308只能是VMess家族，
  **99~307之间不报任何家族** —— 把中间地带硬分给某一方，等于给出一个看起来确定、
  实际靠猜的结论，而它会被写进拦截规则。已知边界写在代码里：VMess自v4.28起默认
  用AEAD头，其线上格式与SS同形，短包无法区分。两条都标experimental，只用于观察
  与人工判断。
- 【修复·重要】**分析器"报出顺序"就是优先级**，这一点之前理解错了。引擎是"每个分析器
  报出属性 → 立刻求值规则 → 首个命中即定案"，所以**谁先报出属性谁就先定义这条流**。
  泛化的fet在列表最前面、且在第一个包就报"全加密"，于是兜底规则抢先定案，VLESS
  分析器根本来不及开口 —— 这才是"VLESS显示成加密代理"的真因（规则里写排除项也救不了：
  排除项要求vless == nil，而那一刻vless确实还没被报出来）。现在分析器列表按
  "具体在前、泛化兜底在后"重排，并在代码里写明顺序即优先级。
- 【修复】家族分析器一度"不命中时也报属性"，代价很大：① 规则里的排除项永远不成立，
  兜底类目再也接不住流量；② 面板取"这条流是什么"用的是属性是否存在，于是每条TCP流
  都被标成ssaead，连SSH都被改名。现在改成只在命中时报出，与引擎里其它分析器一致。
- 【修复】兜底类目（加密代理）的判据补上排除项：它现在真的只接"未识别出具体协议的
  全加密流量"。排除项不会让兜底漏东西：分析器只在规则引用它时才加载，所以具体类目被
  停用时该属性为nil、排除项成立、兜底照样接住。
- 【修复】测试夹具（benchAnalyzers）曾是另抄的一份分析器清单，抄漏了VLESS与新分析器
  —— 端到端测试测的是"一套分析器"、线上跑的是"另一套"，而且抄漏不报错。现在夹具直接
  派生自生产清单。另加测试钉住"注册的分析器"与"规则校验允许的名字"两边一致（任一边
  漏了都会静默失效：要么规则写不出来，要么规则永不命中）。
- 【改进】面板协议清单按后端优先级重排，VMess / Shadowsocks从"旧版"升级为一等公民
  （它们现在有真实判据），兜底类目标注为兜底；自定义表达式的可用名列表补全。
1.39.0
- 【修复·重要】**VLESS被当成"全加密流量"认不出来**。规则顺序错了：泛化的encproxy
  （fet.yes）排在enctunnel（vless != nil）之前，引擎先命中泛化规则，于是VLESS永远
  显示成"加密代理"——明明引擎能认出它（VLESS请求头是明文，连目标域名都读得出来）。
  规则顺序在本引擎里**是判据的一部分**（按顺序求值、先命中者为准），所以泛化兜底必须
  排在具体协议之后，并且带排除项。已加测试钉住这个顺序。
- 【改进】尽可能区分加密代理，并把边界说清楚：能精确判定的（VLESS、Trojan、SOCKS、
  SSH、WireGuard、OpenVPN）各有专用分析器；判不准的（VMess请求头是加密的、
  Shadowsocks是纯随机字节）无法在DPI层区分，因此用同一个排除式表达、说明文字如实写成
  「无法区分VMess与Shadowsocks」——两条规则写不同名字却宣称能分清，才是真会误导人的。
  计数侧也修了：analyzerOf里fet曾排在tls之前、且**根本没有vless**，导致VLESS
  流被计入fet（这是"全加密流量"计数的另一半原因）。
- 【改进】**引擎日志改为全英文**：排查要能按关键字搜索、能直接贴进工单。面板「引擎日志」
  页读的就是它。流量记录（含命中规则详情）保持中文——那是给人看的，不是给grep看的。
- 【修复】面板会说"拦截当前是关闭的，所以不会有流量记录"，而引擎明明在运行、也能抓到包。
  判据错了：原来判的是保存下来的开关值，而真正决定有没有记录的是**引擎在不在跑**。
  现在按引擎状态判断，并区分两种情况：开关开着但引擎没起来（异常，去系统设置看原因）
  与开关确实关着（提示怎么打开）。
- 【新增】**DNS投毒的面板入口**。后端一直支持（dns_poisoning / 指定IPv4与IPv6），
  规则也一直会渲染，但面板上**没有地方填**——所以这条路等于不可用。现在系统设置里有了：
  开关、IPv4/IPv6两个地址、以及两个一键取值（黑洞0.0.0.0与::；回环127.0.0.1与::1）。
  黑洞是标准做法：客户端立即失败，且不会再去试别的地址；这条路不依赖任何页面或服务，
  也是之前那件"DNS劫持/跳转该怎么做"最省事的答案。
- 【改进】「内核功能开关」只在调试模式（日志级别debug）下显示。它是排查"某个列表为什么
  是空的"用的，平时挂在页面上只是噪声。

1.38.1
- 【修复·重要】**装个新版会把面板规则和流量日志全部清掉**。这些文件原来写在app/data下，
  而安装/升级会整目录替换 .pkg/app —— 数据还在，只是随着旧目录一起被删了，且不报任何错。
  现在用户数据（面板规则、自定义应用规则、流量留存）与config.yaml、rules.yaml放在一起
  （/etc/log/naixifw，持久分区），升级不再碰它。启动时会自动把旧位置里**有内容**的文件
  迁移过来（空文件不算数据，不参与"谁覆盖谁"的判断）。
- 【修复】0字节的panel-rules.json会被当成"文件损坏"而使整个规则页报错。空文件其实是
  "还没有任何规则"的合法状态，现在被容忍（内容真正损坏时仍然报错，不会悄悄清空规则）。
  这一点直接影响自救：面板是规则配错后唯一能改回来的入口，它不该因为一个空文件打不开。

1.38.0
- 【修复】**详情里的「检测内容」原来是黑底**。它用的是 .mono-block，而那个类被写成了深色
  代码块（深底浅字）—— 放进内容区就是浅色页面里的一块黑。已改为浅色（浅底、深字、细边框），
  规则原文与配置原文同样受益。
- 【修复·重要】**应用分析的实时流里「客户端/端口/目标/端口」四列永远是空的**。原因是数据源
  选错了：那四列来自内核的识别缓存，而内核缓存一行只有**一个**地址（`IP port appid
  priority proto`），从来不是"客户端 → 目标"一对，所以结构上就填不出来。现在实时流改用
  **引擎自己的记录**（完整五元组 + 协议 + 动作 + 时间），四列真的有了；内核缓存另立一栏
  「内核识别缓存」，并写明它只有单侧地址。前端类型AppFlowRow也一并改成后端真实的字段
  （原来写成client_ip/target_ip，与后端完全不是一回事 —— 这正是那四列空着的前端那一半）。
- 【合并】**「应用分析」与「应用管控」合成一页「应用」**。两页讲的是同一个对象（应用），
  只是动作不同：看哪个在跑vs拦不拦它。分成两页的代价是——在分析页看到一个该拦的应用，
  得切到另一页再找一遍。现在同一页里「看到就能拦」（页签：实时流 / 管控 / 主机 / 命中排行）。
- 【改进】左侧导航底部不再重复「引擎运行中」—— 顶栏已经有一个一模一样的芯片，同一句话在
  同一屏出现两次等于噪声。侧栏改显示**拦截是否开着**与版本号（拦截是改变行为的开关，
  引擎是否在跑只是结果）。
- 【修复】**右上角时钟不会自动更新、也没有日期**。原来它只在30秒轮询里顺手更新一次
  （看着像卡住），而且只显示"17:11:49"。现在每秒自走，并显示完整日期时间。
- 【新增】**规则来源：独立（FAT）/ 同步（FIT）**，参考爱快AP的两种形态。独立 = 每台
  路由器自己在面板上配（原来的行为）；同步 = 规则由上级统一下发，各台按时拉取 ——
  多台设备要保持一样的拦截行为时，不用一台台点。
  · 配置：控制器地址（只允许http或https，内网地址可用）、间隔（默认30分钟）、访问令牌
    （Authorization: Bearer）、「立即同步」（第一次必须能当场验证拉得通，不能等定时器）。
  · 三条安全契约：**拉不到 / 解析不了 / 校验不过，一律保留上一份已经生效的规则**
    （绝不因为一次网络抖动就把规则清空 = 全网放行）；**先校验后落盘**（引擎拒绝规则的
    方式是它自己起不来，代价是整网）；**内容没变不重写闪存**（路由器上是持久分区）。
  · 不跟随重定向（以root身份执行的请求，不能让别人用302指定去哪）；响应体积上限3MB。
  · 状态可见：上次同步时间 / 结果 / 条数 / 失败原因都写在面板上，同步模式时规则页顶部有
    横幅说明"这里改的会被覆盖"，避免改完才发现白改。

1.37.1
- 【修复·重要】**协议类目的键在前端与后端不一致**，导致"删除"被拒、而"生效终端"静默失效。
  前端写的是含下划线的enc_proxy / enc_tunnel，渲染时用的却是encproxy / enctunnel，
  于是：点「删除加密代理」报「未知的协议类目enc_proxy」（你说"这样是不对的"，就是它）；
  而把生效终端配在这两类上，键对不上、按空处理 —— 规则看起来配好了，实际对全屋生效。
  现在键只认一份（spec.ProtoKeys），历史写法做归一：已经存进配置的老键会自动修正，
  不必重新删一遍。
- 【新增】**协议支持自定义表达式**（像Cloudflare的自定义规则）。规则类型里多了
  「协议表达式（自定义）」：自己写匹配表达式，动作可选 拦截 / 仅记录 / 放行（放行用来
  做例外），并可限定生效终端与生效时段。表达式在**保存时**用引擎的真解析器编译一遍 ——
  引擎拒绝规则的方式是自己起不来，那时错误会以"引擎没运行"的形式出现，排查方向完全错。
  弹窗里给了可用的名字（就是分析器：dns / tls / http / quic / fet / trojan / ssh / socks /
  vless / wireguard / openvpn / tailscale / p2p）与字段、函数清单。
- 【改名】界面上显示的「奶昔防火墙」改为「应用层防火墙」（新面板顶栏与标题、旧面板标题与
  停止横幅、多语言词条、插件注册名display_name）。其余不变。
- 顺带把动作词表钉对：自定义协议规则里"放行"渲染成引擎认识的allow。
- 测试：新增协议键归一（含"老键删除要真的生效""老键生效终端要真的限制住"）、
  自定义协议规则渲染（三种动作、停用不渲染、非法时段按全天、生成结果必须可编译）。

1.37.0
- 【修复·重要】**流量日志的「实时」从来不工作**，三个原因叠在一起：
  ① 实时流用的是浏览器自带的EventSource，而它**无法携带认证信息** —— 接口在
  HTTP Basic之后，EventSource拿到的是401，页面只显示"连接中/已断开"；
  ② 服务端在同一路流上发两种事件（引擎日志log、流量记录audit），而前端**只监听
  log**，所以就算连上了也看不到任何一条流量记录；③ 拦截关闭时引擎不运行，本来就
  没有记录，而页面不会说明这一点，只显示一张空表。
  现在：改用能带凭据的fetch流、两种事件都监听、状态区分"连接中/已认证失败/已断开"，
  并在拦截关闭时直接写明"不会有流量记录，请先打开拦截"。
- 【修复】**流量记录的日期时间缺日期**。记录里只存了"17:11:49"，跨天的日志在页面上
  无法区分是哪一天。现在存完整日期时间。
- 【改进】**流量日志页重做**：一张简要的表（日期时间 / 来源协议 / 来源IP:端口 /
  目标IP:端口 / IP归属 / 动作），点任意一行看详情 —— **检测内容放在弹窗里**，
  列表保持简短（你说后面要接大屏，列表越短越好排）。筛选补上：动作、协议、
  关键字；「引擎日志」单独一个页签，级别筛选在那里（多选，真实生效）。
  IP归属只查当前页看得见的行，不整屏外发。
- 【改进】**日志留存改为等级驱动，不再是一个写死的条数**。以前内存环硬上限500条，
  重启即空。现在：记录**落盘**在 /etc/log/naixifw/traffic.jsonl（与爱快自身日志同在
  /etc/log持久分区），每8MB轮转、保留若干个（默认5个 ≈ 40MB），实际上限由磁盘
  决定。**记什么**由日志等级决定：debug全记（含放行）、info记拦截+仅记录（默认）、
  warn只记拦截、error不记流量记录。等级与留存文件数都在option.json里（安装时可选，
  面板改等级后以面板为准）。内存环也按等级放大（info 1万 / debug 2万），
  因为它是实时看板的数据源，不是留存。
- 【改进】**规则管理：协议规则可以删除，不只是停用**。删除后该类目从规则列表消失
  （停用只是关掉开关），列表上方会出现"已删除的协议类目"并提供**恢复** ——
  删除必须可逆，否则误删只能手改配置文件。
- 【新增】**规则的「生效终端」**：新增/编辑规则时可以像选设备一样挑局域网机器
  （列出IP、MAC、主机名），只对这些机器生效。目前用在协议规则上：例如只封办公室
  那台机器的代理，而不是全屋一刀切。留空 = 对所有终端生效。设备列表来自设备自己的
  ARP表与主机表，不联网。
- 【规范】协议类目的合法键由一处集中定义（spec.ProtoKeys），保存时校验"删除的类目"
  与"生效终端"的键 —— 这两个字段写错时渲染会静默跳过，表现是"我明明删了，规则还在"。

1.36.1
- 【修复·重要】**点「启用」报「找不到该规则」**。面板的启用开关把规则id放在请求体里，
  而接口只从URL查询参数里读 —— 读到的永远是空串，于是每次都报"这条规则不存在"，
  而规则明明就在列表里。这个错很容易被当成数据问题去查（其实规则一直都在，
  是参数没读到）。现在两种写法都支持：面板发请求体、命令行脚本发 `?id=`。
  影响范围：域名规则、IP规则的启用开关。命令行的删除走的是查询参数，一直是好的。
- 【修复·重要】**端口规则从来没能保存过**。前端「端口规则」页签点保存发的是
  `POST /api/rules/port`，而路由表里从来没有这一条 —— 请求打到未注册的路径上是404。
  面板会弹一句"请求失败"，但很容易被当成偶发问题，而不是"这个功能根本没有后端"。
  现在接口补齐（增/删/查），并把校验做全：端口范围1-65535、协议名、方向、生效时段。
- 【改进】**规则管理改成直接写WAF规则：域名 / IP / 端口 / 协议 四合一**。
  以前是四个页签、四套表单，"拦某个东西"要先去对页签、再对字段名。现在是一张表、
  一个弹窗：先选匹配类型（域名 / IP地址 / IP网段 / 国家或地区 / 端口 / 协议 / 表达式），
  再填值，动作统一（拦截 / 放行 / 仅记录）。协议类目就是原来的「协议屏蔽」，
  作为一类规则出现在同一个列表里；协议规则的"编辑"即改动作。
  批量管理照旧保留：批量录入按类型分派，多选批量启用/停用/删除按类目分组一次提交
  （不是逐条热加载）。
- 【改进】端口规则补齐**生效时段**，四类规则的能力现在一致。之前统一了界面却留着
  一个"端口不能按时段生效"的缺口，说不通。
- 【修复】界面改回浅色。上一版把左侧导航与顶栏压成深色，而antd的菜单仍是浅色主题
  （深灰字、浅蓝选中底），深底配深字，整屏看起来"黑乎乎"、像配色坏了。
  这是我加的（当时想模仿网络安全设备的控制台），产品不要深色，已改回跟随antd的
  浅色主题。状态芯片与指标条保留 —— 那是功能，与环境色无关。
- 【规范】顶部状态芯片与指标条保留并改为浅色配色。原来那五件事散在各页
  （拦截开关在规则管理、引擎状态在侧栏底部、模式在系统设置），切页面才拼得出全貌；
  "规则建好了但拦截没开"正是最该被一眼看穿的失误。

1.36.0
- 【修复·重要】**网段规则以前从来没生效过**。网段和单个地址都被写成 `in ["..."]`，而 `in`
  做的是**字符串相等**比较 —— `"192.168.50.1" in ["192.168.50.0/24"]` 恒为假。也就是说：
  面板上一条"已启用"的网段拦截规则，从来没有拦下过任何流量，而且不报任何错。现在网段用
  引擎的 `cidr()` 函数（掩码比较），国家或地区用 `geoip()`。
- 【修复】表达式校验不认识引擎自带的函数，把 `cidr()` / `geoip()` / `geosite()` / `lookup()`
  当成"未知名字"拒掉——想拦网段的人只能退回去写单个地址。校验层现在认识这些函数。
- 【新增】**QUIC（HTTP/3）识别层**。这解释了那个很怪的现象：curl明明已经收到RST，
  浏览器却还能打开站点。curl走TCP，被 `REJECT --reject-with tcp-reset` 干净切断；
  而现代浏览器对支持HTTP/3的站点优先走QUIC（UDP 443），那条路径**根本不经过TCP规则**，
  而且QUIC没有RST这回事（只有丢包或超时）。域名规则的识别层现在多一项QUIC，
  解析的就是 `quic.req.sni`。没勾这一项的拦截规则会在列表里标出「缺QUIC」并提示补勾。
- 【改进】域名规则的「识别层」按**推荐值**默认勾选（DNS + TLS + QUIC），并用cookie
  记住上次的选择（编辑时不覆盖偏好）。默认值错的最坏后果是"以为拦了整个站点、实际少拦一层"。
- 【改进】新增域名规则、新增IP规则都改成**弹窗**打开，新增与编辑共用同一个弹窗，
  不再把表单直接铺在页面上。列表里每条都能「编辑」「删除」。
- 【新增】IP规则的**匹配类型**：IP地址 / IP网段 / 国家或地区 / 协议匹配 / 表达式（进阶）。
  带 `/` 的老规则会自动认成网段——不认的话升级后这些规则会因为值为空而被跳过，
  等于拦截规则凭空消失。
- 【新增】每条规则都能设**生效时段**，例如 `22:00-08:00`（跨天）或 `09:00-18:00`（当天），
  留空即全天有效。跨天的判定是单独写的并且有边界测试：写成 `开始<=现在<=结束` 会恒为假，
  表现是"到点了却不生效"，不报任何错。时段非法时保存会被拒（宁可让操作员改对，
  也不能让他以为只在夜间生效、实际整天在拦）。
  时段的实现方式是**到点重算规则文件并热加载**（引擎的表达式环境里没有时间，规则只有
  匹配或不匹配两种结果，所以"几点到几点生效"没法写成一条规则）。守护进程每分钟比对一次，
  只有内容真的变了才写盘——路由器上是持久分区，无谓重写等于磨闪存。
- 【改进】**协议屏蔽细分为七类**：加密代理 / 加密隧道 / Trojan / SOCKS / P2P / WireGuard /
  OpenVPN（SSH保留）。原来的Shadowsocks类 与VMess其实共用同一个判据，面板上是两个
  开关、实际同一个分析器，那样的粒度写不出有意义的规则。每一类都对应引擎里真实存在的
  分析器；**P2P当前不可用**（引擎没有对应分析器），那一行直接标出来并说明用什么替代，
  而不是给一个拨了不会产生任何规则的开关。默认全部关闭，建议先只开「监测」观察。
- 【新增】**自维护威胁情报**。原来的威胁情报页只是读设备内核里的IOC计数——那份清单随
  固件走、由厂商维护，插件只能读不能改。现在改成自己的清单：域名 / IP / 网段 / 关键字
  四类，可编辑、可停用、可设生效时段，并且**条目直接参与拦截**（渲染成引擎规则并立即
  热加载），不只是看板。域名类默认四条路径一起拦（DNS / TLS / QUIC / HTTP）——情报条目的
  语义是"这个域名整体不可信"，漏一条路径就等于还能从那里绕过去。设备自带的那份保留为
  只读卡片，它的命中计数对判断"有没有被打"有用。
- 【改进】左侧导航固定为十项：规则管理 / 流量日志 / 网站记录 / 应用分析 / 应用管控 /
  行为审计 / 应用规则库 / 威胁情报 / 关键字阻断 / 系统设置。「应用管控」紧跟「应用分析」
  —— 先看哪个应用在跑，再决定拦不拦，两件事挨着。
- 【改进】界面顶部加了**常驻状态栏与指标条**：拦截开没开、跑在哪个模式、RST开没关、
  队列怎么收、引擎活着没，都在视野里，不依赖当前在哪一页。之前这些散在各页，
  "规则建好了但拦截没开"这类失误要切页面才能发现。数字只显示服务端确实给出的值，
  缺一个就显示「-」而不是用0冒充（0会被读成"没有任何流量"，含义完全不同）。
- 【修订】「应用管控」页面里两句话还在说旧行为（"规则会写进爱快的配置"、"规则是系统级的"），
  而1.35.0起应用规则已改由插件自管、不写系统配置库。这两句改对了 —— 面板上留一句
  过时的话，比缺一句话更容易让人误判：按旧描述去排查，会找错方向。
- 【规范】面板与提示文案里中文与英文、数字之间不再留空格。这条同时加了闸门，而且判据
  是脚本而不是grep——`grep -P` 的汉字范围写法在本环境里返回0匹配，看起来像"没问题"，
  其实判据根本没生效。

1.35.0
- 【改造】**应用规则不再写系统配置库**。规则改由插件自己下发到内核的TC表：插件启动时
  装入、停止时撤下。原来的做法是走爱快自己的模块（acl_l7），规则落在系统的config.db里
  —— 那条路"重启不丢"，但会在系统配置库里留下行：插件卸载后那些行还在，重启后系统照样
  重放，变成一堆界面上说不清来历的孤儿规则。这与"卸载后不留痕"直接冲突，所以换掉。
- 【行为变化】**插件没运行时应用规则不生效**（规则只在内核里，内核不留东西）。重启后由
  插件重新装入。这是上面那个取舍的代价，面板上写明了；模块模式仍保留为可选
  （配置项 `ikuai.appfwBackend: module`），出问题一条配置切回去，不用换包。
- 【接管】升级时**自动接管**旧规则：启动会读出系统配置库里带本插件前缀的规则，存进插件
  自己的配置、装进内核，然后用系统自己的模块接口把那些行删掉。不做这一步的话，旧规则
  仍在内核里生效，而面板已经不再显示它们 —— "看不见但在拦流量"比规则少更危险。
- 【自愈】启动时会**收敛**内核状态：清掉上次进程被强杀留下的残留（含重复下发的），
  再把配置里该生效的装上。`stop.sh` 里还有一遍同样的清扫，覆盖"进程根本没跑到清理代码"
  的情况。
- 【实测】实现依据来自真机上验证过的两条内核行为：删除必须在**正确的prio** 桶里找
  （prio不对就删不掉）；同一个id重复下发会产生**两条**规则、而删除一次只摘掉第一条。
  这两点决定了代码写成"先清同id残留 → 再装 → 回读内核确认"，也让配置里必须记住prio。
- 【校验】规则id固定使用900000000以上的号段（远高于系统自增id，且在32位atoi
  范围内），配置里出现区间外的id会被**拒绝加载** —— 否则插件可能去删系统的规则。
- 【健壮】装规则时用的应用名会翻成appid再下发（内核认appid不认名字），翻不出来的
  名字一律拒绝并说明，避免装出一条"什么也不匹配"的死规则。
- 【测试】新增13项自管模式验收，假内核刻意照抄真机的两个坑（prio桶、不查重），
  覆盖：装入/停用/删除、收敛清残留与重复、停止时清内核但保留配置、重复装幂等、
  旧规则接管（且绝不碰系统规则）、存储拒绝区间外id、以及模块模式默认行为不被改坏。
  其中"收敛要清掉重复"这一条当场抓出了一个真bug：我原来的"参数一致就放过"会把
  重复项当成正常状态永远留着。
1.34.0
- 【新功能】设置页新增「**阻断行为**」：**RST开关**。默认的「丢包」是被拦的包直接丢掉，
  对方一直重传到超时，被拦的人看到的是「卡住」；打开后连接**立刻**断开，他马上就
  知道被拦了。代价是被拦的一方看得出这是人为阻断（而不是网络故障）。只影响**被规则
  拦下**的连接，放过的不受影响。
- 【说明】RST **只对TCP有效**：UDP / QUIC没有RST这回事，走UDP的应用无论开关
  如何都还是靠丢包与超时（这包括大量视频与新版浏览器流量）。
- 【校验】**本机模式下RST不可用**（引擎会直接拒绝：tcp rst is not supported in
  local mode）。面板上这个开关在本机模式下是禁用的并说明原因，而不是存下一个不生效的值；
  服务端也会拒绝这种组合。
- 【修正】**内置库现在是真正的一主一备**。上一版把「主线路 / 备用」做成了各用一条入口，
  那条线路不通时就会完全查不出结果 —— 而选「内置」的本意是「用内置的库」，不是「只用
  其中一条线路」。现在：`内置（主线路）` = 主优先、失败自动用备；`内置（备用）` = 备优先、
  失败自动用主。自定义仍然只有一条，且**绝不回退到内置**（选了自定义就是不想把地址送给
  内置那家第三方）。
- 【测试】新增：RST的面板接线与「本机模式 + RST被拒」的既有校验、内置库的主/备配对与
  **顺序真的决定先试谁**（用一个入口故意不通的假服务端验证）。打包闸门补上对应检查。
1.33.0
- 【新功能】**IP归属地库可选**：内置（主线路）/ 内置（备用）/ 自定义。
  说清"主/备"是什么意思：两条内置入口实测是**同一个后端的两个入口**（响应逐字节相同），
  所以它们互为线路备份，**不互为后端备份** —— 后端故障时一起失效。要真正抗后端故障得用
  自定义接另一家服务。
- 【新功能】**自定义归属地库**：填一个带 `{ip}` 占位符的地址即可，例如
  `https://get.geojs.io/v1/ip/geo/{ip}.json`、`https://ipinfo.io/widget/demo/{ip}`。
  响应没有固定格式：国家 / 地区 / 城市 / 运营商 / ASN这些字段按常见命名自动识别
  （含"包在data里"的结构），两位国家代码自动翻成中文。实测可用的服务：geojs、
  ipinfo（widget/demo与直连 /json两种地址都验过）。
- 【新功能】面板上有**测试**按钮：填完地址点一下，会**真查一次**并把即将显示的标注原样
  回显（如 `1.1.1.1（澳大利亚Queensland Brisbane …）`）。只做格式校验回答不了
  "这个地址到底能不能查出来"，而那正是操作员关心的。
- 【安全】**自定义地址按白名单严格校验** —— 这个输入框最终会变成一个由root进程发出的
  HTTP请求，所以：只收http/https（堵掉file:// 读本地文件、gopher/dict打内网服务）；
  必须恰好一个 `{ip}`，且不许出现在主机名里（主机必须固定）；拒绝控制字符与空白
  （CR/LF是请求头注入）、反斜杠（部分解析器把 \ 当 /，可绕过按路径的判断）；拒绝
  用户名口令（会随请求发出并显示在日志与面板）与 `#fragment`；主机名必须是合法
  主机名或IP（非ASCII主机名明确拒绝并提示改用punycode）；端口必须是1..65535。
  另外**不跟随重定向**：一次查询被302到别处，就等于把被查的IP发给了操作员没指定的
  主机，这也是SSRF的常见绕法 —— 宁可报错让他改地址。
- 【设计】**自定义源不会回退到内置源**。选了自定义就是不想把被查的IP送给内置的第三方；
  失败时偷偷回退等于在他不知情的情况下把数据送了出去。自定义源失败就是失败（面板上
  如实显示"查不到"，表格照常）。这条有测试守着。
- 【设计】指向本机/内网、或明文http的地址**允许保存**（自建内网服务是合理用法），但面板上
  会一直提示；而"公网地址跳到内网"由禁止重定向堵掉。
- 【设计】换库会**清空归属地缓存** —— 旧结果来自另一个数据源，留着会让标注前后不一致，
  而且那种不一致极难归因。
- 【健壮性】自定义服务的响应在进面板前会清洗：控制字符去掉、连续空白合并、字段截断
  （48字符）、整条标注截断（90字符）；返回的不是JSON（填成了网页地址）时明确说
  "返回的不是JSON"；认不出归属地字段时**列出响应里实际有哪些字段**，便于换一家服务。
- 【健壮性】选自定义却没填地址、或地址不合法：**拒绝保存**并说清原因（不写进配置），
  避免出现"看起来配了其实没生效"。配置校验和面板校验走同一套函数。
- 【约定】`{ip}` 的替换做URL转义，IPv6也能正确送达（`2001:db8::1` → `2001%3Adb8%3A%3A1`）。
- 【测试】新增60+ 项验收：模板校验的正/反清单（含file://、gopher://、CRLF注入、
  反斜杠绕过、用户名口令、占位符位置、端口越界、中文域名等注入尝试）、两种真实服务的
  响应解析（夹具是从真服务抓的原文）、面积代码不被误当省份、字段清洗与截断、
  国家代码翻译、不跟随重定向、换源清缓存、自定义源不回退、设置往返与拒绝非法值。
1.32.0
- 【新功能】**规则批量录入**。域名 / IP / 关键字三个页签都可以一次粘贴一份清单：
  一行一条，`#` 开头的注释与空行自动忽略。能直接吃下常见的各种写法，并把改动说清楚：
  `0.0.0.0 ads.example.com`（hosts格式）、`https://a.com/path`（去掉协议头与路径）、
  `a.com:8080`（去掉端口）、`*.a.com`（通配前缀）。
- 【设计】**每一行都有归宿** —— 这是这个功能的重点。粘贴的内容里总会有几行是那份
  清单特有的写法，所以面板会**逐行**给出结果（带原始行号）：录入了 / 因为什么被跳过 /
  没录入的原因，绝不静默丢掉。若只报一句"已添加187条"，剩下13条去哪了就没人知道，
  而"看起来做了其实没做"正是这个产品最犯不起的错。判不出是域名的（没有点、是IP、
  是IPv6）会明确建议改用哪一页。
- 【新功能】**多选批量操作**：规则的启用 / 停用 / 删除支持多选一次完成（三个页签都有）。
  之前只能一条一条点开关，几十条规则时基本不可用。批量操作只改选中的那些，
  其余原样保留。
- 【设计】**批量只保存一次**。单条接口每次调用都会重写规则文件并热加载一次引擎，
  批量挨个调会变成N次重写 + N次重载、中途失败还会留下半套规则；批量接口在内存里
  改完统一保存并热加载一次。
- 【健壮性】粘贴内容有上限（2000行）：超出部分**逐行报出来**，而不是默默截断 ——
  否则"粘了3000行、只加了2000条"这件事操作员不会知道。
- 【严重修复】**域名与关键字里出现引号或反斜杠，会把整份规则集打成编译不过。**
  面板对这两个字段只查了"非空"，而生成表达式时是手工拼引号 —— 一个 `"` 就把字符串
  提前闭合，结果是保存后引擎装载失败。批量粘贴最容易撞上（清单里混进一行怪东西）。
  现在值统一用转义嵌入，并有引擎级测试守着：带引号/反斜杠/换行的规则不仅编译得过，
  还必须仍然**能命中**自己该命中的东西（否则只是把"编译不过"换成了"永不命中"）。
- 【测试】新增批量解析的验收：hosts格式、URL、端口、通配、大小写与末尾的点、
  注释与空行、行号定位、行数上限、超长行回显截断，以及"每一行都必须有归宿"的
  不变量测试（拿一份真实形态的清单灌进去，逐行核对没有凭空消失的行）。
1.31.0
- 【新功能】**域名标注**：面板上的域名会带上中文名，如 `www.bing.com（微软必应搜索）`。
  数据来自**设备自带的域名库**（/usr/libproto/domaingroup，39个文件21967条，文件名就是
  分类），纯本地读取 —— 不联网、不外发任何东西。标注按标签边界后缀匹配（库里的bing.com
  能命中cn.bing.com，但不会命中notbing.com）。设定里可以关。
- 【新功能】**IP归属地标注**：IP后面带上归属地，如 `223.5.5.5（阿里云）`、
  `58.63.233.34（广东 广州 电信）`。用的是公开的归属地接口，所以：
    · **只查表格当前页看得见的那些地址**（不做全量扫描）—— 每个被查的IP都会发给
      第三方，全量扫等于把用户访问过的目标成批送出去；
    · **可以在设置里关掉**，关掉后一个查询都不发（前后端都挡）。
  标注出现在「网站记录」的域名列与「审计」的客户端/目标列。
- 【设计】**归属地标注的取舍**写在代码里：接口把两种不同的东西塞进同一个字段 ——
  `country` 是中文时是地名（中国/广东/广州），是域名时其实是**机构名**（ALIDNS.COM），
  中文名在 `isp` 里（阿里云）。所以标注分两条路走，国内省略「中国」、国外保留国名。
- 【健壮性】归属地查询：主备两条入口（实测两者响应逐字节相同，是同一后端 —— 能抗
  链路故障，抗不了后端故障，注释里写明了）；查询有总时限，**到点返回已查到的部分**，
  绝不让表格卡住或报错；失败结果短时缓存（否则接口一挂，每个IP每次显示都会重发两次
  请求，把一次故障放大成持续的请求风暴）；私网/保留地址本地作答、不发请求；空IP在
  本地就被拒（那个接口对空query会返回**查询者自己**的位置）。
- 【健壮性】域名库不存在是**正常状态**（低配型号的固件里没有这个目录）：面板会如实说明
  「这台设备上没有域名库」，而不是让标注静默消失。
- 【测试】新增domainname / annotate两个包的验收：库解析（名字带空格、只有域名没有名字、
  注释与空行、分类来自文件名）、标签边界后缀匹配、库不存在不算错误、标注开关关掉后
  **零请求**、超时返回部分结果、接口全挂时静默降级且不重复猛打、域名库缺失不影响IP标注。
- 【测试】geolocate的夹具用的是**实测抓到的真实响应原文**（包括1.1.1.1 → CLOUDFLARE.COM
  这种"机构名而不是地名"的情形），所以这些测试同时锁住"接口真实长什么样"。

（本包内只保留最近的版本记录，完整历史见项目仓库的 changelog。
原因：插件市场的安装器把整条记录作为一个命令行参数交给 sqlite3，而系统对
单个参数有硬上限（131072 字节）——包内文本过大会导致注册失败。）
