1.29.0
- 【严重修复】**域名/关键字规则在真实流量上求值失败，且 HTTP 那一层从未匹配过任何流量。**
  现场报错：`规则 panel:domain:d-1:http 求值出错：interface conversion: interface {} is nil,
  not string`。查下去是两个叠在一起的缺陷，两个都**无法从面板上看出来**：
    ① `http.req.host` 这个字段**根本不存在** —— 引擎的 reqMap 只有 method/path/version，
       Host 在 `req.headers.host`（键名小写）。所以每一条 HTTP 域规则都从未匹配过流量。
    ② `x != ""` **守不住 nil**：引擎把分析器属性暴露成 map[string]interface{} 且以
       Strict=false 编译，缺 key 取到 nil，而 `nil != ""` 判为真 —— nil 被送进
       hasSuffix/matches 就报错。中间层缺失时连读字段本身都会失败（`tls.req.sni` 在
       req 不存在时炸；`dns.questions[0]` 在 questions 缺失时报 `cannot fetch 0 from <nil>`）。
  后果比"报错"严重：规则求值失败即**不匹配**，也就是该拦的放过去了。
- 【修复】三层（TLS SNI / HTTP Host / DNS 问题名）改为**逐层守卫**的属性路径，且字段名
  修正为 `http.req.headers.host`。四种写法在引擎里实测对比过：只加 `!= nil` 仍会在中间层
  缺失时报错，`?? ""` 只能救叶子、救不了中间层。
- 【测试】新增验收（`internal/core/panel_expr_test.go`），断言分两层，缺一不可：
  **必须真命中**（`http.req.host` 那种不报错但永不命中的字段名错误，只有这一层能抓到）
  + **在任何残缺流型上都不许有求值错误**（16 种流型 × TCP/UDP）。共 21 项。
- 【界面】协议屏蔽的模式名「仅透视（记录不切断）」改为「监测」。
- 【界面】新增域名规则默认勾选「含子域」，并**记住上次的勾选**（模板偏好，存 localStorage）：
  域名规则通常成批添加，每次重勾是重复劳动，漏勾会让规则少匹配一层 —— 而面板上看不出
  规则只拦了主机名、子域全放行。
- 【界面】修掉「在协议屏蔽里拨开关保存后跳回域名规则页签」：保存会 reload，而 `if (loading)
  return <Spin />` 把整棵子树卸载重挂，页签是 antd 的内部状态、随之重置；滚动位置和展开的
  表单也一起丢。改为只在**首次**加载时占满页面。
1.28.0
- 【新功能】**DNS 改写**（拦截域名返回预设 IP，如 127.0.0.1）。此前面板上有这个开关，
  但后端只把它记进"不支持"列表 —— 打开它什么都不会发生，而且没有任何报错。引擎其实
  一直支持（`modifier/udp/dns.go` 的 dns 修饰符 + `action: modify`），缺的只是面板
  把规则生成出来。现已接通：被拦域名的 DNS 规则改为 `action: modify`，应答里的
  A/AAAA 换成面板填的地址。
- 【关键取舍】开启改写后，该域名的 **DNS 拦截规则会被改写规则取代**，而不是两条并存。
  原因：引擎对一条流只在**首包**做规则匹配，而首包是 DNS 查询（改写对查询必然失败、
  退化为 maybe），紧随其后的拦截规则就会把查询丢掉 —— 应答永远不来，改写也就永远
  不会发生。两条并存 = 改写静默失效。TLS / HTTP 的拦截不受影响。
- 【安全】改写地址在生成规则前逐条校验：非法地址（含"IPv4 位置填了 IPv6"）会被忽略
  并退回拦截，并在面板上点名。原因是引擎在**编译期**解析这两个地址，非法值会让整份
  规则集装载失败 —— 一个填错的格子能带走全部规则，而面板上看起来一切正常。
- 【改名】显示名改为「应用防火墙」；应用介绍与安装说明按产品给定文案重写。
- 【产品】去掉旧面板动作下拉框里的「白名单放行」：本插件只做黑名单（需要白名单放行
  的场景，爱快自身的防火墙/上网控制做得更完整）。vendored 的 html 保持逐字节不变，
  改动走**服务时替换**（第 5 处）。规则语言里仍保留 `action: allow` —— 老配置可能
  已经写了它，从语言里删掉会让那些设备升级后起不来；只是不再对外提供入口。
- 【统计】面板的"DNS 改写次数"接上真实计数。引擎只在改写**失败**时回调 ModifyError，
  成功时只报一条 `action=modify` 的流动作，所以这个数是从流动作里数出来的。
- 【测试】新增 DNS 改写验收：形状断言 + **真编译一次**（用 core.ValidateRules 让引擎
  实际编译面板生成的规则，形状写错会在编译期暴露）+ 非法地址的四种情形 + 关掉开关必须
  回到拦截；另有前端闸门验证旧面板里确实不再有白名单选项。
1.27.0
- 【变更】识别范围只剩「全量接管」，「辅助增强」已移除。原因：那一档只把内核未识别的
  流量交给插件，而内核对一条流做分类是在它**开始带载荷之后** —— 于是它只把握手报文交给
  插件、把带载荷的报文留在内核，依赖载荷的规则（域名 / SNI / Host / 协议识别）**永远
  匹配不上**，且全链路没有任何报错。表现是「规则在、拦截开着、什么都拦不住」。
- 【默认】交付即「网关模式 + 全量接管 + 全部端口」，装完不用改设置就能拦到东西。
  要省 CPU 请收窄端口（queueOnly: custom + queuePorts），而不是减少"看多少流"。
- 【兼容】老配置里的 scope: assist 启动时自动迁移为 full 并写日志说明；面板写入侧直接
  拒绝（一个已不存在的档位不该还能被保存）。
