奶昔防火墙
=================

基于 Netfilter NFQUEUE 的深度报文检测（DPI）插件。

DPI 引擎（github.com/nyarime/dpi-engine，本团队的独立项目）在编译期链接进本插件，
安装包内只有一个可执行文件 naixifwd：检测引擎、面板、内核规则管理、自我保护都在同一个
进程里。没有子进程，没有日志管道。

引擎在本仓库里**没有源码副本**：它有自己的仓库与版本号，本插件只声明依赖哪个版本。
这么做是为了让"打进包里的引擎是哪一份"可复现、可追溯。

配置文件
--------
引擎的全部行为都在配置文件里：

  /etc/log/naixifw/config.yaml   引擎配置（log / io / workers，外加本插件的 ikuai 段）
  /etc/log/naixifw/rules.yaml    规则集（name / action / log / modifier / expr）

两个文件都可以直接手改，也可以与引擎原生的配置互换；面板改的就是这两个
文件，所以在哪边改都是同一件事。路径可在应用设置里改。

放在 /etc/log 下是有原因的：iKuai 只有 /etc/log 是持久分区（独立数据盘），
/etc 本身在固件分区上，固件升级或恢复出厂会把它抹掉。
首次启动会用默认内容创建这两个文件。

面板的「原生配置预览」可以直接编辑这两个文件并保存（保存前会校验：配置文件要能
解析、规则要能被引擎编译，不通过就还原原文件），也可以「重置为出厂设置」恢复默认内容。

全量接管（唯一的识别范围）
---------------------------------------------
  所有到达钩子的转发流量都送进插件判定。**这是唯一的档位。**

  早期有过一个「辅助增强」档（只把内核未能识别的流量交给插件），已移除。原因是它的
  判据与内核的实际行为对不上：内核对一条流做分类是在它**开始带载荷之后**，所以那一档
  只把握手报文交给插件、把带载荷的报文留在内核 —— 依赖载荷的规则（域名 / SNI / Host /
  协议屏蔽）**永远匹配不上**，而且全链路没有任何报错。表现就是「规则在、拦截开着、
  什么都拦不住」，最难排查的一类问题。

  要省 CPU 就收窄**端口**，不要去猜哪些流值得看：

    ikuai:
      scope: full              # 唯一值；写别的会被拒绝
      queueOnly: custom
      queuePorts: "53,80,443"  # 只让这些端口的流量进插件

  这样队列规则里只有端口条件，插件能看到这些流开头的那一段（域名 / SNI / fet 判定
  所需的载荷），其余端口的流量连队列都不进，完全不受影响。

  IP / 端口类规则不依赖载荷，收窄与否都有效。

  另外两条会让插件"少看到东西"的**部署**条件（不是设置错误，面板会显示、启动日志
  也会写）：流量加速（被卸载的长流绕过 netfilter）、二层桥接类部署（转发不经过
  netfilter，会被直接拒绝启动）。详见下文「识别范围」。

「全灭」模式的推荐配置
----------------------
  想把各协议尽可能拦住（全端口、每条流都送插件），用：

    ikuai:
      enabled: true
      scope: full          # 唯一档位（省略也行，默认就是它）
      queueOnly: all       # 全部端口
      filterNoise: true
      failopen: true       # 队列过载时放行而不是丢包（务必保持打开）
    io:
      queueSize: 8192      # 默认 1024；繁忙时调大以吸收突发（上限 65535）
    workers:
      count: 8             # 默认 2；上限 256。CPU 没跑满就不用加
      queueSize: 1024
  面板「深度协议防护」里四个协议开关全部打开。

  实测开销（4 核虚拟机、全量接管、每个报文一次判定）：约 17000 包/秒·核，
  即 10 万包/秒 ≈ 6 核。核多的机器可以直接全量接管；核少的机器建议按上面的
  queuePorts 精准模式收窄端口。

  这个模式能拦住什么、拦不住什么（都有实测依据）：
    能拦   非 TLS 的全加密流量（Shadowsocks / Shadowsocks-2022 / 裸 VMess 等）
           —— 判定依据是流的首包熵值等四个指标（fet 分析器）
    能拦   非 TLS 的 Trojan、SSH（各自有专门分析器）
    拦不住 TLS 伪装：Trojan / VMess 套真 TLS 时，首包与正常 HTTPS 无法区分。
           fet 有一条「像 TLS / HTTP 就豁免」的规则，这是为了不误杀全部 HTTPS，
           属于设计取舍；要连这类一起拦，只能改用"只放行白名单域名"的做法，
           代价是未列入白名单的 HTTPS 全部不可用。
    拦不住 UDP：fet 只处理 TCP，Shadowsocks 的 UDP relay 不经过它
           （面板的协议屏蔽对 UDP 流量无效）。
    注意  打开「屏蔽 SSH」会把经过路由的 SSH 连接一并拦掉（含你自己远程登录
           服务器、git over ssh 等）。

