1.65.0
  - 【规则优先级：谁先说话，现在由你定】引擎判定时按规则表**从上往下**匹配，**第一个命中就定案**。
    而规则表的先后原来是由**规则类型**决定的（域名 → 关键字 → 威胁情报 → IP → 端口 →
    协议类目 → 自定义协议），界面上既看不到这个顺序，也改不了 —— 于是「给 a.example.com
    放行」和「拦截所有 Trojan」谁生效，取决于这两条规则的**类型**，而不是你的意图。
    现在每条规则可以填一个「优先级」（新增输入框，留空 = 用它那个类型的缺省位置）。
    数字小的先求值。典型用法：整体拦某类协议，同时把要给例外的域名规则填一个小数字
    （比如 50），例外才真的成立。
    升级安全：没有填过优先级的规则，行为与升级前**逐条完全相同**（这一点是用"改动前后
    生成的规则文件逐字节比对"验过的，不是口头保证）。
  - 【新增「拒绝（回RST）」动作】原来「拦截」是静默丢弃 —— 客户端像遇到网络不稳，
    会重传、等待，最后往往换一个节点重连。新动作「拒绝」在 TCP 上是**回一个 RST**：
    明确、立即失败，客户端马上知道这是被拒绝了。
    两点如实说明：① UDP 与 ICMP 没有 RST，那里「拒绝」与「拦截」等价（给 UDP 回
    "端口不可达"需要两套 IPv4/IPv6 拼写，写错会让整份规则装载失败 —— 那是"看起来什么
    都没拦"，所以没做）；② 想选哪个取决于你要什么：让对端**困惑**（拦截）还是让对端
    **明白**（拒绝）。
  - 【升级引擎到 v0.20.0】新增 reject 动作（上面那条的实现）。

1.64.0
  - 【性能：按协议识别不再吃掉吞吐】引擎升到 v0.19.4，规则表改为「按守卫跳组」。
    背景：规则是逐条比对的，而且**首个命中就定案** —— 所以最贵的是那些**本来就不该被任何
    规则命中的包**，而设备上大多数包正是如此。规则上千条时，每个包要逐条比对一遍：本机实测
    （面板规则的形状）1000 条规则每包约 155 微秒，单核吞吐掉到约 1/19。
    现在：一条规则如果以「某协议已经报出了东西」为前提，而该协议对这条流还什么都没报出来，
    就整条跳过、不做比对。同一实测：1000 条规则每包 82 微秒（−47%）。
    **边界说清**：这只在「该协议还没报出属性」时生效；一旦报出，仍然逐条比对 —— 它不是
    "规则多也不花钱"，而是把最贵那一类的常数降了两个数量级。
  - 【修·性能】UDP 流的内核旁路曾被协议分析器废掉。
    现象：少数 UDP 协议的分析器在长流量下永远不结束，于是那**整条流永久留在用户态逐包处理**
    ——吞吐塌陷，而面板上一切正常。现在只在握手期可判的 UDP 分析器被限制在有界窗口内
    （按字节算），保证能结束、能拿到内核旁路。配了两个护栏测试：普通 UDP 流必须到达内核旁路、
    每个分析器必须有界（且每条都要写清"它靠什么结束"）。
  - 【新：规则列表显示「命中数」】回答运维第一个会问的问题 —— 哪条规则从来没生效过。
    四种状态各自说**当时唯一成立**的那句话，顺序是刻意的：**已停用**（命中为 0 是因为你关了它）、
    **未生效**（引擎没接管时计数天然是 0，此时的 0 不是"数据是零"）、**从未命中**、**数字**。
    规则列表同时加了筛选框、「符合/总数」计数、两种空态（还没有规则 / 筛不出来）。
    顺带修掉一个崩溃：引擎只造了一半时读状态会空指针（此前 `/api/rules` 真会挂）。
  - 【修：DNS 引流不限来源】+ 面板上说明"这些查询本来是谁在处理"。
    引流：只要走这台设备当网关，LAN 与 WAN 一视同仁（空来源＝不限来源）；仍然 fail-closed 的是
    **目标**（没有显式目标就什么都不装，绝不猜一个解析器）。显示：从内核 nat 规则读出爱快的
    DNS 处理路径（强制改向 / Clash 接管）并显示在卡片上，只认**会被执行**的链。同时修掉
    「检测到却没人说」的环境告警。
  - 【ZeroTier 切断改用共信号】低置信度判据不单独切断。
    动手时逮住一个差一点写进去的错误：原计划叠加的共信号（非标端口、证书元数据）都是
    **TCP 专属**分析器，而 ZeroTier 走 UDP 9993 —— 照抄会写成一条**永远不触发**的规则，
    表现为面板显示"已拦截"、内核里一条都没拦。现在用 UDP 侧真会出现的判据。
  - 【正名】产品名由 WAF 改为 **ALF（应用层防火墙）**，界面与文案统一；构建加了命名闸门，
    防止改回去。