- 【修正】纠正了一条写在代码注释里的错误结论：曾称"爱快 LAN 是网桥、桥流量不经过
  netfilter，所以插件看不到 LAN 客户端流量"。实测（82 测试机、全量接管）不成立 ——
  爱快的 LAN 与 WAN 各自是网桥，而被**路由**的流量照常经过 netfilter：发 20 个转发包，
  队列规则的计数与 mangle FORWARD 的转发计数完全一致（各 +20）。真正看不到的是二层
  桥接串接那种部署，它由环境检测明确拒绝启动。
- 【新增】「本机模式 + 全部端口」现在被拒绝：该组合实测会把设备自身出网饿死。以前它靠
  "自动降级成辅助增强"兜底，assist 移除后兜底没了，所以改成显式拒绝并把改法写在错误
  里 —— 顺带给「只做 DNS 的旁路由」指了路（那类部署的 DNS 发往设备自身、走 INPUT，
  只有本机模式 + 端口 53 看得到）。
- 【新增】部署层面的警告接到面板：开着流量加速（FASTOFFLOAD）时提示"被卸载的长流不再
  经过 netfilter，插件只看得到每条流的开头"。这条以前只在启动日志里，而它正是
  「规则配了却总有漏网」的原因。
- 【测试】闸门与用例盯住这次移除的原因：唯一档位必须是 full、辅助增强的队列筛选
  （appmark / ikverdict）不许回来、assist 必须被迁移而不是被拒绝、前端不许再展示
  「辅助增强」、部署警告必须存在且接到 /api/rules 上。
1.26.0
- 【修复】**面板入口指向错了**：新界面（React + Ant Design）此前只挂在 `/ui/`，而应用
  市场的「打开」按钮读的是 `APP_WEB_LOGIN_URI=/dashboard`，那个路径落在**旧界面**上。
  结果是新界面做完了、也装进包里了、直接访问 /ui/ 也正常，但操作员日常点进去看到的
  一直是旧界面，而且没有任何报错。
- 【调整】新界面现在挂在**根路径**：应用市场按钮、直接访问 `https://<设备IP>:端口/`
  都是它；`/ui/` 作为等价前缀保留。
- 【保留】旧界面没有删除，退到 `/legacy/` 下 —— 新界面出状况时还有一条熟悉的路可走。
- 【兼容】`/dashboard` 重定向到新界面。`app/.env` 在升级时会被保留（应用市场只在它
  不存在时才创建），所以**已经装在设备上的实例**不会因为新包改了默认值而跟着变；
  有这条重定向，老设备不重装也能切到新界面。
- 【新增】面板入口的路由测试（7 项）与打包闸门：根路径必须是新界面、`/dashboard` 必须
  重定向、旧界面必须在 `/legacy/` 且资源引用已改写、未注册的 `/api/` 路径必须仍是 404。
  这类「功能做好了但入口指错地方」的问题以前没有任何东西能挡住。
- 【修复】未注册的 `/api/` 路径此前会回落到面板 HTML（根路径兜底带来的副作用），
  接口写错字会收到 200 + HTML；现在仍是 404。
- 【修复】`start.sh` 的端口占用检查排在「本插件自己的实例」检查之前，于是覆盖安装或
  重启时会把**自己的旧进程**误报成「端口被占用」并退出 1 —— 真实后果是新版本没生效、
  旧进程继续跑旧代码。现在先停自己的实例再判端口；pidfile 丢失时按端口反查占用者身份，
  只对确属本插件的进程动手，端口真被别的程序占用才算配置冲突（同样退出 1）。
- 【修复】守护进程缺少可执行位时自动补上并记日志（安装链路只 chmod `scripts/*.sh`，
  `app/bin` 依赖解包时的权限位，丢了会表现成「包坏了」）。
1.25.0
- 【新增】`naixifwd` 增加**内核机制诊断与「特征编译」子命令**，与守护进程同一个
  二进制（因此自动匹配设备架构，不需要第二份工具）：
  · `-kernel-status`：只读汇总内核各机制状态（DPI/用户DPI/L4识别表/URL过滤器/
    应用规则条数与命中数/识别缓存条数/特征库版本）。这些开关的**命令返回值不可信**，
    只能回读状态，所以需要一个"一眼看清哪条链路是活的"的地方。
  · `-gen-userdpi`：把一条载荷特征编译成**内核用户 DPI 特征库**（内核原本不认识的
    特征，装载后由内核自己识别、线速执行）。支持关键词子串与精确匹配两种方式。
  · `-gen-l4`：把「识别出的服务器地址」编译成内核 L4 识别表（IP[:端口][/协议]）。
  · `-gen-urlfilter`：把域名/关键字规则编译成内核 URL 过滤器数据。
  · `-decode`：回读以上三种数据，便于在设备上核对"内核到底收到了什么"。
  背景：这三类数据都必须是 protobuf 二进制，而设备上没有 protoc；由插件自己把
  规则编译成内核认的字节，是唯一不引入第二种工具链的办法。
- 子命令一律在守护进程启动前完成并退出，不触碰运行状态。
1.24.0
- 【新增】面板直接显示每条规则的**内核状态**与**命中次数**。来源是内核自己的
  汇总节点（/proc/ikuai/stats/ik_summary 的 "tc rules Info" 段）。
  这一列存在的理由：库里"有这行"、日志里"打印了 Add"都只能证明**装上了**；
  只有内核自己的生效判定与 match 计数才能说明**它在工作**。
  规则可能是"未生效"（不匹配任何流量）而在外部完全看不出异常。
- 【新增】有规则在内核里未生效时，页面顶部给出明确告警（红色），并在规则表
  标题里点出条数 —— 这是最容易让人白折腾的一种失败。
