bcap/doc/dev.md
2026-03-24 23:39:55 +08:00

519 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# bcap 开发设计备忘
## 1. 这次重构的前提
当前已确认,直接使用 `bcap` 主包的范围很小,主要只有:
- `apps/tcp`
- `apps/b612` 中的 `tcm`
- `apps/b612` 中的 `tcpkill`
因此这次讨论 `bcap` 新架构时,可以明确采用以下前提:
1. 可以摒弃旧接口,不必为了兼容历史调用持续背负旧模型。
2. `bcap` 子包暂时不动,尤其是 `libpcap``nfq` 这类输入适配层先保持现状。
3. 重点只讨论 `bcap` 主包的新职责、新模型和新接口。
## 2. 当前主包存在的问题
当前 `bcap` 主包中,`Packets``PacketInfo``StateDescript` 这一套模型混合了承担以下几类职责:
- 报文事实解码
- TCP 状态推断
- 连接状态缓存
- 统计汇总
- 工具侧临时状态寄存
- 文本格式化辅助
这几个职责混在一起,直接导致了以下问题:
### 2.1 `PacketInfo` 同时承载“事实”和“推断”
例如:
- `SrcIP``DstIP``SrcMac``DstMac` 属于原始事实
- `TcpSeq``TcpAck``TcpWindow` 虽然也是事实,但只适用于 TCP
- `StateDescript` 则是推断结果
- `Comment` 又是工具业务的临时状态
这会让上层调用方无法区分:哪些字段是包里本来就有的,哪些字段是状态机推出来的,哪些字段甚至只是上层工具临时借位存进去的。
### 2.2 主包过于 TCP 化
当前主模型的核心字段和访问器明显偏向 TCP
- `TcpSeq()`
- `TcpAck()`
- `TcpWindow()`
- `TcpPayloads()`
- `StateDescript()`
虽然已经开始支持 UDP、ICMP、ICMPv6但整体接口风格仍然默认“TCP 才是主角,其他协议只是附带”。如果目标是做“通用 transport/protocol hint”这个建模方式会持续妨碍扩展。
### 2.3 `Packets` 同时是解析器、状态仓库和工具 scratchpad
当前 `Packets` 既负责:
- 解析 gopacket.Packet
- 保存连接状态
- 维护统计
- 暴露 `Key()` 查询
- 暴露 `SetComment()` 给上层写入工具状态
这实际上把“协议解析层”和“工具业务状态层”耦死了。
### 2.4 字符串 key 被当成核心接口
当前以 `tcp://a:b-c:d` 这类字符串 key 作为主要流标识,这虽然方便快速实现,但不适合作为长期公共接口。
问题包括:
- 协议语义不够结构化
- 上层反复自行拼 key
- 不利于未来支持可逆 flow、单向 packet、无端口协议、ARP 等场景
### 2.5 文本格式化与 JSON 导出混入主包
`GetStateDescription``PrintStats``ExportConnectionsToJSON` 这种能力,不属于主包核心解析模型,应该降级为上层工具职责,或者至少转到辅助层,而不是继续成为主接口的一部分。
## 3. 新架构的总体目标
`bcap` 主包应收敛成:
- 一个统一的报文事实解码层
- 一个统一的协议 hint 推断层
- 一个以 TCP 为重点的轻量状态跟踪层
同时明确边界:
### 3.1 `bcap` 应负责的事
- 识别链路层、网络层、传输层协议
- 提取结构化元数据
- 输出通用 protocol hint
- 对 TCP 做轻状态推断
- 对 UDP/ICMP/ARP 做协议识别和元数据抽取
- 对未知协议输出“尽量完整的基础事实”
### 3.2 `bcap` 不应负责的事
- 诊断报告拼装
- 干预动作编排
- CLI/TUI 展示文案
- 工具侧临时状态寄存
- 业务策略倒计时
- 面向最终用户的统计打印
也就是说,`bcap` 的定位应是“提供事实和提示”,而不是“替工具做决策”。
## 4. 新架构的核心分层
建议主包收敛成 3 个核心对象:
1. `Decoder`
2. `Tracker`
3. `Analyzer`
### 4.1 `Decoder`
`Decoder` 是无状态组件,只负责把 `gopacket.Packet` 解码成结构化“事实”。
职责:
- 识别 L2/L3/L4 协议
- 抽取 MAC、IP、端口、TTL、flags、payload 长度等事实
- 不做跨包状态推断
- 不维护连接表
- 不暴露工具业务状态
### 4.2 `Tracker`
`Tracker` 是有状态组件,只负责基于连续观测结果做轻量 hint 推断。
职责:
- 维护按 flow 组织的轻状态
- 针对 TCP 输出握手、挥手、重传、keepalive、RST 等 hint
- 针对 ICMP/ARP 等协议补充轻量语义标签
- 不负责最终展示
- 不负责业务动作控制
### 4.3 `Analyzer`
`Analyzer` 是便捷组合层,内部持有 `Decoder + Tracker`,给上层一个简单入口。
典型用途:
- `tcm` / `tcml` 这种一边收包一边展示的工具
- `diag` 这种离线分析工具
- `show` 这种需要 packet facts + hints 的浏览工具
这样设计后:
- 只关心事实解码的调用方可直接用 `Decoder`
- 需要 hint 的调用方可用 `Analyzer`
- 需要更细粒度控制的调用方可分别持有 `Decoder``Tracker`
## 5. 新的数据模型
新模型建议明确拆成“事实”和“推断”两层。
### 5.1 第一层Packet Facts
建议定义统一的结构化事实模型,例如:
```go
type Packet struct {
Meta Meta
Link LinkFacts
Network NetworkFacts
Transport TransportFacts
Raw RawFacts
}
```
其中:
- `Meta`:时间戳、捕获长度、原始长度、相对时间等
- `LinkFacts`:链路层类型、源/目标 MAC、链路层协议等
- `NetworkFacts`IPv4/IPv6/ARP 等信息包含地址、TTL/HopLimit、fragment、protocol number 等
- `TransportFacts`TCP/UDP/ICMP/unknown 等结构化内容
- `RawFacts`payload 长度、原始 packet 引用等
这层全部表示“包里本来就有”的事实,不包含推断结论。
### 5.2 第二层Observation
建议把“经过跟踪器分析后的结果”定义为单独对象:
```go
type Observation struct {
Packet Packet
Flow FlowRef
Hints HintSet
}
```
这里:
- `Packet` 是原始事实
- `Flow` 是结构化流引用
- `Hints` 是推断结果
### 5.3 FlowKey / FlowRef
建议引入结构化 flow 标识,而不是继续把字符串 key 作为主接口。
```go
type FlowKey struct {
Family NetworkFamily
Protocol TransportKind
Src Endpoint
Dst Endpoint
}
type FlowRef struct {
Forward FlowKey
Reverse FlowKey
Stable string
}
```
其中:
- `Forward` / `Reverse` 提供方向化 key
- `Stable` 可以保留字符串形式方便日志、map key、兼容旧调试习惯
- `Endpoint` 应允许“只有地址没有端口”的协议
### 5.4 HintSet
建议将所有推断信息挂在 `HintSet` 上:
```go
type HintSet struct {
Summary SummaryHint
Tags []Tag
TCP *TCPHint
UDP *UDPHint
ICMP *ICMPHint
ARP *ARPHint
}
```
这样做的好处是:
- `Summary` 提供给 `tcm/tcml` 这类工具快速展示一句话
- `Tags` 提供统一筛选、着色、统计能力
- 协议专属 hint 由独立结构体承载,不再污染通用顶层对象
## 6. 协议专属 hint 的建议形态
### 6.1 `TCPHint`
`TCPHint` 是新架构里的重点能力,建议承载:
- seq / ack / window
- flags
- payload length
- options 摘要
- handshake / teardown 阶段
- retransmission 判定
- keepalive 判定
- keepalive response 判定
- rst / ece / cwr 等特殊标记
- 是否疑似 out-of-order
同时建议把原来的 `StateDescript uint8` 弃用,改成更结构化的字段或 tag例如
- `Phase: TCPPhase`
- `Event: TCPEvent`
- `Tags: []Tag`
### 6.2 `UDPHint`
UDP 先做轻支持即可,建议包括:
- payload length
- checksum presence / checksum value
- zero-length payload
- fragment 上下文关联信息(如果网络层提供)
不建议在主包内直接引入 DNS、QUIC 等应用层推断,除非后续确认收益足够大。
### 6.3 `ICMPHint`
建议至少包括:
- family: v4 / v6
- type / code
- id / seq
- 是否 echo request / echo reply
- 是否 destination unreachable / time exceeded
这层对 `show``tcm/tcml` 的“多协议展示”已经足够有价值。
### 6.4 `ARPHint`
如果目标是支持“展示其他协议”ARP 必须成为一等公民。
建议至少包括:
- operation
- sender MAC / sender IP
- target MAC / target IP
- request / reply tag
同时 `Decoder` 对 ARP 不应再因为没有 `NetworkLayer()` 而直接报错。
## 7. 标签体系建议
建议在主包内定义统一 tag 体系,供所有上层工具复用。
例如:
- `tcp.handshake.syn`
- `tcp.handshake.synack`
- `tcp.handshake.ack`
- `tcp.teardown.fin`
- `tcp.keepalive`
- `tcp.keepalive.response`
- `tcp.retransmit`
- `tcp.rst`
- `tcp.ece`
- `tcp.cwr`
- `udp.packet`
- `icmp.echo-request`
- `icmp.echo-reply`
- `icmp.unreachable`
- `arp.request`
- `arp.reply`
- `transport.unknown`
这比继续依赖一个不断膨胀的 `StateDescript` 枚举更稳。
## 8. 状态与并发语义
这是新架构里必须提前定死的部分。
### 8.1 `Decoder` 的并发语义
`Decoder` 无状态,应明确支持并发使用。
### 8.2 `Tracker` 的并发语义
`Tracker` 有状态,应明确:
- 同一 flow 的观测结果必须按顺序输入
- 允许多 flow 并发,但内部状态更新语义以 flow 顺序为准
- 如果调用方无法保证顺序,则 hint 结果可能退化
这点非常重要。当前的 `shardedMap` 只能提供 map 级线程安全不能自动提供时序正确性。TCP hint 尤其依赖时序,因此新接口必须把这一点写进设计,而不是默认调用方自己猜。
### 8.3 工具侧临时状态必须外移
例如 `tcml` 的:
- delay countdown
- block / allow 状态
- 业务关键字命中后的动作状态
都不应再借用 `Comment/SetComment` 写进 `bcap` 状态仓库。
新架构里,`Tracker` 只维护协议分析所需状态,不维护工具策略状态。
## 9. 错误模型建议
建议错误也按层次整理:
- 解码失败
- 支持范围外
- 协议字段损坏
- 状态推断降级
同时要允许“部分可用”的结果。
例如:
- 能识别到 IPv4但 transport 未识别
- 能识别到 ARP但字段不完整
- 能拿到 TCP flags但因为缺少上下文无法判定 retransmit
这种情况下应尽量返回事实层结果,而不是简单整体失败。
## 10. 主包不再保留的旧概念
建议在新架构中明确移除或降级以下旧概念:
- `PacketInfo`
- `Packets`
- `StateDescript()`
- `Comment()` / `SetComment()`
- `GetStateDescription()`
- `PrintStats()`
- `ExportConnectionsToJSON()`
- 各类以字符串 key 作为核心接口的模型
原因是:
- `PacketInfo` 混杂事实、推断和业务临时状态
- `Packets` 角色过多,不适合作为长期主对象
- `StateDescript` 无法优雅承载多协议 hint
- `Comment/SetComment` 明显越界
- 格式化和导出不应继续作为主包核心能力
## 11. 建议的新接口形态
建议主包至少提供两层使用方式。
### 11.1 低层接口
```go
decoder := bcap.NewDecoder(opts)
pkt, err := decoder.Decode(gpkt)
tracker := bcap.NewTracker(opts)
obs, err := tracker.Observe(pkt)
```
适合:
- 想分别控制解码与跟踪的调用方
- 想单测某一层的调用方
- 想把事实层和 hint 层分开使用的工具
### 11.2 便捷接口
```go
analyzer := bcap.NewAnalyzer(opts)
obs, err := analyzer.ObservePacket(gpkt)
```
适合:
- `tcm`
- `tcml`
- `diag`
- `show`
这类工具大多要的是“输入 gopacket.Packet得到事实 + hint”。
## 12. 对现有调用方的迁移影响
### 12.1 `tcpkill`
迁移成本最低。
它主要只需要:
- TCP 四元组
- seq / ack / window
- MAC
因此它可以优先迁移到:
- `Decoder`
-`Analyzer` 中的 `Observation.Packet + Observation.Hints.TCP`
### 12.2 `tcm` / `tcml`
迁移重点在展示层。
它们当前主要依赖:
- `StateDescript`
- `TcpSeq/TcpAck/TcpWindow/TcpPayloads`
- `Key/ReverseKey`
- `Comment/SetComment`
迁移后应改成:
-`Observation.Hints.Summary``Tags` 做展示语义
- 用结构化 `FlowRef` 替代字符串 key 拼接
-`tcml` 的动作状态迁出 `bcap`
### 12.3 `diag`
`diag` 会受益最大。
它本质上就是一个消费“事实 + hint”的离线分析器。等 `bcap` 能稳定输出结构化 TCP/ICMP/ARP/UDP hint 后,`diag` 内部很多启发式逻辑会更容易表达,也更少依赖旧的 TCP 枚举状态。
## 13. 与子包的边界
本次先不动子包,但应明确子包定位:
- `libpcap`:抓包输入适配层
- `nfq`NFQUEUE 输入适配层
它们不属于主模型核心,只是 `gopacket.Packet` 的来源适配。
换句话说:
- `bcap` 主包定义“如何理解 packet”
- 子包定义“packet 从哪里来”
这个边界应该长期保持稳定。
## 14. 当前结论
如果重做 `bcap` 主包,最合理的方向是:
1. 主包已经从 `Packets/PacketInfo` 旧模型切换到 `Decoder/Tracker/Analyzer` 新模型。
2. 明确分离“报文事实”和“推断 hint”。
3. 引入结构化 `FlowKey/FlowRef`,不再把字符串 key 作为主接口。
4.`HintSet + Tags + protocol-specific hints` 取代 `StateDescript`
5. 删除 `Comment/SetComment` 这类工具业务越界能力。
6.`bcap` 成为“轻量协议事实 + hint 提供层”,而不是“工具状态仓库”。
## 15. 推荐的实施顺序
建议真正开始改实现时,按下面顺序推进:
1. 先定义新的公共类型草图:`Packet``Observation``FlowKey``HintSet``TCPHint``ICMPHint``ARPHint`
2. 再实现无状态 `Decoder`,保证多协议事实提取完整。
3. 再实现有状态 `Tracker`,先把 TCP hint 迁进去。
4. 再提供 `Analyzer` 便捷入口。
5. 最后再让 `apps/tcp``apps/b612` 的调用方迁到新接口,并删除旧兼容 facade。
在这个顺序下,主架构会先稳定下来,后续迁移也更可控。