1.63.0
  - 【规则模板：控制面域名】规则管理工具栏新增「模板」，一件事一步到位：挑一条 → 域名自动
    填进批量录入框 → 你看过再按录入。**模板只填空、不创建任何规则**（新装仍然是空的，符合
    "第一次拿到时一个项目也没有"）。两条：
    · Tailscale控制面 —— tailscale.com 及其子域。客户端靠域名找控制面与 DERP 中继，指到
      黑洞就协商不出新的连接参数。
    · ZeroTier管理台 —— 只挡管理台，并**明说数据面拦不到**（客户端内置 root/planet 服务器
      IP、不查域名），要拦数据面得选「协议 ZeroTier」或按 UDP 9993 拦。
    每条模板都带**边界说明**并显示在录入弹窗里 —— 一个"看着像拦住了、其实什么都没拦"的
    规则，是这个产品最犯不起的错。

  - 【去掉两处重复】顶栏原来同时显示「拦截已开启」与「引擎运行中」—— 它们是同一个事实的
    两种说法（引擎的起停完全由拦截开关驱动，见 internal/engine 的 tick）。删掉引擎芯片，
    顶栏保留四个互不重复的项：拦截状态（三态）/ 模式 / RST / 入队。规则页状态条同理删掉
    「引擎接管」与「入队策略」两格，只留顶栏没有的「拦截范围」与「自定义端口」。
    其中「引擎接管」是**两态**的，引擎起不来时会显示成乐观的绿色、与顶栏准确的红
    「拦截未生效」直接打架 —— 正是这一版要消掉的那种错觉。

  - 【修正一处说错的注释】spec.Settings.Enabled 上原写「关掉时引擎仍运行、只是不装 NFQUEUE
    规则」，与代码不符，而且正是顶栏把两个状态并列成两个芯片的源头。已按代码改正：
    关掉时引擎的整个核心被停掉。**注释说错一件事，界面就会跟着错。**

  - 【真机验证记录】docs/dns-hijack-device-verification.md —— DNS 引流（A 方案）的规则形态
    在真机上验证通过，并量出了设计里关键的一条：在 nat 表里**一条规则做出 NAT 决定后，
    该 hook 的后续遍历即停止**，所以插在 PREROUTING 第 1 位的 DNAT 是终局，iKuai 自己后面
    那条笼统的 :53 DNAT 与 Clash 的 REDIRECT 都覆盖不了它。

