1.57.0
- 【拦截未生效·面板必须在任何一页就说清楚】新增全站横幅：当「拦截已开启、引擎却没在运行」时，
  面板最上方直接给出三件事 —— 一句「当前不拦任何流量」、为什么会这样、以及能照着做的下一步
  （去规则管理 / 看引擎日志两个按钮）。报错原文原样贴出（等宽块、不截断不转述），因为它里面
  点名的规则名就是操作员要去改的那条。
- 【为什么要改顶栏芯片】原来顶栏同时显示「拦截已开启」（绿）与「引擎未运行」（红）—— 都没说错，
  但合起来读就成了「你以为在拦，其实什么都没拦」，而这正是整个产品最要紧的一个状态：它意味着
  所有规则、所有识别、所有日志都不存在。现在合成三态：已关闭 / 已开启 / 未生效。
- 【failed 与 stopped 分开说】「反复启动失败、已停止重试」不会自己变好（除了改规则不会有任何
  变化），所以明说「不会自己变好」；「还没起来/正在重试」可能自己好，所以不把话说死。两者在
  界面上长得一样的话，人会一直等一个不会变的状态。
- 【/api/status 新增 state】面板要分辨上面两种状态就得有 state 字段（此前只有 running 布尔值）。
- 【顺带】引擎报错块用等宽字体：报错里的位置指针（`| protstd != nil` / `| ^`）靠等宽对齐，
  按比例字体渲染就对不上是哪一列。

1.56.0
- 【规则写错时的报错·你点名要的「好找」】写错字段名不再只是「unknown name」，而是直接告诉你该怎么改：
    第 1条规则（我写的规则）的expr有问题：unknown name protstd (1:1)
             | protstd != nil
             | ^（你是不是想写 portstd？）
  四样东西同时在：规则名（去哪条改）、写错的名字（改什么）、建议（改成什么）、位置行:列（改哪儿）。
- 【不许乱给建议】名字错得太远、没有近似项时，退回「可用字段」完整清单 —— 这时读者要的是「我到底
  能写什么」。说「你是不是想写 X」却说错了，比不说话更坏。
- 【一个刻意的取舍：短名不互推】`tcp` 与 `udp` 只差一个字母，但它俩是两个不同的名字，不是拼错。
  所以三个字母以下的名字不给建议 —— 编辑距离单独分不出「拼错」和「另一个名字」，靠长度分：
  短名共一个字母的情况太普遍（id/ip、tls/fet）。
- 【建议只有一个来源】插件与引擎共用同一个建议助手（引擎导出），不在插件侧复制一份距离算法 ——
  否则同一个错名会因「哪个校验先跑到」而得到两条不同的建议。
- 【引擎v0.19.1】导出该助手。注意 v0.19.0 的标签内容不含它：**已发布的模块版本不可变**（代理会
  一直发它第一次看到的字节），所以只能发新版本 —— 强推标签的后果是调用方拿到旧内容而 go.mod
  看着正确，报错是莫名其妙的「undefined: X」。这条已写进闸门。
- 【顺带】引擎v0.19.0：拼错时给建议、名称含换位（protstd→portstd）算一次编辑、大小写写错也认。

1.55.0
- 【强制TLS降级·引擎v0.17.0接进面板】新增「强制TLS降级」卡片：让TLS1.3协商降级成1.2。为什么需要它：
  1.3把证书加密了，所以「证书是否可信」这条判据在1.3上拿不到任何证据 —— 自签的、域名不匹配的证书在
  1.3下完全看不见。降到1.2之后证书重新以明文出现在握手里，证书异常类目才有东西可判。它是**用户态
  改写**（NFQUEUE里改完再放行），不碰tc，也不需要自建分类树。
  代价写在卡片上：放弃1.3的握手保护。这是拿安全换可见性，所以默认关、并且建议把范围写窄。
- 【作用范围】填IP或网段（如10.0.0.5或10.0.0.0/8），多个用逗号分隔；单个IP会自动补成/32或/128。
  留空=所有走443的TLS连接都降级，面板会提示那是个很大的动作。
- 【一处刻意的选择】范围里有不合法项时，整个功能不生效（一条规则都不发），并在规则页指出哪一项写错了。
  不按合法项发一条「看起来生效」的规则，也不退化成全域规则 —— 前者会让你以为范围就是自己写的那样，
  后者会把「只想管几个网段」执行成「全网降级」。
- 【表达式必须限定TCP】proto == "tcp" && port.dst == 443。不带proto的话UDP:443（QUIC）也会命中，
  而TCP改写器收到UDP流会报错 —— 每条QUIC流一条错误日志。已在测试里钉住。
- 【只对完整的握手生效】引擎侧校验「重组到的数据 == 本包载荷」，所以跨两个报文分片的ClientHello会被
  原样放过（不打坏，只是没降级）。这是刻意的：改短或改长会撕裂TCP连接。