- 读到内核状态失败时安静降级：该列显示"未知"，不影响规则列表其余功能。
1.23.0
- 【严重修复】此前创建的每一条应用规则在内核里都是**不生效**的（状态 inactive、
  永不匹配），表现就是"配了规则却没有任何效果"。根因有两层：
  ① 内核判据（ik_acl_app.c）是"时间集名字为空 → 永远生效；否则必须能查到该
     时间集且当前时间在集内"；
  ② 而系统的模块用 `jq '[.custom[]]'` 判断有没有时间限制，**空数组会输出字符串
     "[]"，是"非空"** —— 于是它建出一个空的时间集，规则引用之后永远判不到生效时段。
  规则照样落库、内核日志照样打印 Add、内核里也查得到，所以从外面完全看不出问题。
  修法：不再下发 time 参数（本插件不提供时间限制），实测内核状态变为 active、
  时间集为空。已加回归断言防止有人把 time 加回来。
- 【验证工具】意外发现内核本身就暴露了每条规则的**命中计数**：
  /proc/ikuai/stats/ik_summary 的 "tc rules Info" 段会打印每条规则的
  action、应用集与 match:N。这是唯一能证明"规则真的匹配到流量"的信号
  （dmesg 只能证明"装上了"）。
- 双架构包（amd64 / arm64）一并产出，便于同步测试。
1.22.0
- 【定位】本插件的定位明确为「强化系统 DPI 能力的辅助增强」：不自己重做识别，
  复用系统内核已有的数千个应用识别，把结果变成可管理的应用层拦截规则，
  规则写进系统自己的配置表（acl_l7），重启由系统自己重放。
- 【调整】规则的 tag 改回哈希（前缀 + 规则名哈希），规则名写在 comment 里，
  comment 就是这条规则的标签。这样 tag 必然合法（系统对 tagname 有限制：
  实测 15 字符上限、不接受下划线/点/空格/斜杠），也不会因为名字里有中文
  或超长而被截断成看不懂的样子。
- 【新增】系统里已有的应用规则现在也在面板里列出（标为「系统规则」），
  让面板呈现完整的配置面而不是只有插件那一半。系统规则**只读**：
  可以看，但插件不会去改、不会去删 —— 它们请在系统自己的界面里修改。
- 【新增】重叠提示。系统的优先级是跨规则统一排序的，所以插件规则与系统规则
  命中同一个应用时只有优先级高的那条生效。这处重叠会：① 在规则行上标注
  ② 建规则时当场把"谁将生效"讲清楚 ③ 在页顶汇总。界面看起来"拦了却没效果"
  的问题，绝大多数就出在这里。
- 【加固】归属校验必须同时验证标签前缀（此前只按 id 匹配）。列表改为返回
  系统规则后，这个疏漏会让插件能改到操作员在系统界面里自建的规则 ——
  由新增的用例抓出并修掉。
1.21.0
- 【新增】应用管控：按应用拦截或放行。这在产品定位上是核心能力 ——
  系统内核本来就能识别 3400 个应用（并在转发路径上给每个连接打好 appid），
  此版本把它变成可管理的规则：搜索应用（按名字或 appid）→ 选拦截/放行 →
  定优先级 → 下发，可启停、可删除。
- 【新增】规则落进系统自己的配置（config.db 的 acl_l7 表）并即时装入内核，
  因此重启后由系统自己重放，不会丢；也会出现在系统原有的应用管控界面里。
  实现路径是系统自己的模块接口（/usr/ikuai/function/acl_l7），
  不是插件维护的一套影子规则 —— 真机验证内核日志确认收到规则。
- 【安全】插件只列出、只修改、只删除**自己创建的**规则（标签前缀 naixifw-）。
  在系统界面里自建的应用规则这一页看不到、也删不掉：一个能误删别人规则的
  插件比一个功能少的插件危险得多。
- 【安全】下发前逐个校验应用名。系统对不认识的应用名是**静默跳过**的，
  不校验就会得到一条"装上了却不匹配任何流量"的规则，操作员只会以为
  "拦了但没生效"。现在名字不认识直接拒绝并指出是哪一个。
- 【重要】规则在系统界面里的名称改为**可读**。tagname 就是系统界面显示的
  规则名（实测确认），原先用哈希会让人在系统界面里看到一串 naixifw-a1b2c，
  等于没有名字 —— 规则明明长在系统里却认不出来。现在优先用规则名本身
  （过滤掉系统会拒的字符、按实测的 15 字符上限截断），只在可读形式为空或
  与现有规则撞车时才回退到短哈希。
- 【性能】应用清单只解析一次并缓存（6808 条名字映射）；搜索在服务端做，
  面板每次最多取 40 条 —— 把 3400 个选项全发给浏览器会让界面明显变卡，
  而面板常常是从很远的链路打开的。
- 【测试】新增 internal/appfw 的用例：参数形状（app_proto 的 .custom/.object
  必须与系统脚本的解析一致）、拒绝非法动作/优先级/名字、只处理自己的规则、
  删改前必须确认归属、中英文名字合并、搜索命中名字与 appid。
1.20.0
- 【修复】「应用分析」页白屏：接口给的 path 是字符串，前端按数组调了 .join()，
  抛异常后 React 卸载整棵树，整页变白。现在两种形状都能接。
- 【新增】页面级错误边界：任何一页渲染失败只在内容区显示错误与该页信息，
  侧边菜单仍可用，不再整站白屏。此类问题原先用户只能看到一片白、无从判断。
- 【性能】静态资源开始压缩：实测 antd 分块原样传输 1,043 KB，压缩后约 336 KB，
  省掉约 2/3 字节。压缩结果常驻内存，不做每请求重算（路由器 CPU 弱）。
- 【性能】静态资源开始可缓存：此前没有任何缓存头（embed.FS 的修改时间为零值，
  Go 连 Last-Modified 都不发），浏览器每次打开面板都要重下 1 MB。
  带构建哈希的产物改为长缓存 + immutable，首页保持每次校验；条件请求返回 304。
- 【外观】建立统一设计令牌（间距刻度 / 颜色语义 / 圆角 / 控件高度），
  由 antd ConfigProvider 与 CSS 变量共同承载，改一处全局生效。
- 【外观】侧栏与内容区做出表面区分，品牌区加标识块，侧栏底部补状态与下沿。
- 【外观】状态条改为等宽四列网格，值改中文（原来直接显示 assist / all），
  空值区分「未设置 / 不适用 / 未填写」而不是一个看起来像坏了的「-」。