config.yaml 各段的作用（文件里也有同样的注释）：

  log.level      debug / info / warn / error。等级越低代价越高，见下文「日志」。
  io.local       true = 处理路由器自身流量（本机模式），false = 只处理转发流量。
  io.queueSize   NFQUEUE 队列长度（报文数）。
  io.rst         true = 阻断时发 TCP RST，连接立刻断开而不是等超时。
  workers.*      工作线程数与每流缓冲上限（流卡住时限制内存占用）。

  ikuai.enabled      是否真正拦截，默认 false（只记录不拦截）。
  ikuai.scope        识别范围：assist（默认）/ full，见下文「识别范围」。
  ikuai.queueOnly    流量筛选：all（默认）/ dns / web / custom，见下文「流量筛选」。
  ikuai.queuePorts   queueOnly: custom 时的端口，如 "53,8000-8100"。
  ikuai.filterNoise  组播噪声过滤，默认 true。
  ikuai.failopen     过载保护，默认 true。
  ikuai.bypass       不进入检测队列的 IP/CIDR，空格分隔。

写错的处理方式：段名写错（如 workr）会在日志里列出被忽略的字段，不静默忽略；
已存在的段里值非法（如 log.level: verbose）会直接报错并保留原文件 —— 宁可起不来
让人看见，也不静默换一个值替你把规则跑歪。文件损坏时保留原文件、以默认值启动面板
（面板是修复入口）。

统计数字的来源
--------------
面板总览上的数字来自两处，都不是估出来的：

  * 「总分析包」——内核交给本插件的报文数，读的是 iKuai iptables 规则自带的计数。
    读不到时不会显示成一个看起来正常的 0。
  * 「已拦截流量包」与各协议的「命中」数字——引擎自己的判定与命中计数。
    本引擎按流（连接）判定，所以这里是**判定次数**而不是逐包数量：用户态拿不到
    "内核丢了多少包"这个数，写一个出来就是编的。
    这些计数与日志等级无关，把日志调到 warn 也不会让它们变成 0。

「全加密代理 (SS / 裸 VMess)」是一张合并的卡：从外面看，Shadowsocks 和裸 VMess
都是"全加密流量"（引擎的 fet 分析器），本插件区分不了这两者，所以不拆分显示。

管理面板
--------
面板前端基于上游引擎配套的那套 dashboard 界面，内置在本插件里；后端是本插件自己的服务，
不是上游那个 dashboard 程序（它启动引擎前要求 nftables 的内核模块，而 iKuai 把 NFQUEUE
编进内核、且没有 nftables，在真机上会被它拒绝）。

面板能做的事：

  * 域名规则：按域名拦截/放行，可分别选择拦 DNS、TLS(SNI)、HTTP，可选是否含子域；
  * IP 规则：按 IP 或网段，可选方向（进/出/双向）；
  * 端口规则：按端口（TCP 与 UDP 都支持）；
  * 屏蔽：VMess 类全加密流量、Shadowsocks 类、Trojan 特征、SSH；
  * 引擎启停、重启、热重载；
  * 实时日志（不写盘，只在内存里滚动）与流量透视；
  * 原始配置编辑：直接编辑 config.yaml 与 rules.yaml（与 SSH 改的是同一对文件）；
  * 规则仿真测试：输入域名，看哪条规则会命中；
  * 环境诊断与「安装内核模块」（本固件上会如实回答：NFQUEUE 已内建，无需安装）。

面板的规则会翻译成引擎的表达式规则，写在 rules.yaml 的标记行之后：

  # ==== 以下规则由面板生成（面板每次保存都会重写这一段；自己的规则请写在上面）====

标记行以上的内容面板**逐字保留**（含注释与格式），所以你完全可以一半用面板、一半用
SSH 手写。生成的表达式会先用引擎的编译器校验，写不出来的规则不会落盘。

