# 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。 在这个顺序下,主架构会先稳定下来,后续迁移也更可控。