- 【外观】九个页面各自一句有信息量的副标题（原来九页共用同一句，等于零信息）。
- 【外观】表格统一：斑马纹、悬停高亮、表头加重、单元格顶对齐；分类路径与
  数字/标识符用等宽字体（比例字体下扫一列数字很累）。
- 【外观】空状态给出「这里会出现什么 + 怎么让它出现」，不再是一片白加一个
  很小的「暂无数据」；标签页计数弱化为小徽标，不再与标签正文同权重。
1.19.0
- 新增「内核 DPI 开关」由插件接管：高级设置里可开关内核的 QUIC 解析与 HTTPS 解析，
  并可选择是否落盘。插件启动时与保存设置时按设置调 ik_cntl 应用（「以插件为准」）。
  落盘会把爱快配置库的 mode 置为 3（自定义模式）——只有自定义模式不在开机脚本的
  重算分支里，否则设好的 quic=1 会在下次开机被「平衡模式」的分支覆盖回 0。
- 【重要结论，实测】这些内核开关**不会**让本插件看到更多流量，别误以为开了就有用：
  爱快的 LAN 口是网桥，桥流量不经过 netfilter/iptables；实测内核里也没有 br_netfilter
  模块，无法把桥流量接进来。因此 LAN 客户端的流量根本到不了插件的队列 —— 这与
  「辅助增强 / 全量接管」的选择无关，两者都收不到。
  证据：让一台假 LAN 客户端（接进 lan1 桥 + 从爱快 DHCP 拿到租约）发出真实 QUIC，
  tcpdump 在 lan1 上抓到这些包，但在 raw PREROUTING、mangle PREROUTING、
  filter FORWARD 三处同时挂的 NFQUEUE 规则命中数全部为 0。
  这些开关的意义在于让爱快自己的 DPI（云防火墙类功能）认识 QUIC。
- 设备上新增一键救援脚本 /etc/log/naixifw-rescue.sh：插件状态、内核开关、爱快配置库、
  实验残留（netns/veth/临时 iptables 规则）全部恢复到常态，幂等可反复执行；
  另有 /etc/log/naixifw-rescue.sh status 只查看当前状态。
1.18.0
- 修一个只有真机对照才看得出来的静默失效：引擎只实例化规则表达式引用到的分析器，
  而默认规则只引用了 dns —— 于是 QUIC 与 TLS 分析器根本没被加载，行为学判定的
  输入（QUIC 指纹、TLS 的 ALPN）始终不存在，面板上的计数恒为 0，且没有任何报错。
  面板现在恒定生成三条「观察规则」（observe: true）：只为让引擎加载 quic/tls/dns
  分析器，不做判定、不记录日志。
- 引擎升到 v0.8.0：新增 observe 规则类型。不参与求值（匹配时直接跳过）、不产日志；
  编译期拒绝 observe 与 action 并存（那会让 action 被静默忽略，属于骗人的规则），
  也仍然拒绝既无 action/log/observe 的规则（拼错的 action 不该变成静默空规则）。
- 面板显示内核 QUIC 开关：真机实测内核 ik_features_status 里 quic: disable 时，
  设备的 QUIC 流量不会进入用户态引擎。此时面板直接说明「内核没有把 QUIC 交给插件，
  所以计数恒为 0」，而不是让人对着一个零去猜是自己网络的问题还是插件的问题。
- 性能：把每流的内存分配全部消除。
  ObserveTCP        110ns / 32B / 2 allocs  →  54ns / 0B / 0 allocs
  QUIC 判重         505ns / 256B / 8 allocs → 240ns / 0B / 0 allocs
  属性翻译          856ns / 660B / 10 allocs → 525ns / 2 allocs
  做法：热路径不再调 IP.String()（改用定长数组做键）、判重键不再用 Sprintf 拼字符串、
  去掉 TCPStreamNew 上一次必然读不到 ALPN 的多余取值。分配是要还的，
  在内存小的设备上比绝对耗时更值得盯。
- 新增基准测试（含并发与满载路径）与 observe 规则的不变量测试。
1.17.0
- DPI 引擎升到 v0.7.0：QUIC 分析器修跨包 CRYPTO 重组（此前对 hysteria 完全漏报）；
  新增 QUIC 客户端指纹（JA4(q) + transport parameters）；新增 DNS HTTPS 记录解析，
  能识别域名是否宣告 ECH。
- 新增「行为学判定」（实验性）：引擎的单流指纹对做了浏览器拟态的代理基本无效
  （实测 hysteria 与真 Chrome 的 transport parameters、Initial 补齐尺寸、
  ClientHello 长度、JA4 Part B 全部相同），于是改为看流与流之间的关系：
  判据 A「ECH 无据」——握手用了 ECH 但该域名在 DNS 里从没宣告过 ECH；
  判据 B「无前置 TCP」——从没和这台服务器建过 TCP 就直接上 QUIC，
  且该域名也没在 DNS 里宣告过 h3。
- 新增连接记忆表：按客户端记住「和哪些服务器走过 TCP」，并学习 DNS 里的
  域名知识（是否宣告 ECH / h3）。表有硬上限与超时淘汰，淘汰顺序确定不含随机。
- 判据 B 分两档，噪音不同请分开看：
  「无前置 TCP」= 我们确实见过该域名的 DNS 记录且它没宣告 h3（有正面证据，较净）；
  「无前置 TCP（DNS 未知）」= 没见过该域名的 DNS 记录（可能走了 DoH，明显更吵）。
- 修一个由负对照测试发现的真实误报：按 IP 直连（SNI 为空）的流量曾被判成代理。
  真浏览器连 IP 时 sni 就是空的，而最初实现把「没有域名」当成了「该域名没宣告 h3」
  的证据。没有域名就无从查证 h3 的可发现性 —— 无知不是证据。