面板上这几项本引擎没有对应实现，**会被明确说明不生效**（不会假装生效）：

  * DNS 污染（把命中域名解析到指定 IP）：上游由它自带的 DNS 服务实现，本插件未内置；
  * 反诈 HTTP 跳转：同上（上游自带 9881 端口的跳转服务）；
  * 屏蔽模式里的「仅记录」：本引擎的判定只有放行与拦截两种结果。

--------
https://<lan1-ip>:9170/

（面板默认路径就是根路径，应用市场的「打开」按钮也指向这里。旧版界面保留在
 /legacy/ 下作为退路，仅在需要时使用。）

默认启用 HTTPS，与 HTTP 共用这一个端口：浏览器访问 http:// 会被 307 跳到同地址的
https，明文请求不会被真正处理（面板登录是 HTTP Basic，明文连接上密码可被链路上
任何人读取）。证书默认用路由器自带的那张（`/usr/openresty/ssl/server.crt` 与
`server.key`），可在应用设置里改路径：

  * 证书续期后自动重新加载，无需重启插件；
  * 读不到证书时自动改用自签名证书并在面板说明原因 —— 连接仍是加密的，只是浏览器
    无法验证身份、会提示不受信任；
  * 用 IP 打开时提示不受信任属正常：内置证书的主体是路由器自己的域名；
  * 在应用设置里把「面板 HTTPS」设为关闭即回到纯 HTTP（此时面板会警告密码明文传输）。

首次登录用默认账号 admin / ikuai8.com（HTTP Basic）。面板能开关流量拦截，
请改掉密码；也可以把用户名改成 off 完全关闭鉴权（不建议）。面板只绑定 LAN1
地址，不会暴露到外网。

拦截默认关闭
------------
安装后不会阻断任何流量。默认规则是「只记录不拦截」，用来先观察识别结果，
确认符合预期后再把 action 改成 block。

两种模式
--------
网关模式（推荐）
  处理转发流量（FORWARD 链），即 LAN 客户端的上网流量。
  路由器自身的管理面不受影响。

本机模式（高级，慎用）
  处理路由器自身流量（INPUT/OUTPUT 链）。
  注意：此模式会接管管理面。它把路由器自己的出入流量交到用户态，一旦插件
  停止消费，队列里的报文会被扣住，管理通道可能随之卡住。插件内置两重保护
  （下文的「过载保护」与自动卸载），但都只是减小影响、不能消除影响。
  本机模式必须收窄端口：把设备自身**全部**流量送进插件会把设备饿死，实测（1 条规则
  即可）会让自身出网停摆。所以「本机模式 + 全部端口」这个组合会被直接拒绝，请改成
  自定义端口清单（例如 53），或改用网关模式。

  这也决定了「只做 DNS 的旁路由」该怎么做：那类部署里客户端的 DNS 查询是发往**本
  设备自身**的（走 INPUT，不是转发），而网关模式的队列只挂在 FORWARD 上 —— 所以
  只有「本机模式 + 端口 53」才看得到它们。
  仅在需要短时诊断时使用，用完请立即关闭。
  自我保护：开启本机模式后会周期性探测本机出网，连续失败则自动卸下内核规则
  并关闭拦截（无需人工介入即可恢复）。本机模式不跨重启保留。

识别范围
--------
只有一档：**全量接管**。所有到达钩子的转发流量都送进插件判定。

  ★ 真机实测（那档被移除的直接原因）：按域名/DNS 的拦截（例如 gfwlist 这类规则）在
  早期那个「辅助增强」档下【完全不生效】—— 爱快内核已识别的流不会进入本插件。同一份
  gfwlist 规则集：辅助增强下 80/80 条 DNS 查询全部放行（引擎根本看不到这些流）；
  切到全量接管后 80/80 条被拦、对照域名 0/40 误拦。

  代价是性能与延迟：每个报文都要送到用户态处理，大流量下 CPU 与延迟会上升（实测数字
  见下文「处理能力」）。要压成本请收窄端口，不要减少"看多少流"。

  除设置之外，还有两条**部署**条件会让插件少看到东西 —— 面板会显示，启动日志也会写：

  流量加速（FASTOFFLOAD）
    iKuai 在一条流双向各过 50 个包之后把它交给 FLOWOFFLOAD 卸载，**被卸载的流不再
    经过 netfilter**，插件只看得到它开头的那部分报文。也就是"识别到的都是短连接"。
    要完整识别请在爱快里关闭流量加速 —— 注意这与"全量接管"是此消彼长的：加速越好，
    能看到的越少。

  二层桥接类部署（旁路模式 / SD-WAN 网桥 / 网桥透传）
    这类部署下转发发生在二层，**根本不经过 netfilter**，插件看不到任何流量 ——
    会被直接拒绝启动并说明原因，而不是让它假装在工作。本机的内核里连 br_netfilter
    模块都没有，所以也无法通过加载模块补救；这种情况必须改成三层路由部署。