- 【引擎v0.18.0：加载期拒绝写错的规则】规则引用了引擎无法解析的标识符（拼错、或引用了这次没加载的
  分析器）现在会在加载期**直接拒绝**，并指出规则名与标识符。此前是静默跳过 —— 规则能编译、能加载、
  永远不命中、不报任何错，而面板显示它生效中。这正是最坏的一种失效：没人会去查一条看起来正常的规则。
  引用了一个**已加载但没报属性**的分析器不是错误 —— 属性不存在是事实，`portstd != nil` 就是这么用的。
- 【引擎v0.18.0：收包路径不为配置错误而崩】logger缺失（配置写错）时降级为空实现，而不是在匹配器里
  空指针。一条规则不被求值，好过整个拦截器倒下。
- 【顺带修掉测试里的一处不真实】那组QUIC端到端测试原来用精简的分析器清单（只放TLS/QUIC/DNS），而面板
  生成的observe规则引用了cloudflarecdn/portstd/certmeta —— 以前静默跳过所以没暴露，引擎改严之后立刻红。
  现在改用生产清单（core.Analyzers()）：面板规则与生产分析器才是一起上线的那一对。
- 【未做】iptoasn出厂携带与自动更新：按你的决定用紧凑二进制编码（2.02MB、可直接查询、不用解压），
  这轮先把TLS接线做完，ASN放下一轮。

1.54.1
- 【工具链】Go 从 go1.24.4 升到 go1.26.5。选择依据是实测体积：amd64 静态二进制
  go1.24.4=21.7MB、go1.25.0=21.4MB、go1.26.5=21.9MB、go1.27.1=23.0MB。1.26.5 只大 0.2MB，
  换来新运行时的 GC 与安全修复；1.27.1 要 +1.3MB，路由器上体积是真实成本，所以先不追。
  两种架构（amd64/arm64）都用新工具链重新编译并通过全量测试。
- 【重要】这一版的二进制是**新编译器**产出的。你之前 x64/arm64 的通过结论是对 go1.24.4
  编译出来的包，所以这一版请重新测一次 —— 不是流程要求，是这次换了编译器。

1.54.0
- 【留存压缩·你点名要的】轮转出去的段现在用 gzip 压成 .gz（最高档），最新的活动文件保持明文
  —— 排查时 tail/grep 是刚需。压缩在后台 goroutine 里做，不占写入路径；轮转本身仍然只是
  一次改名。
- 【留更久】默认保留段数从 5 提到 12：8MB × 12 段未压缩约 96MB，压完约 10MB，加上活动文件
  8MB 约 18MB —— 比原来"5 段不压缩"的 40MB 更省，而历史是 2.4 倍长。压缩省下的余量花在
  "留更久"上，这比花在"少占盘"上更符合经得起长期堆积。

  实测（2000 条真实形状记录）：普通 gzip 约 9~12 倍；重复度极高的合成样本能到 75 倍。
  面板的"留存条数"会把压缩段解开一起数，所以显示的数字仍然是真实条数。
- 【改对了一个会丢段的缺陷·测试抓到的】原来的段名是"逐级后移"（.1 → .2 → .3 …）。后台压缩器
  正在压 .1 时，一次新轮转会把 .1 改名成 .2，压缩器压完却把结果写成 .1.gz —— 内容是 .2 的。
  于是同一段在盘上出现两次，而"丢掉最老一个"执行时删掉的可能是真段，**记录就这么少了**。
  测试抓到过相邻段之间少几条。改成不可变段名（traffic.jsonl.<时间戳>）后改名不再发生，
  这个竞态从根上消失；排序靠固定宽度的 UTC 时间戳，字典序即时间序。
  顺带修掉：清空日志原来按编号删，段名不可变后会漏删 .gz —— 那会让"清空"之后面板还读得到旧记录。
- 【运行时垃圾统一 /tmp/naixifw·你定的】缓存、pid 文件、临时文件、以及本进程自己的日志
  （run.log / engine.log）全部移到 /tmp/naixifw/ 下。原来它们放在安装目录，而升级会整目录替换
  .pkg/app —— 一次升级就把正在用的 pid 文件删掉了，日志也会被清（"该留的证据被升级清掉"比丢
  缓存更糟）。放 /tmp 还多一层：重启即清空，小存储设备上"靠重启回收"比任何清理逻辑都可靠。
  注意生命周期：这些是"本次运行的诊断"，需要长期留存的是流量记录（traffic.jsonl，在持久目录
  /etc/log/naixifw 下，另有轮转与压缩）。
- 【引擎测试搬回源码旁·你选的 2】test/ 目录取消，4 个测试回到各自包的目录里。
- 【未做】io 写路径的 recover：我在测试里踩到过规则引用未加载分析器时的空指针，但那是我的测试
  写错了（没给 Logger）。收包路径要不要兜住"规则写错导致崩溃"是**产品决策**不是缺陷修复，
  没有你的话我不动它。