- 行为审计页新增「行为学判定」面板：显示观测到的 QUIC 握手数、命中数、
  按判据统计与疑似名单。**恒带实验性标记，不参与规则匹配、不拦截任何流量。**
- 新增接口 /api/v1/behav/stats 与 /api/v1/behav/suspects。
- 修 transport parameters 指纹不稳定（同一浏览器连同一服务器 24 次连接曾得到
  24 个不同指纹）。
1.16.0
- 新面板（第一版）：左侧九菜单 —— 规则管理、流量日志、网站记录、应用分析、行为审计、
  应用规则库、威胁情报、关键字阻断、系统设置。React 19 + Ant Design 6，产物离线内嵌，
  挂在 /ui/ 下；旧面板仍在根路径，两套并存，新界面出问题时还有退路。
- 新增「关键字阻断」：对 SNI / DNS 查询名 / HTTP Host 做子串匹配（不区分大小写）。
  与域名规则的区别是语义不同 —— 域名规则匹配确定的名字（整名或后缀），关键字匹配一类名字。
  过去只能靠后缀匹配近似，那是另一种语义，会漏也会误伤。
- 新增「应用规则库」：直接浏览设备固件自带的协议库（当前 3.0.29 / 3339 条），
  支持按中文名、英文名、分类路径、appid 搜索。不内置静态清单 —— 协议库随固件升级，
  内置的那份迟早会和内核上报的 appid 对不上。
- 应用分析新增内核功能开关展示（实测这台设备有 28 个开关）。它是解释"某个列表为什么
  是空的"的钥匙：设备上 appids_stat 默认关闭，这正是主机活跃应用列表为空的原因。
- iframe 兼容：新增 CSP frame-ancestors 头。爱快管理页从另一个端口用 iframe 嵌入面板，
  端口属于 origin，不发这个头会被浏览器拒绝渲染，表现为白屏且服务端毫无错误。

1.15.0
- 插件改名为「奶昔防火墙」（NaixiFW）。原来的名字只是内嵌引擎的名字，产品不该叫这个。
  可执行文件 naixifwd、数据目录 /etc/log/naixifw、环境变量前缀 NAIXIFW_、包名 naixifw-<版本>-<架构>.ipkg
  一并改过来。引擎（DPI 核心）也独立出去了：它是本团队自己维护的私有 module
  github.com/nyarime/dpi-engine，不再以源码形式放在本仓库里 —— 它有自己的仓库、版本号、
  更新记录与授权归属，本插件只声明需要哪个版本。引擎里的授权与来源记录归它自己管
  （见该仓库的 PROVENANCE.md）。
- DPI 引擎抽成独立项目（github.com/nyarime/dpi-engine，私有仓库）。协议识别是引擎的职责，
  自研的 VLESS / IPSec / L2TP / Tailscale / ZeroTier 分析器随之归位到引擎的 analyzer 包，
  本仓库不再持有它们的源码。插件通过 module 依赖使用引擎，发布构建关掉工作区、只按
  go.mod 里声明的版本构建 —— 这样"打进包里的引擎是哪一份"是可复现、可追溯的，
  不会混入本地未提交的改动。
- 去掉全部向后兼容代码。本插件尚未对外发布，配置文件直接落在 /etc/log/naixifw/，
  不需要从旧路径迁移；旧版内核规则标记也不再被认作自己的规则。
- 清理引擎的对外品牌与来源物料：上游那份 README（描述的是它自己的 CLI 产品，并带有与
  本产品无关的政治化文案）、docs/、.github/ 不随迁；授权所需的 LICENSE 与来源记录保留。
- 新增 VLESS 协议识别。VLESS 的请求头是明文的，所以除了认出协议，还能直接读出它要访问的
  目标域名和端口 —— 面板「网站记录 / 应用分析」的"目标"一列就来自这里。判定链每一环都很窄
  （版本必须 0、附加长度必须 0、命令 1..3、地址类型 1..3），叠加起来随机数据全中的概率约
  1.8e-7。这是一条启发式规则、不是取证结论，改掉版本字节即可绕过；要用它做拦截请配合目标
  地址等条件。