组播噪声过滤
------------
默认开启。把组播与广播报文排除在检测队列之外：这类报文内核从不参与分类，会持续
占用队列（仅 SSDP/DLNA 的广播就足够占住），排除后不会漏掉任何需要检测的流量。
IPv4 与 IPv6 分别探测（两个协议族的匹配写法不同），若某个协议族不支持，面板会
明确显示为「部分生效」，而不是假装已生效。

过载保护
--------
使用爱快内核自带的 NFQUEUE 过载保护（默认开启）。

不开时用的是内核原生行为：队列一旦打满，后续报文直接被丢弃——在内网里这会
表现为断网。开启后，队列占用超过 90% 时改为放行，报文不再被丢。

请注意这是减小影响，不是免疫：已经进入队列的报文仍然在等待，所以插件若停止
处理，最多仍会有「队列长度」这么多报文被扣住。真正兜底的是插件自身的设计
（读取永不阻塞、异常自动卸载）与这个内核开关一起作用。

该开关是全内核生效的，与其它使用 NFQUEUE 的组件共用；本插件启动拦截时会按
设置置位，停止时还原为原先的值。

处理能力（真机实测，供选型参考）
--------
「全量接管」是把转发报文逐个送进用户态判定，成本按【报文】计，不是按流。实测
（4 核 x86，DNS 类小报文，4310 条域名规则）：

  约 10 万包/秒的极端小包压力下：约 4.5 万包/秒可全额处理且队列不积压（约 1.4 核）；
  压力继续升高时队列开始积压，但内核过载保护会让报文放行而不是丢弃——
  此时表现是「过滤效果下降」，不是「网络中断」。

真实网关流量远低于此：1500 字节满包时 4.5 万包/秒相当于约 540 Mbps，而内网
浏览/视频的包速率通常只有几千到几万包/秒。若你只需要按域名拦截 DNS，建议把
「流量筛选」设为「仅 DNS」—— 那时代价几乎为零，实测与不装插件处于同一量级。

流量筛选（降低开销的主要办法）
--------
决定哪些报文进入本插件：

  全部流量   每个转发报文都送进用户态，代价最高（见上）。
  仅 DNS     只送 53 端口（TCP 与 UDP）。按域名拦截（gfwlist 那种）够用，
             代价几乎为零 —— 这是「只想拦域名」时的推荐值。
  仅网页     只送 80 与 443。按 TLS SNI / HTTP 拦截需要它。
  自定义     自填端口，支持范围（如 8000-8100），TCP 与 UDP 都生效。

注意两件事：

  1. 没被包含的流量【完全不会】被本插件检测。比如把筛选设为「仅 DNS」，任何
     按 TLS SNI 拦截的规则都不会生效 —— 那是流量根本没被看到，不是规则写错。
  2. 筛选与「识别范围」是两个独立维度：前者决定「看多少」，后者决定「对谁负责」
     （辅助增强只管内核未识别的流，全量接管接管全部转发流量）。

日志
--------
等级越低日志越详细、开销越大。引擎对每个报文的判定记录会格式化整份 DNS 属性，
所以等级设为 info/debug 且流量较大时会明显拖慢转发（实测未做限制时吞吐被压到
约 9 千包/秒）。现在有每秒 500 条的上限，超出部分丢弃并计数，面板会提示
「已丢弃 N 条」；排查完建议调回 warn 或 error。

部署前提
--------
以下情况插件根本看不到流量，会直接拒绝启用并说明原因（而不是安静地一条都不
命中，让人误以为"没有匹配流量"）：

- 链路模式为旁路模式或 SD-WAN 网桥：这类流量走二层桥接，不经过 iptables。
- 检测到网桥透传。