1.62.0
  - 【协议类目不再"预设可删"】新装打开「规则管理」是**空的**，要什么自己加 —— 这是用户明确
    要求的产品语义：「第一次拿到的时候里面一个项目也没有，需要它自己创建，而不是预设可删除，
    这跟预装APP有啥区别」。删掉配套的「已删除的协议类目：… 恢复」条，以及 `deleted` 这套
    "关掉+记名"的第二状态：只有开着（在列表里）与关着（不在列表里）两种。副作用是好的 ——
    不再存在"规则还在生效、界面却把这条藏起来"的状态。
  - 【补上 5 个 VPN/组网协议类目】引擎一直认得出、面板却一个都没暴露：
    · SSL VPN（AnyConnect / 深信服EasyConnect / Fortinet / GlobalProtect …）—— 判据是 TLS
      握手里服务端证书的厂商字样（这类流量信令全是密文，证书是唯一明文可见的身份）
    · IPSec（IKEv1/IKEv2）、L2TP（L2TPv2）、Tailscale、ZeroTier
    这组漏掉的原因是结构性的：面板的类目表是手写的，引擎里加了分析器不等于面板里加了类目。
    所以顺带把"引擎真的挂着它"这条断言补进测试表（sslvpn 此前一直在表外）。
  - 【Tailscale 的切断只认确定判据】分析器把 disco 魔法值与"默认端口 41641"两条路都报出来，
    并用 method 标出用的是哪条；端口那条自己的注释写着低置信度（可能误判该端口上的原生
    WireGuard）。按既定的"低置信度判据不许单独切断"，切断表达式写 method == "disco"。
  - 【PPTP 没有实现】引擎里没有 PPTP 分析器（只有端口表里的 1723 一条记录），所以它不是
    "忘了加类目"，而是**没有判据可用** —— 与其编一条包不住的说法，不如不做。
1.61.0
  - 【前端：侧栏收敛为八项、全部四字】运行概览 · 规则管理 · 流量日志 · 网站记录 · 应用管控 · 行为审计 · 威胁情报 · 系统设置。
    [应用] 就地改名 [应用管控]，并去掉原先那个不可点的分组标题 —— 同一个名字在侧栏出现两次、还多一层
    缩进，是看过去"没做完"的根源。侧栏现在完全扁平。「应用规则库」「关键字阻断」并入「规则管理」的
    页签（关键字 / 规则库）：它们本来就只是规则的不同类型，各占一行一级菜单才是不整齐的根源。
  - 【规则管理从 0 项开始】原来把引擎内置的十六个类目（证书异常 / 非标端口 / Trojan / …）全部铺成
    "规则"行，于是新装打开先看到十六条自己没写过的规则。改为只显示真的开着的：新装为空，加一条才
    出现一条。类目没有消失，回到"新建规则"的类目选择器里按需引用。
  - 【生效范围成为全局设置】移除协议规则里的「生效终端」选择器 —— 本防火墙对全体列管生效，范围是
    全局属性，不该逐条规则配。新增「系统设置 › 生效范围」：生效设备与覆盖端口，**两个框都留空即全局**，
    与后端 all 语义一致。数据字段保留，老模型里已写过的值不会被静默丢弃。
  - 页签改名：[管控] → [管控规则]，[主机] → [主机列表]。类目名 [TLS/HTTPS] → [TLS]。
  - 构建闸门同步更新且没有放宽：项数 10 → 8；新增"侧栏不许再有分组标题"、"每一项必须恰好四字"
    （直接数长度，不靠人眼）、"关键字/规则库必须真的是规则页的页签"（否则少两个菜单项就只是把它们
    删掉了，那是一次静默的功能删除）。
1.60.0
- 【新增「概览」页，导航九项 → 十项】聚合统计终于有了自己的家。
  原来四个累计计数器挂在**页面外壳**上，于是九个页面每页都印着同一组与它们无关的引擎计数器，
  而"聚合统计"本身没有地方安放。现在新增第一个导航项「概览」：
  · 拦截效率（速率/拦截率/资源占用/协议分布/规则命中排行，带时间窗口）
  · 累计统计（四个计数器），并**标明口径**「自引擎启动以来累计」——
    不标的话"累计包数 701"看不出是今天的还是上周的
  · 引擎未运行时显示「无数据」而不是继续挂着累计值：累计值不随引擎停止变化，
    所以它永远不会自己暴露"引擎没在跑"这个矛盾，必须由界面说
  参照 SmartDNS 的形态（它整个面板只有登录页 + 仪表盘：聚合统计只活在一个地方、每个数字都带口径）。
  其余九个页面不再显示这组全局数字，各管各的数据。
- 构建闸门的相应变更（契约变了闸门要跟着改，但不能放宽成永远为真）：
  · 导航项数 9 → 10，且**第一项必须是「概览」**（只判"存在"不够：挪到末尾同样能过计数，
    而那正是这个产品决定要保证的东西）
  · 统计键断言从 App.tsx 外壳区段移到 overview.tsx 的统计卡区段，并收紧 awk 退出条件
  · 新增反向断言：指标条**不许**回到页面外壳上