- 新增「应用分析」：接内核 appid —— 内核里那套 DPI 已经按 3340 条协议给流量打好标签，
  我们**白拿这份结果**，不再自己造一份特征库。做法是读内核已经算好的统计
  （/proc/ikuai/stats/*、/proc/ikuai/dpi/*），再用设备自带的协议库
  （/etc/mnt/data/libproto.gz，含协议树与中英对照）把 appid 数字翻译成名字与分类路径。
  读设备自己的库而不是把语料编进二进制，是为了保证名字永远和内核一致：库随固件升级
  （当前 3.0.29），编死的话内核用新 appid 上报、插件还在用旧名字解释，显示错名字比
  显示数字更坏。新增四个接口：实时流（appid → 名字，实测 4116 条）、每主机应用与流量、
  识别命中排行（实测「其它HTTP」11.3 万次命中）、分类统计。协议库解析失败、内核统计
  读不到（非爱快设备）都会在响应里说明原因，而不是返回空数组让人以为"没有流量"。
- 部分修正 TLS 套娃判据的可调性：尺寸区间改为**从本机真实 ClientHello 分布自校准**
  （见引擎 v0.4.0），窗口实测从 200-1600 收紧到 207-346，属性里带 calibrated 标明来源。
- 新增 scripts/sslvpn-verify.sh：SSL VPN 识别验证脚本。它解决的第一个问题是"插件到底
  看到没看到这条流量" —— 辅助增强只把 appid 0 的流量送进引擎，如果内核自己把 TLS 认成了
  HTTPS，我们的证书指纹分析器根本没机会跑。脚本会给出内核给这条流打的 appid、NFQUEUE
  计数变化、以及引擎日志，并按结论给出下一步；probe 模式可临时绕过 appmark 只按端口入队，
  用来隔离"队列条件问题"与"分析器问题"。
- 新增 TLS 套娃检测（实验性）：识别"外层 TLS 里再套一层 TLS 握手"的形态（Trojan、
  v2ray 的 TLS 传输、各类自建隧道）—— 规则可写 nestedtls != nil && nestedtls.yes。
  内层握手的载荷是密文，链路上看不见；中间人唯一能看到的是外层 TLS 记录的**类型与长度**
  （记录头是明文，长度在加密后保留），所以这个判据本质是启发式，只看三个条件：客户端握手
  后的第一条载荷记录在 200-1600 字节（内层 ClientHello 的尺寸区间）、第二条小于 100 字节
  （内层握手的小记录）、第三条存在。只查第一条载荷记录，不做全文扫描 —— 位置本身就是判据
  的一部分。可预期误报面：首条载荷记录恰好在区间内、紧跟一条小记录、之后还有数据的正常
  客户端形态（普通 HTTP/2 因为序言只有 54 字节左右不会被命中）。
  **这一条建议先只记录、看日志里的统计，确认自己网络里的误报情况后再考虑拦截** ——
  它是在 24 条真实抓包上标定的（套娃 12 条、普通 HTTPS 12 条，全部判对），但还没有用
  真实代理流量验证过，属性里也带着 experimental=true 以便一眼看出。
- SSL VPN 厂商表从 12 家扩到 26 家：补上 Citrix/NetScaler、F5、Sophos、WatchGuard、
  Barracuda、Zscaler、Aruba、OpenVPN、SoftEther、DrayTek，以及山石网科、绿盟、启明星辰、
  奇安信、联软、锐捷，深信服的 aTrust 与 Ivanti（Pulse 后继）也纳入。这张表针对的是
  设备**出厂/默认证书**（多数管理员不会更换内部 VPN 网关的证书）；证书被换成不含厂商
  字样的仍会给出证书原文，可以按自己环境里的实际证书写规则。
- 新增 SSL VPN 产品识别：Cisco AnyConnect、深信服 EasyConnect、Fortinet、Palo Alto
  GlobalProtect、Juniper/Pulse、SonicWall、Check Point、Array、ocserv、华为、H3C、天融信。
  规则可写 sslvpn.vendor == "easyconnect"，也可以按证书原文自己写（sslvpn.cn contains "sangfor"）。
  这类协议跑在 TLS 里、握手之后的信令全是密文，网关侧读不到它的登录地址或协议载荷 ——
  唯一明文可见的判据是**服务端证书**里的厂商字样。所以判出来的是"产品身份"，不是协议内部
  结构的取证。两条边界必须知道：① TLS 1.3 下证书在 ServerHello 之后就被加密，此时只报
  sslvpn.tls13=true，判不出产品；② 管理员换成不含厂商字样的证书（自签通用域名证书常见）
  就认不出，此时仍会给出证书原文 sslvpn.cn / sslvpn.org 供自定义规则。
  实现上还要注意：证书往往跨 TCP 分段、跨 TLS 记录到达，所以入队窗口如果只看"首包"，
  要放宽到能覆盖证书（一般 <4KB），否则这个分析器看不到任何东西。
- 新增 VPN / 组网协议识别：WireGuard（已有）、OpenVPN（已有）、IKEv1/IKEv2（IPSec）、
  L2TPv2、Tailscale、ZeroTier。之前这些流量在内核和本插件眼里同样是"一段高熵字节"，
  只能靠"全加密流量"（fet）笼统归类，规则没法精确到具体协议。
- 每个协议各自一个名字，精度按可判性如实分级：
  · ipsec 最硬：版本低位必须为 0 + 交换类型合法 + 头部长度字段必须等于报文长度，
    三者同时成立才认，误报率可忽略，因此不依赖端口。
  · tailscale 靠 disco 报文里的 6 字节魔法值（"TS" + 😀 的 UTF-8 编码）确定 ——
    这个魔法值只属于 Tailscale。原生 WireGuard 握手与 Tailscale 的逐字节同构，
    光看握手分不开，所以另外补了两档：同一条五元组上先见过 disco（确定），
    以及 WireGuard 形状出现在 41641 端口上（标为 port_hint，低置信度，会有误报）。
  · l2tp 限 1701 端口，再加版本号 + 控制位 + 长度字段三重校验。
  · zerotier 只有端口 + 类型字节，代码里如实标成低置信度，不建议用于自动拦截。
- 关于"禁 Shadowsocks 会不会误伤 Tailscale"：不会。面板上的 shadowsocks/vmess 走的是全加密
  流量分析器 fet，而它是纯 TCP 分析器；Tailscale/WireGuard/IPSec/L2TP/ZeroTier 全是 UDP，
  两个分析器集合不相交。这条用测试钉住了（谁把 fet 改成也吃 UDP，测试立刻失败）。
  但要如实说清：**裸连接的 Shadowsocks 与 VMess 在流量层无法区分**（都没有可辨认的握手），
  面板上那两个开关的条件本来就是同一个。能区分的只有带握手的那些：
  VLESS（请求头明文）、Trojan（伪装成真 TLS）、以及上面这些 VPN 协议。
- 去掉所有向后兼容代码：本插件尚未对外发布，配置文件直接落在 /etc/log/naixifw/，
  不需要从旧路径迁移（未发布的版本不可能有旧路径的生产部署）。同时去掉旧版内核规则标记的
  兼容识别 —— 旧版内核规则标记不再被认作自己的规则。
  测试环境如果装过旧版，升级前先卸载旧包（旧包的 stop.sh 会清掉自己的内核规则）。
- 注意：改名会改变应用身份，应用市场里这是新应用，老版本的安装不会显示为"可更新"，
  需要卸载重装。

1.14.0
- 新增「受控设备」：可以把插件要看的流量限定在选定设备上，其余设备的流量根本不进插件。
  面板首页新增一张卡片（设备范围：全部设备 / 仅选定设备；设备清单支持 IP、网段、MAC，
  一行一个或用逗号分隔）。这是上游面板里 device_routing 那一档，本插件补上它，
  因为在此之前"只想盯住某一台设备"只能全量接管，代价是整个网络的每个包都要过用户态。
  实现：给选定设备的连接打一个 connmark（0x400，与引擎自己的 1001/1002 及爱快
  自身使用的连接标记都不冲突），入队规则再加一条 connmark 匹配。标记打在连接上
  而不是报文上，所以两个方向、后续每个报文都跟着走 —— 回程流量不需要再按
  "目的地址是内网设备"去猜。规则插在引擎入队跳转之前，与绕过名单同一位置。
- MAC 与 IP 都支持：MAC 扛得住 DHCP 换 IP（盯住"那台电脑"不会因为续租就断掉），
  IP/网段适合按用户组划范围。
- 选「仅选定设备」却留空设备清单会被拒绝，面板直接说明后果：这样一条流量都不会
  进入插件。本机模式下也拒绝这一档（本机模式处理的是设备自身的流量，没有"设备"可选）。
- 面板会提示口径差别：「仅选定设备」＋「辅助增强」下，内核已识别的流本来就不进插件，
  按设备也看不到它们的识别结果；要看全量流量要把 ikuai.scope 改成 full。
- 状态接口新增设备范围、设备清单、连接标记值与内核里实际装了几条标记规则
  （device_rules），能分清"设置存下来了"和"内核真的装上了"。
- config.yaml 的 ikuai 段新增 deviceScope / deviceList，手改文件同样生效。
- 绕过规则的计数不再把设备标记规则算进去（两者在面板上是两个数字，含义不同）。

1.13.4
- 修一个"开着但什么都没监测到"的静默失效：「仅透视」一条规则都不生成。
  面板三个协议按钮（全加密流 / TLS 伪装 / SSH）发的是 audit，后端只认 block，
  于是设成「仅透视」后 rules.yaml 里没有对应规则 —— 而引擎只加载规则里用到
  的分析器，所以这类流量压根不进分析流程：面板上「异常流量」卡片恒为 0，
  「实时流量透视看板」永远空白，操作者看到的是"没有异常流量"。
  更糟的是面板自己在教操作者用这个模式：「把协议设为仅透视即可在此实时查看」。
  现在「仅透视」生成一条只记录不拦截的规则（log: true，不写 action），
  命中即记录、流量原样放行；面板上三个协议卡片与看板因此才有数据。
  不认识模式值（拼错、上游新值）只会"少拦"，绝不会变成拦截。
- 「流量透视看板」不再拿日志当数据源。原来它读日志环再按文本猜协议，
  而日志环按等级与每秒预算裁剪：日志等级一调到 warn，或者一秒超过 500 条，
  看板就整页空白 —— 这跟"确实没有异常流量"长得一模一样，是测量功能最坏的错。
  现在引擎在日志门禁之前写入独立的透视缓冲（500 条，记录双向 IP 与端口、动作、
  命中规则、单调递增 id），记录不再受日志等级影响；新增的 SSE 事件让看板实时更新；
  「清空记录」只清看板，不再顺手动日志。「总分析包」等统计磁贴的计数行为不变。
- 辅助增强下的「仅透视」规则同样静默无效（内核分类过的流不再进插件，插件只看得到
  握手、看不到载荷），面板警告与启动日志现在把这类规则一并列出，而不是只报拦截类。
- 验证方式补强：新增端到端测试，用内存合成报文（裸全加密流 / SSH / 正常 TLS）
  跑真实的内嵌引擎，走「面板保存 → 生成 rules.yaml → 引擎编译 → 实际识别」全链路，
  断言「仅透视」下识别出全加密流量与 SSH、正常 TLS 不误报、且不切断任何流量。
  修好前该测试失败并直接指出"仅透视没有生成任何规则"。
- 面板模型新增透传保留上游的 device_routing 段：本版前端没有它的界面，
  但保存时会整体回写模型，之前被丢弃的字段会在操作者动任何一次设置后永久丢失。

1.13.3
- 修 1.13.2 没生效的 worker 上限：配置文件加载阶段把 workers.count 钳在 1-64
  （沿用旧值 + 记一条说明），而校验函数里的新上限 1-256 永远看不到这个值 ——
  实测在真机上把 count 改成 128 保存，接口回"已保存并重新加载"，但实际仍是 2。
  现在上下限抽成 spec.WorkersMin / WorkersMax 一处定义，加载与校验共用；
  新增测试同时钉住"128 原样保留、999 沿用旧值且有说明、加载结果能过校验"。

1.13.2
- 工作线程数上限从 64 放宽到 256：插件也部署在 128 核的机器上，64 线程把引擎
  卡在硬件之下（仍是合理性边界，不是推荐值 —— 家用路由远在到达上限前就先 CPU 瓶颈了）。
- readme 增加「全灭」模式的推荐配置与实测开销（约 17000 包/秒·核），并写明该模式
  能拦什么、拦不住什么：非 TLS 的全加密流量与 SSH 能拦；TLS 伪装的 Trojan / VMess
  按设计豁免（否则会误杀全部 HTTPS）；UDP 不处理（fet 只处理 TCP）。

1.13.1
- 识别范围警告补上「更省 CPU」的改法：不必把全部流量都拉进插件，可以 ikuai.scope=full
  配 ikuai.queueOnly=custom + ikuai.queuePorts（如 "53,80,443" 或某协议的服务端口），
  只让这些端口的流量进插件 —— 内核不再替这些端口做判断，于是插件能看到流的开头，
  载荷类规则就能生效。1.13.0 的警告只给了"全量接管"一条路。
  已在真机确认该组合生成的队列规则只含端口条件（不含 appmark/ikverdict 限制）。

1.13.0
- 修一个"开了但不会生效"的静默失效：识别范围为「辅助增强」时，依赖报文载荷的拦截
  规则根本不会命中，而界面上完全看不出来。现在会明确提示：
    * 面板顶部显示黄色警告条，列出受影响的规则名，并说明改法（改成「全量接管」）。
    * 启动日志里同样打印一条带 ⚠️ 的说明。
    * /api/rules 新增 warnings 字段。
  原因：辅助增强只把「内核未分类」的报文交给插件，而内核是在一条流开始带载荷之后
  才给它分类的 —— 于是这类流只有握手报文会到插件。域名 / SNI / Host / 协议屏蔽都
  要靠载荷才能判断，因此永远匹配不上。
  只对「拦截」类规则报警：只记录不拦截的规则不报警（默认安装不会挂常驻警告），
  IP / 端口这类不依赖载荷的规则也不报警。

1.12.0
- 配置文件位置改到持久分区：/etc/log/naixifw/config.yaml 与 /etc/log/naixifw/rules.yaml。
  原来在 /etc/naixifw，那属于固件分区（实测只有 129MB），固件升级或恢复出厂会把配置抹掉。
  升级时自动迁移：老位置的文件会被复制到新位置并逐字节校验，确认无误后才删除原文件；
  新位置已有文件时不覆盖。迁移会在启动日志里说明。
  另外处理了一个升级陷阱：应用设置里的路径是安装时一次性写入的，升级不会更新，
  所以老安装启动时仍被指向 /etc/naixifw；该路径现在会在读取时自动重定向到新位置
  （只针对恰好等于老默认值的路径，自己改过的路径不动）。
- 「原生配置预览」新增「重置为出厂设置」：把该文件恢复出厂内容。
  重置 rules.yaml 会同时清空面板上配置的规则（否则面板会显示一批不在文件里的规则）；
  重置 config.yaml 会把拦截恢复为关闭。两项都会先确认再执行。
- 发布闸门新增：默认路径必须在 /etc/log 下、迁移与重置逻辑必须存在、重置端点必须注册。

1.11.0
- 「原生配置预览」现在可以直接在面板里编辑并保存（原来只能看）。
  两个文件（config.yaml / rules.yaml）各自有「编辑 / 保存 / 取消」；保存先过校验：
  配置文件要能解析成有效设置（例如 log.level 只能是 debug/info/warn/error），
  规则要能被引擎编译。不通过就不写盘，并把原文件还原、把引擎的错误原文回给你，
  所以贴坏内容不会把插件弄成起不来的状态。保存成功即时生效（热加载）。
  上游那份面板的这块本来是只读的，编辑功能是本插件加的（独立脚本，上游前端保持原样）。

1.10.0
- 管理面板换成上游面板的前端，本插件自研面板退役。
  为什么：自研面板是与上游并行的第二套界面与第二套参数定义；上游那套是现成的、功能更全的
  中文界面（域名/IP/端口规则、屏蔽、流量透视、实时日志、环境诊断、规则仿真测试）。
  做法：把它的前端资产（index.html / css / js / i18n）原样内置进本插件，并按它前端实际调用的
  21 个 /api/* 端点实现后端 —— 底层是**本插件自己的引擎**，不是上游的 dashboard 二进制。
  为什么不直接跑上游的 dashboard 二进制：它在启动引擎前检查 /sys/module/nft_queue，
  而 iKuai 把 NFQUEUE 编进内核（不是模块），该前置条件在本固件上永远不成立（实测被拒）；
  它还会用 nftables 自己写防火墙规则，而 iKuai 没有 nftables。
- 调度方式沿用本插件的「内核优先」：内核 DPI 先判，只有内核未能识别的流量才进入本插件队列
  （爱快自身的 appmark/ikverdict 语义），与云防火墙那类内核调度一致，不是把全部转发流量
  都搬到用户态。
- 面板的规则模型（域名/协议开关、IP、端口、屏蔽）由本插件翻译成引擎的表达式规则：
  域名按 DNS 名称 / TLS SNI / HTTP Host 分别生成，子域用 hasSuffix（DNS 名称带末尾点、
  SNI 与 Host 不带，写错就会静默不命中）；生成的规则经引擎编译器校验通过才落盘。
- 面板生成的规则写在 rules.yaml 的标记行之后，标记之前的操作者规则**逐字保留**（含注释与
  格式），面板只重写自己那一段。
- 面板里的「DNS 污染」「反诈 HTTP 跳转」以及屏蔽的「仅记录」模式，本引擎没有对应实现：
  设置会被保存但**在面板上明确说明不生效**，不会假装生效。
- 「安装内核模块」按钮在本固件上如实回答「NFQUEUE 已内建」——上游那一版正是卡在这里。
- 环境诊断给出 iKuai 实际相关的检查项（NFQUEUE 内核接口、iptables 可读性、挂载一致性、
  当前调度方式），只诊断不改内核。
- 原始配置编辑（config.yaml / rules.yaml）保存前先校验，内容非法则拒绝并**还原原文件**。
- 本插件自己的 /api/v1/* 保持不变：发布闸门与验收脚本用它，且它带有面板放不下的字段。
- 面板头部的版本徽标改为显示**本插件**的版本（原来显示上游 dashboard 自己的 v1.1.2-14）。
- 面板总览的统计磁贴接上真实数据，不再恒为 0：
    * 「总分析包」= 内核交给本插件的报文数（读 iKuai iptables 规则自带的 -c 计数，
      读不到时不显示 0 而是保持 0 并在 status 里以 queue_counters_ok 标明"未测到"）；
    * 「已拦截流量包」= 引擎的拦截判定数（本引擎按流判定，用户态拿不到逐包拦截数，
      因此这里给的是判定次数，不是编造出来的包数）；
    * 「全加密代理 / Trojan / SSH」等卡片 = 引擎识别并命中的次数。
  计数在日志等级门禁**之前**累加：否则把日志调到 warn 就会让面板数字变成 0，
  看起来像"没有流量"，这是最坏的一种错。
  说明：fet（全加密流量）无法区分裸 VMess 与 Shadowsocks，两者合并计入
  「全加密代理 (SS / 裸 VMess)」这一张卡，不编造拆分。

1.9.0
