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

13 KiB
Raw Permalink Blame History

bcap 开发设计备忘

1. 这次重构的前提

当前已确认,直接使用 bcap 主包的范围很小,主要只有:

  • apps/tcp
  • apps/b612 中的 tcm
  • apps/b612 中的 tcpkill

因此这次讨论 bcap 新架构时,可以明确采用以下前提:

  1. 可以摒弃旧接口,不必为了兼容历史调用持续背负旧模型。
  2. bcap 子包暂时不动,尤其是 libpcapnfq 这类输入适配层先保持现状。
  3. 重点只讨论 bcap 主包的新职责、新模型和新接口。

2. 当前主包存在的问题

当前 bcap 主包中,PacketsPacketInfoStateDescript 这一套模型混合了承担以下几类职责:

  • 报文事实解码
  • TCP 状态推断
  • 连接状态缓存
  • 统计汇总
  • 工具侧临时状态寄存
  • 文本格式化辅助

这几个职责混在一起,直接导致了以下问题:

2.1 PacketInfo 同时承载“事实”和“推断”

例如:

  • SrcIPDstIPSrcMacDstMac 属于原始事实
  • TcpSeqTcpAckTcpWindow 虽然也是事实,但只适用于 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 导出混入主包

GetStateDescriptionPrintStatsExportConnectionsToJSON 这种能力,不属于主包核心解析模型,应该降级为上层工具职责,或者至少转到辅助层,而不是继续成为主接口的一部分。

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
  • 需要更细粒度控制的调用方可分别持有 DecoderTracker

5. 新的数据模型

新模型建议明确拆成“事实”和“推断”两层。

5.1 第一层Packet Facts

建议定义统一的结构化事实模型,例如:

type Packet struct {
    Meta      Meta
    Link      LinkFacts
    Network   NetworkFacts
    Transport TransportFacts
    Raw       RawFacts
}

其中:

  • Meta:时间戳、捕获长度、原始长度、相对时间等
  • LinkFacts:链路层类型、源/目标 MAC、链路层协议等
  • NetworkFactsIPv4/IPv6/ARP 等信息包含地址、TTL/HopLimit、fragment、protocol number 等
  • TransportFactsTCP/UDP/ICMP/unknown 等结构化内容
  • RawFactspayload 长度、原始 packet 引用等

这层全部表示“包里本来就有”的事实,不包含推断结论。

5.2 第二层Observation

建议把“经过跟踪器分析后的结果”定义为单独对象:

type Observation struct {
    Packet Packet
    Flow   FlowRef
    Hints  HintSet
}

这里:

  • Packet 是原始事实
  • Flow 是结构化流引用
  • Hints 是推断结果

5.3 FlowKey / FlowRef

建议引入结构化 flow 标识,而不是继续把字符串 key 作为主接口。

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 上:

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

这层对 showtcm/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 低层接口

decoder := bcap.NewDecoder(opts)
pkt, err := decoder.Decode(gpkt)

tracker := bcap.NewTracker(opts)
obs, err := tracker.Observe(pkt)

适合:

  • 想分别控制解码与跟踪的调用方
  • 想单测某一层的调用方
  • 想把事实层和 hint 层分开使用的工具

11.2 便捷接口

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.SummaryTags 做展示语义
  • 用结构化 FlowRef 替代字符串 key 拼接
  • tcml 的动作状态迁出 bcap

12.3 diag

diag 会受益最大。

它本质上就是一个消费“事实 + hint”的离线分析器。等 bcap 能稳定输出结构化 TCP/ICMP/ARP/UDP hint 后,diag 内部很多启发式逻辑会更容易表达,也更少依赖旧的 TCP 枚举状态。

13. 与子包的边界

本次先不动子包,但应明确子包定位:

  • libpcap:抓包输入适配层
  • nfqNFQUEUE 输入适配层

它们不属于主模型核心,只是 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. 先定义新的公共类型草图:PacketObservationFlowKeyHintSetTCPHintICMPHintARPHint
  2. 再实现无状态 Decoder,保证多协议事实提取完整。
  3. 再实现有状态 Tracker,先把 TCP hint 迁进去。
  4. 再提供 Analyzer 便捷入口。
  5. 最后再让 apps/tcpapps/b612 的调用方迁到新接口,并删除旧兼容 facade。

在这个顺序下,主架构会先稳定下来,后续迁移也更可控。