1.53.0
- 【判据收敛·你定的口径】「端口与协议不符」改称 **协议不一致**，两档范围按你的原话重划：
  · 宽松档**容忍**「把 25 端口拿来开 web」「把 3389 拿来开 SSH」—— 这两种在宽松档下不再判定。
  · 严格档才判，命中即拦，日志里写「协议不一致」。
  实质：原先这一整类（在别人的端口上跑服务）被放在"绝无可能"那一档，而宽松档只跑那一档，所以
  一开宽松就会把自己家网络上"奇怪但正常"的服务误拦 —— 这正是会让操作员永久关掉一个功能的那类
  误报。现在宽松档只保留真正跑不通的组合。
- 【宽松档剩下什么】只剩一条：**443 端口上的明文HTTP**。它不属于"有人故意这么配"，而是
  "这么配根本连不上" —— 全世界的浏览器都当 443 是加密端口，出现明文说明中间有人拆掉了 TLS。
  这一档在任何模式下都敢拦，因为它不依赖任何阈值。
- 【补一个漏掉的端口】5900（VNC）加入远程管理端口组，与 3389（RDP）、445（SMB）同组。
- 【引擎结构按你的要求固定】引擎不再在根目录长新文件夹，IANA 端口表与 Cloudflare 地址段移入
  analyzer/ 之下；根目录固定为 analyzer / ruleset / engine / io / modifier / docs / test。
  插件侧有闸门盯着这件事，日后再加会构建失败。
- 【引擎测试搬进 test/】只用导出接口的验收测试移入引擎的 test/ 目录；需要访问包内私有符号的
  测试搬不了 —— Go 规定测试文件必须与源码同目录才能访问私有符号，这是语言限制不是偏好。

1.52.0
- 【新增·「端口与协议不符」类目】判"矛盾"而不是"可疑"：某个端口上出现了绝不属于它的东西。
  判据数据来自引擎内置的 **IANA 端口注册表**（714 条：0-1023 全部公认端口 + 一组有实际流量的
  注册端口，由 IANA 的 CSV 生成而非手抄），分组出"这个端口不可能承载什么"。
  例：SMTP 的 25 端口上跑Web明文流量 —— 这正是你举的例子。它比"非标端口"强在：非标端口只是
  事实（可能完全正常），而矛盾是两个独立事实互相打架，误报率因此有界。
- 【宽松/严格两档】判据分两级，面板上可切换：
  · 宽松（默认）：只用"绝无可能"一级 —— 邮件端口跑Web、共享/远程管理端口跑Web、数据库端口跑Web、
    这些端口上出现SSH。敢拦，因为它要求两个事实矛盾。
  · 严格：加上"很反常"一级 —— 协议在该端口没有TLS形态（445上出现TLS）、端口承诺加密却出现明文
    （443上跑明文HTTP）。这一级是线索不是判决，默认关闭。
  刻意排除的误报（有测试钉住）：邮件端口的TLS（STARTTLS是正常的邮件提交方式）、3389的TLS
  （RDP的现代默认）、21的TLS（显式FTPS）、1433的TLS（SQL Server加密）、389的TLS（StartTLS）。
  这些一旦标出来，每个正常在这种端口上工作的人都会上板子，而一次误伤就足以让操作员不再看板子。
- 【新增·高危端口名单】25/135/139/444/445/5800/5900（运营商与云厂商默认封堵，来源：阿里云ECS
  常见端口说明）。这是**信息**不是判决：端口高危不代表流量可疑，它代表**服务**放在这里很反常 ——
  而这正是隧道会躲的地方。
- 【新增·端口表接口】/api/v1/ports/reference 返回判据用到的端口、它们的IANA名字、以及每个端口
  上不容许什么（含中文说明与命中的两级）。判据要可解释：只给一个开关的拦截功能，操作员在第一次
  误伤之后就会永久关掉它。
- 【记录·一条被权威表纠正的记忆】IANA 现在把 465 注册为 urd，不是多数资料仍在写的 smtps。
  端口表以注册表为准，并有测试钉住这一点，免得日后有人"修回"成记忆里的名字。

1.51.0
- 【新增·「Cloudflare CDN」作为一种类型】上一版把CF豁免写成了规则里的 cloudflare_cdn(...) 调用；
  按你的意见改成**一个类型**：协议库里多一条「Cloudflare CDN」，判据是"目的地属于CF地址段 **且**
  端口是CF公开端口"。非标端口类目的豁免因此变成引用它：portstd != nil && cloudflarecdn == nil
  —— 豁免所豁免的东西从此在协议库里有名字、看得见、改得动，而不是表达式里的一段条件。
  这个类型默认关，而且面板上写明：**拦它几乎总是错的**（CF上是一大块正常网站）。它的意义是
  ①让豁免有名字 ②操作员能基于它写自己的规则。
- 【保留·cloudflare_cdn 内置函数】表达式里仍可用 cloudflare_cdn(ip, 端口)，供自定义规则直接判。
  它与类型读的是引擎里同一份 cloudflare 清单，不存在两份数据。
- 【记录·面板为什么不用它命名流量】这个类型**不参与**流量记录的命名（analyzerOf 里没有它）：
  CF 上是全世界一大块普通 HTTPS，用它命名会把实时看板刷满"Cloudflare CDN"，而操作员要看的是
  "这条是不是隧道"。要判断某条流量是不是CF CDN，看协议库与规则即可。


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