1.59.0
- 【统计判据不再单独拥有切断能力】Trojan/VMess/Shadowsocks 三条的判据是**统计性**的
  （决策树/请求头长度/首包长度），而面板原来允许它们各自切断 —— Trojan 那条自带 1% 误报率，
  一次网页加载有几十上百条 TLS 连接，拦掉百分之一就是「网页打不开」。现在这三条分开两件事：
  **识别**用原判据（记录照旧，能看见才调得动规则），**切断**要求再命中一个独立判据
  （「非标端口」或「证书异常」）。代价写在界面上了：配置规范的 Trojan（443+有效证书）
  只被记录、不被切断 —— 宁可漏报，也不让一条统计判据独自拥有断网的能力。
  确定性判据（明文 VLESS 请求头、明文 SOCKS 握手）不受影响，照旧可切。
- 【规则分发（服务端）】主控可以把本机规则开放给子路由拉取，模式与爱快 AP 的胖瘦一样。
  子路由在「规则来源」里填 https://<主控IP>:9170/naixifw/rules.json 即可。要点：
  · 令牌**可选**。不设 = 明文提供（局域网内任何主机都能读到你的规则，面板上有醒目提示）；
    设了 = 必须带令牌、内容用 AES-256-GCM 加密，令牌同时是密钥，所以下限 16 个字符。
  · 认证与面板**分开**：这个端点不套面板的 Basic（子路由按既有契约发 Bearer，
    套上去永远 401），用的是它自己的分发令牌。
  · 只吐规则，**绝不吐本机凭据** —— 本机的拉取令牌与分发令牌在响应里整个键都不存在
    （不是「置空」，置空靠不住：omitempty 对结构体无效）。
  · 响应带 ETag（按明文算）。加密报文每次 nonce 都不同，按密文算的话子路由会永远判成
    「内容变了」并每轮重写一次闪存。
  · 面板上能看到「最近一次被谁拉过」，否则无法判断子路由到底有没有在拉。
1.58.0
- 【引擎内嵌 v0.19.3】修掉一个会误伤自己人的误报：判「非标端口」时只跳过了私有/环回/链路本地，
  而 Go 的私网判断**不含运营商级 NAT 段（100.64.0.0/10）**。于是「目的地不在公网」的流量
  （例如自己那条落在 CGNAT 地址上的 VLess 节点 100.111.222.1:8765）被报成"非标端口"，
  开了非标端口拦截就把自己的隧道切断了。现在判据是「目的地是不是公网」，CGNAT、
  0.0.0.0/8、RFC6890、RFC2544、组播、广播、保留段一并跳过。
- 【引擎内嵌 v0.19.3·另一处】更正一条会误导真机验证的结论：早先认为「改写只认单包 SYN 带载荷」，
  那其实是**测试规则集没有分析器**造成的假象（规则匹配被 `if updated || s.virgin` 门控，
  只有分析器能产生 update）。真机面板的规则集带着 observe 规则，所以**真实客户端那种
  "握手后数据包里带 ClientHello"的形态是会被改写的**。真正不改写的是跨包分片的 ClientHello
  （尾段到达时重组数据长于本包载荷，改写会破坏流）。
- 【引擎配置不再有任何注释】config.yaml 原来带一整段键说明。它是**生成的**、每次改设置都重写、
  且要能原样同步到别的节点 —— 里面的注释要么是噪声，要么在第一次保存后就与下面的值不符。
  键的解释留在编辑它的地方（面板）。新增闸门测试，防止注释再回来。
- 【左侧导航】「应用规则库」收进新的不可点组标题「应用管控」下，与「应用」相邻
  （组标题回答"这一片在管什么"，不是"点这里去某页"）。
- 【应用页签改名】「实时流」→「实时流量」，「内核识别缓存」→「内核缓存」，正文里的旧名一并改掉。
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 字节）——包内文本过大会导致注册失败。）