加速模式（FASTOFFLOAD）不阻止启用，但会提示：开启后每条流只能看到前若干报文，
依赖整条流行为的检测（如全加密流量判定）会失效。

规则写法
--------
规则为 YAML 列表，每条形如：

  - name: log-dns
    action: allow      # allow / block / drop
    log: true
    expr: dns != nil

常用表达式片段（字段名必须是真实存在的分析器）：

  dns != nil                              DNS 查询
  tls != nil && tls.req.sni != ""         TLS SNI（注意 SNI 在 tls.req 下）
  quic != nil                             QUIC / HTTP3
  http != nil                             HTTP
  ssh != nil / socks != nil               SSH / SOCKS
  wireguard != nil / openvpn != nil       WireGuard / OpenVPN
  fet != nil && fet.yes                   全加密流量（Shadowsocks 类）
  trojan != nil                           Trojan 特征

注意：引擎不会拒绝写错的字段名（规则会静默永不命中），面板会在保存时校验，
请以面板的校验结果为准。例如并不存在 ss / vmess 分析器，全加密流量检测是 fet。

绕过名单
--------
填写不进入检测队列的 IP 或 CIDR，多个用逗号或换行分隔。规则会插在 NFQUEUE
之前，这些地址直接放行，不消耗检测资源。

安全须知
--------
- 开启拦截前，请先用「只记录」规则观察日志。
- 关闭拦截时会自动卸下内核挂载，不留残余规则；异常退出后重新启动也会清理。
- 若上次未正常退出（拦截仍处于挂载状态），本次开机不会自动恢复拦截。
- 本机模式不跨重启保留，每次都需要手动开启。
- 应用设置里的改动（识别范围、过载保护）需改 config.yaml 后重启插件生效；面板里的改动
  立即生效。

与爱快内置功能的关系
--------------------
在网络流量的处理顺序上，本插件的钩子位于爱快自身功能链之后：

  转发路径：ACL_MAC（MAC 绑定）→ 连接数限制 → 限速 → 防火墙 → 本插件

所以辅助增强模式不会绕过或削弱这些功能。全量接管的代价见上文。

开发
--------
引擎是私有 module，构建机需要能取到它：

    export GOPRIVATE=github.com/nyarime/*

同时改引擎与本插件时，用 go.work 把本地引擎检出接进来，不必每次先给引擎发版：

    # /root/vp/go.work
    go 1.22
    use (
        ./dpi-engine
        ./docker-app/native-apps/naixifw
    )

**只在开发时这样用。** 发布构建必须关掉工作区（构建脚本里已设 GOWORK=off），否则打进包里
的引擎可能是本地未提交、未发版的代码，与 go.mod 声明的版本对不上 —— 产物就不再是"由哪个
版本构建出来的"可复现的东西。改了引擎要生效，先给引擎打 tag 发版，再升本插件的依赖。

构建出包：

    bash scripts/build-ipkg.sh

脚本从源码目录组装整棵包树、签名并逐项校验，只产出安装包，**不上传任何地方**。
它需要 `GOPRIVATE=github.com/nyarime/*` 与可用的 git 凭据（引擎是私有 module）。
产物目录、签名脚本路径可用 `BUILD_DIR`、`SIGN_SCRIPT` 覆盖。

开源组件与授权
--------------
本插件链接一份外部来源的 DPI 引擎（github.com/nyarime/dpi-engine），该引擎以 Mozilla
Public License 2.0 授权。它的源码与作者归属记录（LICENSE、PROVENANCE.md、
UPSTREAM_COMMIT）都保存在**引擎自己的仓库**里，本仓库不再持有副本 —— 第三方源码的
授权声明留在它自己的项目里，本仓库因此不会被牵连。

MPL-2.0 是**文件级** copyleft：它只要求受其覆盖的那些文件（及其改动）继续以 MPL-2.0
提供源码，**不要求使用它的产品开源**。本插件自身的代码（internal/、cmd/）授权由我们
自己决定。构建脚本会核对引擎来源提交号确实编进了二进制。

日志文件
--------
面板「日志」页显示引擎识别结果（命中规则、解析到的协议字段）。
文件：<插件目录>/.pkg/log/run.log（插件自身状态）、.pkg/log/engine.log（引擎输出），
面板的「日志」页读的就是它们。
