From 744ac8c2e9b47fa301d79b96fbae140e97e5a8e3 Mon Sep 17 00:00:00 2001 From: starainrt Date: Tue, 24 Mar 2026 23:39:55 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 201 +++++++ README.md | 218 +++++++ analyzer.go | 73 +++ api_test.go | 315 ++++++++++ bcap.go | 1359 ------------------------------------------ conn_map.go | 50 ++ decoder.go | 227 +++++++ doc/api.md | 726 ++++++++++++++++++++++ doc/dev.md | 518 ++++++++++++++++ format.go | 34 ++ format_test.go | 24 + model.go | 258 ++++++++ nfq/nfqueue.go | 4 +- state.go | 12 + tcp_seq.go | 15 + tcp_test.go | 747 +++++++++++++++++++++++ test_helpers_test.go | 354 +++++++++++ tracker.go | 348 +++++++++++ tracker_tcp.go | 278 +++++++++ types.go | 102 ++++ 20 files changed, 4502 insertions(+), 1361 deletions(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analyzer.go create mode 100644 api_test.go delete mode 100644 bcap.go create mode 100644 conn_map.go create mode 100644 decoder.go create mode 100644 doc/api.md create mode 100644 doc/dev.md create mode 100644 format.go create mode 100644 format_test.go create mode 100644 model.go create mode 100644 state.go create mode 100644 tcp_seq.go create mode 100644 tcp_test.go create mode 100644 test_helpers_test.go create mode 100644 tracker.go create mode 100644 tracker_tcp.go create mode 100644 types.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9590d39 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "[]" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a24e70d --- /dev/null +++ b/README.md @@ -0,0 +1,218 @@ +# bcap + +`bcap` 是一个轻量的 Go 报文解析库,重点提供两类能力: + +- 统一提取报文事实 +- 基于连续报文输出轻量协议提示 + +它适合作为抓包工具、离线诊断工具、CLI/TUI 浏览器的底层解析层,但并不试图替代完整协议栈、深度重组引擎或最终用户报告系统。 + +## 1. 核心定位 + +`bcap` 主包当前围绕 3 个对象展开: + +- `Decoder` + - 无状态,只负责单包解码,输出 `Packet` +- `Tracker` + - 有状态,只负责基于连续报文做轻量跟踪,输出 `Observation` +- `Analyzer` + - `Decoder + Tracker` 的组合入口,适合大多数直接接入场景 + +分工可以直接理解为: + +- `Decoder` 负责单包事实 +- `Tracker` 负责跨包提示 +- `Analyzer` 负责把两者串起来 + +## 2. 适用场景 + +`bcap` 主要适合: + +- 在线抓包后的统一元数据提取 +- 离线 pcap 遍历分析 +- TCP/UDP/ICMP/ARP 的统一识别 +- TCP 关键行为提示,例如握手、挥手、重传、keepalive、RST + +`bcap` 不直接负责: + +- 诊断结论生成 +- CLI/TUI 文案 +- Excel / JSON / 报告拼装 +- 动作编排与干预逻辑 +- 工具侧业务状态缓存 + +## 3. 快速开始 + +安装: + +```bash +go get b612.me/bcap +``` + +最常见的接入方式是直接使用 `Analyzer`: + +```go +package main + +import ( + "fmt" + + "b612.me/bcap" + "github.com/gopacket/gopacket" +) + +func consume(packet gopacket.Packet) error { + analyzer := bcap.NewAnalyzer() + defer analyzer.Stop() + + obs, err := analyzer.ObservePacket(packet) + if err != nil { + return err + } + + fmt.Println("flow:", obs.Flow.Stable) + fmt.Println("protocol:", obs.Packet.Transport.Kind) + fmt.Println("summary:", obs.Hints.Summary.Code) + + if obs.Hints.TCP != nil { + fmt.Println("tcp event:", obs.Hints.TCP.Event) + } + return nil +} +``` + +如果你需要自行控制“解码”和“跟踪”两个阶段,也可以拆开使用: + +```go +decoder := bcap.NewDecoder() +tracker := bcap.NewTracker() +defer tracker.Stop() + +decoded, err := decoder.Decode(packet) +if err != nil { + return err +} + +obs, err := tracker.Observe(decoded) +if err != nil { + return err +} +``` + +## 4. 支持范围 + +当前主包支持的主要识别范围: + +- 链路层 + - `Ethernet` + - `Linux SLL` + - `Linux SLL2` +- 网络层 + - `IPv4` + - `IPv6` + - `ARP` +- 传输层 / 协议层 + - `TCP` + - `UDP` + - `ICMPv4` + - `ICMPv6` + - `Unknown` + +当前 TCP 轻量提示重点覆盖: + +- 三次握手 +- 四次挥手 +- 普通 ACK +- 重传 +- keepalive +- keepalive response +- RST +- ECE / CWR + +## 5. 如何选接口 + +优先级建议: + +- 大多数工具直接用 `Analyzer` +- 只要报文事实时用 `Decoder` +- 你已经有自己的输入管线,只缺轻量状态跟踪时用 `Tracker` + +如果没有特殊理由,优先选择 `Analyzer`,接入成本最低。 + +## 6. 包结构 + +### 6.1 主包 `b612.me/bcap` + +负责: + +- 统一事实模型 +- 统一 flow 模型 +- 统一 hint 模型 +- 轻量 TCP 跟踪 + +### 6.2 子包 `libpcap` + +负责: + +- 基于 pcap 的抓包输入适配 + +### 6.3 子包 `nfq` + +负责: + +- 基于 NFQUEUE 的输入适配 + +通常的组合方式是: + +- `libpcap` / `nfq` 提供输入 +- `bcap.Analyzer` 负责解析与提示 +- 上层工具负责展示、统计、导出和策略 + +## 7. 文档导航 + +如果你只是要快速接入,看本 README 基本够用。 + +如果你要看完整 API、模型字段、配置项和迁移说明,请继续阅读: + +- [`doc/api.md`](./doc/api.md) + +如果你要看当前架构思路和设计边界,请看: + +- [`doc/dev.md`](./doc/dev.md) + +## 8. 迁移提示 + +`bcap` 主包已经移除了旧的胖接口,旧的以下模型不再推荐使用,并且主体已经删除: + +- `Packets` +- `PacketInfo` +- `ParsePacket` +- `NewPackets` +- `NewPacketsWithConfig` +- `LegacyPacketInfoFromObservation` +- `GetStateDescription` +- `PrintStats` +- `ExportConnectionsToJSON` + +新的迁移方向是: + +- 单包事实解析改用 `Decoder` +- 报文观察结果改用 `Observation` +- TCP 轻量状态识别改用 `Tracker` +- 大多数场景直接改用 `Analyzer` + +## 9. 测试 + +开发或改动后,至少建议执行: + +```bash +go test ./... +``` + +## 10. License + +本项目采用 Apache License 2.0。 + +完整协议文本见: + +- [`LICENSE`](./LICENSE) diff --git a/analyzer.go b/analyzer.go new file mode 100644 index 0000000..296bd73 --- /dev/null +++ b/analyzer.go @@ -0,0 +1,73 @@ +package bcap + +import ( + "sync" + "time" + + "github.com/gopacket/gopacket" +) + +type Analyzer struct { + decoder *Decoder + tracker *Tracker + + mu sync.Mutex + firstPacketTS time.Time +} + +func NewAnalyzer() *Analyzer { + return NewAnalyzerWithConfig(nil) +} + +func NewAnalyzerWithConfig(config *PacketsConfig) *Analyzer { + return &Analyzer{ + decoder: NewDecoder(), + tracker: NewTrackerWithConfig(config), + } +} + +func (a *Analyzer) Stop() { + if a.tracker != nil { + a.tracker.Stop() + } +} + +func (a *Analyzer) Decoder() *Decoder { + return a.decoder +} + +func (a *Analyzer) Tracker() *Tracker { + return a.tracker +} + +func (a *Analyzer) ObservePacket(packet gopacket.Packet) (Observation, error) { + return a.ObservePacketWithOptions(packet, DecodeOptions{}) +} + +func (a *Analyzer) ObservePacketWithOptions(packet gopacket.Packet, opts DecodeOptions) (Observation, error) { + if opts.BaseTime.IsZero() { + opts.BaseTime = a.ensureBaseTime(packet) + } + decoded, err := a.decoder.DecodeWithOptions(packet, opts) + if err != nil { + return Observation{}, err + } + return a.tracker.Observe(decoded) +} + +func (a *Analyzer) ensureBaseTime(packet gopacket.Packet) time.Time { + a.mu.Lock() + defer a.mu.Unlock() + + if packet == nil { + return time.Time{} + } + metadata := packet.Metadata() + if metadata == nil || metadata.Timestamp.IsZero() { + return time.Time{} + } + if a.firstPacketTS.IsZero() { + a.firstPacketTS = metadata.Timestamp + } + return a.firstPacketTS +} diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..0930f20 --- /dev/null +++ b/api_test.go @@ -0,0 +1,315 @@ +package bcap + +import ( + "net" + "testing" + "time" +) + +func TestDecoderDecodeARP(t *testing.T) { + decoder := NewDecoder() + base := time.Unix(1700005000, 0) + + packet := mustBuildARPPacket(t, base, arpPacketSpec{ + srcMAC: "02:00:00:00:00:01", + dstMAC: "ff:ff:ff:ff:ff:ff", + senderMAC: "02:00:00:00:00:01", + targetMAC: "00:00:00:00:00:00", + senderIP: "10.0.0.1", + targetIP: "10.0.0.2", + operation: 1, + }) + + decoded, err := decoder.Decode(packet) + if err != nil { + t.Fatalf("decode arp: %v", err) + } + if decoded.Network.Family != NetworkFamilyARP { + t.Fatalf("network family = %q, want %q", decoded.Network.Family, NetworkFamilyARP) + } + if decoded.Transport.Kind != ProtocolARP { + t.Fatalf("protocol = %q, want %q", decoded.Transport.Kind, ProtocolARP) + } + if decoded.Network.ARP == nil { + t.Fatal("expected arp facts") + } + if decoded.Network.ARP.SenderIP != "10.0.0.1" { + t.Fatalf("sender ip = %q, want %q", decoded.Network.ARP.SenderIP, "10.0.0.1") + } + if decoded.Network.ARP.TargetIP != "10.0.0.2" { + t.Fatalf("target ip = %q, want %q", decoded.Network.ARP.TargetIP, "10.0.0.2") + } +} + +func TestDecoderDecodeIPv4WithoutTransportReturnsUnknown(t *testing.T) { + decoder := NewDecoder() + base := time.Unix(1700005001, 0) + + packet := mustBuildIPv4OnlyPacket(t, base, "10.0.0.1", "10.0.0.2") + decoded, err := decoder.Decode(packet) + if err != nil { + t.Fatalf("decode ipv4-only: %v", err) + } + if decoded.Network.Family != NetworkFamilyIPv4 { + t.Fatalf("network family = %q, want %q", decoded.Network.Family, NetworkFamilyIPv4) + } + if decoded.Transport.Kind != ProtocolUnknown { + t.Fatalf("transport kind = %q, want %q", decoded.Transport.Kind, ProtocolUnknown) + } +} + +func TestAnalyzerObservePacketTCP(t *testing.T) { + analyzer := NewAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700005002, 0) + + obs, err := analyzer.ObservePacket(mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 12345, + dstPort: 80, + seq: 100, + syn: true, + window: 4096, + })) + if err != nil { + t.Fatalf("observe tcp packet: %v", err) + } + if obs.Flow.Forward.Protocol != ProtocolTCP { + t.Fatalf("flow protocol = %q, want %q", obs.Flow.Forward.Protocol, ProtocolTCP) + } + if obs.Hints.TCP == nil { + t.Fatal("expected tcp hint") + } + if obs.Hints.TCP.Event != TCPEventSYN { + t.Fatalf("tcp event = %q, want %q", obs.Hints.TCP.Event, TCPEventSYN) + } + if obs.Hints.Summary.Code != string(TagTCPHandshakeSYN) { + t.Fatalf("summary = %q, want %q", obs.Hints.Summary.Code, TagTCPHandshakeSYN) + } +} + +func TestAnalyzerObservePacketARP(t *testing.T) { + analyzer := NewAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700005003, 0) + + obs, err := analyzer.ObservePacket(mustBuildARPPacket(t, base, arpPacketSpec{ + srcMAC: "02:00:00:00:00:01", + dstMAC: "ff:ff:ff:ff:ff:ff", + senderMAC: "02:00:00:00:00:01", + targetMAC: "00:00:00:00:00:00", + senderIP: "10.0.0.1", + targetIP: "10.0.0.2", + operation: 1, + })) + if err != nil { + t.Fatalf("observe arp packet: %v", err) + } + if obs.Flow.Forward.Protocol != ProtocolARP { + t.Fatalf("flow protocol = %q, want %q", obs.Flow.Forward.Protocol, ProtocolARP) + } + if obs.Hints.ARP == nil { + t.Fatal("expected arp hint") + } + if !obs.Hints.ARP.Request { + t.Fatal("expected arp request hint") + } + if obs.Hints.Summary.Code != string(TagARPRequest) { + t.Fatalf("summary = %q, want %q", obs.Hints.Summary.Code, TagARPRequest) + } +} + +func TestTrackerObserveDecodedTCPWithoutRawPacket(t *testing.T) { + decoder := NewDecoder() + tracker := NewTracker() + defer tracker.Stop() + base := time.Unix(1700005004, 0) + + decoded, err := decoder.Decode(mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 12345, + dstPort: 80, + seq: 100, + syn: true, + window: 4096, + })) + if err != nil { + t.Fatalf("decode tcp: %v", err) + } + decoded.Raw.Packet = nil + + obs, err := tracker.Observe(decoded) + if err != nil { + t.Fatalf("observe decoded tcp: %v", err) + } + if obs.Hints.TCP == nil { + t.Fatal("expected tcp hint") + } + if obs.Hints.TCP.Event != TCPEventSYN { + t.Fatalf("tcp event = %q, want %q", obs.Hints.TCP.Event, TCPEventSYN) + } +} + +func TestAnalyzerObservePacketWithOptionsSrcMACOverride(t *testing.T) { + analyzer := NewAnalyzer() + defer analyzer.Stop() + + override := net.HardwareAddr{0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee} + base := time.Unix(1700005005, 0) + + obs, err := analyzer.ObservePacketWithOptions(mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 12345, + dstPort: 80, + seq: 100, + syn: true, + window: 4096, + }), DecodeOptions{ + SrcMACOverride: override, + }) + if err != nil { + t.Fatalf("observe tcp packet with options: %v", err) + } + if got := obs.Packet.Link.SrcMAC.String(); got != override.String() { + t.Fatalf("src mac = %q, want %q", got, override.String()) + } + if obs.Packet.Meta.RelativeTime != 0 { + t.Fatalf("relative time = %v, want 0", obs.Packet.Meta.RelativeTime) + } +} + +func TestAnalyzerObservePacketTCPKeepaliveResponse(t *testing.T) { + analyzer := NewAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700005006, 0) + + _, err := analyzer.ObservePacket(mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if err != nil { + t.Fatalf("observe baseline packet: %v", err) + } + + probe, err := analyzer.ObservePacket(mustBuildTCPPacket(t, base.Add(75*time.Second), tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if err != nil { + t.Fatalf("observe keepalive probe: %v", err) + } + if probe.Hints.TCP == nil || probe.Hints.TCP.Event != TCPEventKeepalive { + t.Fatalf("probe event = %#v, want %q", probe.Hints.TCP, TCPEventKeepalive) + } + + resp, err := analyzer.ObservePacket(mustBuildTCPPacket(t, base.Add(75*time.Second+50*time.Millisecond), tcpPacketSpec{ + srcIP: "122.210.110.99", + dstIP: "122.210.105.240", + srcPort: 60818, + dstPort: 3306, + seq: 2951532891, + ack: 172126746, + ackFlag: true, + window: 1024, + })) + if err != nil { + t.Fatalf("observe keepalive response: %v", err) + } + if resp.Hints.TCP == nil { + t.Fatal("expected tcp hint") + } + if resp.Hints.TCP.LegacyState != StateTcpKeepalive { + t.Fatalf("legacy state = %d, want %d", resp.Hints.TCP.LegacyState, StateTcpKeepalive) + } + if resp.Hints.TCP.Event != TCPEventKeepaliveResp { + t.Fatalf("tcp event = %q, want %q", resp.Hints.TCP.Event, TCPEventKeepaliveResp) + } + if !resp.Hints.TCP.KeepaliveResponse { + t.Fatal("expected keepalive response flag") + } + if resp.Hints.Summary.Code != string(TagTCPKeepaliveResp) { + t.Fatalf("summary = %q, want %q", resp.Hints.Summary.Code, TagTCPKeepaliveResp) + } + if !containsTag(resp.Hints.Tags, TagTCPKeepaliveResp) { + t.Fatalf("tags = %v, want %q", resp.Hints.Tags, TagTCPKeepaliveResp) + } + if !containsTag(resp.Hints.Tags, TagTCPKeepalive) { + t.Fatalf("tags = %v, want %q", resp.Hints.Tags, TagTCPKeepalive) + } +} + +func TestTrackerCleanupExpiredFlows(t *testing.T) { + cfg := DefaultConfig() + cfg.ConnectionTimeout = time.Second + cfg.CleanupInterval = 0 + + decoder := NewDecoder() + tracker := NewTrackerWithConfig(cfg) + defer tracker.Stop() + + stale, err := decoder.Decode(mustBuildTCPPacket(t, time.Now().Add(-5*time.Second), tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 12345, + dstPort: 80, + seq: 100, + syn: true, + window: 4096, + })) + if err != nil { + t.Fatalf("decode stale packet: %v", err) + } + if _, err := tracker.Observe(stale); err != nil { + t.Fatalf("observe stale packet: %v", err) + } + + fresh, err := decoder.Decode(mustBuildTCPPacket(t, time.Now(), tcpPacketSpec{ + srcIP: "10.0.0.3", + dstIP: "10.0.0.4", + srcPort: 23456, + dstPort: 443, + seq: 200, + syn: true, + window: 4096, + })) + if err != nil { + t.Fatalf("decode fresh packet: %v", err) + } + if _, err := tracker.Observe(fresh); err != nil { + t.Fatalf("observe fresh packet: %v", err) + } + + if got := tracker.ActiveFlowCount(); got != 2 { + t.Fatalf("active flow count = %d, want 2", got) + } + if removed := tracker.CleanupExpiredFlows(); removed != 1 { + t.Fatalf("removed stale flows = %d, want 1", removed) + } + if got := tracker.ActiveFlowCount(); got != 1 { + t.Fatalf("active flow count after cleanup = %d, want 1", got) + } +} + +func containsTag(tags []Tag, want Tag) bool { + for _, tag := range tags { + if tag == want { + return true + } + } + return false +} diff --git a/bcap.go b/bcap.go deleted file mode 100644 index 0279bd1..0000000 --- a/bcap.go +++ /dev/null @@ -1,1359 +0,0 @@ -package bcap - -import ( - "fmt" - "net" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gopacket/gopacket" - "github.com/gopacket/gopacket/layers" -) - -// ════════════════════════════════════════════════ -// 错误类型定义 / Error Type Definitions -// ════════════════════════════════════════════════ - -// ParseErrorType 解析错误类型 / Parse error type -type ParseErrorType int - -const ( - ErrTypeNone ParseErrorType = iota // 无错误 / No error - ErrTypeLinkLayer // 链路层错误 / Link layer error - ErrTypeNetwork // 网络层错误 / Network layer error - ErrTypeTransport // 传输层错误 / Transport layer error - ErrTypeUnsupported // 不支持的协议 / Unsupported protocol -) - -// ParseError 解析错误结构体,包含详细的错误信息 -// ParseError structure containing detailed error information -type ParseError struct { - Type ParseErrorType // 错误类型 / Error type - Layer string // 出错的层 / Layer where error occurred - Message string // 错误信息 / Error message - Err error // 原始错误 / Original error -} - -func (e *ParseError) Error() string { - if e.Err != nil { - return fmt.Sprintf("[%s] %s: %v", e.Layer, e.Message, e.Err) - } - return fmt.Sprintf("[%s] %s", e.Layer, e.Message) -} - -// NewParseError 创建新的解析错误 / Create new parse error -func NewParseError(errType ParseErrorType, layer, message string, err error) *ParseError { - return &ParseError{ - Type: errType, - Layer: layer, - Message: message, - Err: err, - } -} - -// ════════════════════════════════════════════════ -// 状态常量定义 / State Constants -// ════════════════════════════════════════════════════════════════════════════════ - -// TCP 状态描述符定义 / TCP state descriptor definitions -const ( - StateUnknown uint8 = 0 // 未知状态 / Unknown state - StateTcpConnect1 uint8 = 1 // 三次握手第一步 (SYN) / Three-way handshake step 1 (SYN) - StateTcpConnect2 uint8 = 2 // 三次握手第二步 (SYN-ACK) / Three-way handshake step 2 (SYN-ACK) - StateTcpConnect3 uint8 = 3 // 三次握手第三步 (ACK) / Three-way handshake step 3 (ACK) - StateTcpDisconnect1 uint8 = 4 // 四次挥手第一步 (FIN) / Four-way handshake step 1 (FIN) - StateTcpDisconnect2 uint8 = 5 // 四次挥手第二步 (ACK) / Four-way handshake step 2 (ACK) - StateTcpDisconnect23 uint8 = 6 // 四次挥手第二三步合并 (FIN-ACK) / Four-way handshake step 2+3 combined - StateTcpDisconnect3 uint8 = 7 // 四次挥手第三步 (FIN) / Four-way handshake step 3 (FIN) - StateTcpDisconnect4 uint8 = 8 // 四次挥手第四步 (ACK) / Four-way handshake step 4 (ACK) - StateTcpAckOk uint8 = 9 // 正常 ACK / Normal ACK - StateTcpRetransmit uint8 = 10 // TCP 重传 / TCP retransmission - StateTcpEce uint8 = 11 // TCP ECE 标志 / TCP ECE flag - StateTcpCwr uint8 = 12 // TCP CWR 标志 / TCP CWR flag - StateTcpRst uint8 = 13 // TCP RST 标志 / TCP RST flag - StateTcpKeepalive uint8 = 14 // TCP 保活 / TCP keepalive - StateUdp uint8 = 20 // UDP 数据包 / UDP packet - StateIcmp uint8 = 30 // ICMP 数据包 / ICMP packet - StateIcmpv6 uint8 = 31 // ICMPv6 数据包 / ICMPv6 packet -) - -// 默认配置常量 / Default configuration constants -const ( - DefaultConnectionTimeout = 5 * time.Minute // 默认连接超时时间 / Default connection timeout - DefaultCleanupInterval = 1 * time.Minute // 默认清理间隔 / Default cleanup interval - DefaultShardCount = 32 // 默认分片数量 / Default shard count -) - -// ════════════════════════════════════════════════ -// 回调函数类型定义 / Callback Function Types -// ════════════════════════════════════════════════ - -// ConnectionCallback 连接事件回调函数类型 -// ConnectionCallback connection event callback function type -type ConnectionCallback func(info PacketInfo) - -// ErrorCallback 错误事件回调函数类型 -// ErrorCallback error event callback function type -type ErrorCallback func(err *ParseError, packet gopacket.Packet) - -// ════════════════════════════════════════════════════════════════════════════════ -// 配置结构体 / Configuration Structures -// ════════════════════════════════════════════════════════════════════════════════ - -// PacketsConfig 数据包管理器配置 -// PacketsConfig configuration for packets manager -type PacketsConfig struct { - // 连接超时时间,超过此时间未活动的连接将被清理 - // Connection timeout, connections inactive beyond this time will be cleaned up - ConnectionTimeout time.Duration - - // 自动清理间隔,设置为 0 则禁用自动清理 - // Auto cleanup interval, set to 0 to disable auto cleanup - CleanupInterval time.Duration - - // 是否启用懒加载模式(按需解析详细信息) - // Whether to enable lazy parsing mode (parse details on demand) - LazyParsing bool - - // 是否启用调试日志 - // Whether to enable debug logging - EnableDebugLog bool - - // 分片数量(用于减少锁竞争) - // Shard count (for reducing lock contention) - ShardCount int - - // 新连接回调 / New connection callback - OnNewConnection ConnectionCallback - - // 连接关闭回调 / Connection closed callback - OnConnectionClosed ConnectionCallback - - // 错误回调 / Error callback - OnError ErrorCallback -} - -// DefaultConfig 返回默认配置 -// DefaultConfig returns default configuration -func DefaultConfig() *PacketsConfig { - return &PacketsConfig{ - ConnectionTimeout: DefaultConnectionTimeout, - CleanupInterval: DefaultCleanupInterval, - LazyParsing: false, - EnableDebugLog: false, - ShardCount: DefaultShardCount, - } -} - -// ════════════════════════════════════════════════ -// 统计信息结构体 / Statistics Structures -// ════════════════════════════════════════════════ - -// ErrorStats 错误统计信息 -// ErrorStats error statistics -type ErrorStats struct { - LinkLayerErrors uint64 // 链路层错误数 / Link layer error count - NetworkErrors uint64 // 网络层错误数 / Network layer error count - TransportErrors uint64 // 传输层错误数 / Transport layer error count - UnsupportedErrors uint64 // 不支持协议错误数 / Unsupported protocol error count - TotalErrors uint64 // 总错误数 / Total error count -} - -// ConnectionStats 连接统计信息 -// ConnectionStats connection statistics -type ConnectionStats struct { - ActiveConnections int64 // 当前活跃连接数 / Current active connections - TotalConnections uint64 // 历史总连接数 / Total historical connections - ClosedConnections uint64 // 已关闭连接数 / Closed connections - TimeoutConnections uint64 // 超时清理的连接数 / Timeout cleaned connections - TcpConnections int64 // TCP 连接数 / TCP connection count - UdpConnections int64 // UDP 连接数 / UDP connection count - IcmpConnections int64 // ICMP 连接数 / ICMP connection count -} - -// Stats 综合统计信息 -// Stats comprehensive statistics -type Stats struct { - Errors ErrorStats // 错误统计 / Error statistics - Connections ConnectionStats // 连接统计 / Connection statistics - StartTime time.Time // 启动时间 / Start time - LastCleanup time.Time // 上次清理时间 / Last cleanup time -} - -// ════════════════════════════════════════════════ -// 数据包信息结构体 / Packet Info Structure -// ════════════════════════════════════════════════════════════════════════════════ - -// PacketInfo 数据包信息结构体,包含完整的网络层和传输层信息 -// PacketInfo structure containing complete network and transport layer information -type PacketInfo struct { - Key string // 连接唯一标识 / Unique connection identifier - ReverseKey string // 反向连接标识 / Reverse connection identifier - Type string // 协议类型: tcp/udp/icmp/icmpv6 / Protocol type - SrcMac net.HardwareAddr // 源 MAC 地址 / Source MAC address - DstMac net.HardwareAddr // 目标 MAC 地址 / Destination MAC address - SrcIP string // 源 IP 地址 / Source IP address - SrcPort string // 源端口 (TCP/UDP) / Source port - DstIP string // 目标 IP 地址 / Destination IP address - DstPort string // 目标端口 (TCP/UDP) / Destination port - - // 时间戳相关字段 / Timestamp related fields - Timestamp time.Time // 数据包时间戳 / Packet timestamp - TimestampMicros int64 // 微秒级时间戳 / Microsecond timestamp - RelativeTime time.Duration // 相对于首包的时间偏移 / Time offset relative to first packet - - // ICMP 相关字段 / ICMP related fields - IcmpType uint8 // ICMP 类型 / ICMP type - IcmpCode uint8 // ICMP 代码 / ICMP code - IcmpChecksum uint16 // ICMP 校验和 / ICMP checksum - IcmpId uint16 // ICMP 标识符 / ICMP identifier - IcmpSeq uint16 // ICMP 序列号 / ICMP sequence number - - // 连接统计字段 / Connection statistics fields - PacketCount uint64 // 该连接的数据包计数 / Packet count for this connection - ByteCount uint64 // 该连接的字节计数 / Byte count for this connection - FirstSeen time.Time // 首次出现时间 / First seen time - LastSeen time.Time // 最后出现时间 / Last seen time - - // 内部状态字段 / Internal state fields - comment string // 用户自定义注释 / User-defined comment - packet gopacket.Packet // 原始数据包 / Raw packet - tcpSeq uint32 // TCP 序列号 / TCP sequence number - tcpAck uint32 // TCP 确认号 / TCP acknowledgment number - tcpWindow uint16 // TCP 窗口大小 / TCP window size - tcpPayloads int // 载荷长度 / Payload length - finState bool // TCP FIN 标志 / TCP FIN flag - synState bool // TCP SYN 标志 / TCP SYN flag - isFirst bool // 是否为首次出现 / Whether first occurrence - stateDescript uint8 // 状态描述符 / State descriptor -} - -// PacketInfo 的 Getter 方法 / PacketInfo getter methods - -func (p PacketInfo) StateDescript() uint8 { return p.stateDescript } -func (p PacketInfo) TcpPayloads() int { return p.tcpPayloads } -func (p PacketInfo) FinState() bool { return p.finState } -func (p PacketInfo) SynState() bool { return p.synState } -func (p PacketInfo) TcpWindow() uint16 { return p.tcpWindow } -func (p PacketInfo) TcpAck() uint32 { return p.tcpAck } -func (p PacketInfo) TcpSeq() uint32 { return p.tcpSeq } -func (p PacketInfo) Packet() gopacket.Packet { return p.packet } -func (p PacketInfo) Comment() string { return p.comment } -func (p *PacketInfo) SetComment(comment string) { p.comment = comment } - -// ════════════════════════════════════════════════ -// 分片 Map 实现(减少锁竞争)/ Sharded Map Implementation (reduce lock contention) -// ════════════════════════════════════════════════ - -// shard 单个分片 -// shard single shard -type shard struct { - sync.RWMutex - items map[string]PacketInfo -} - -// shardedMap 分片 Map,通过将数据分散到多个分片来减少锁竞争 -// shardedMap sharded map, reduces lock contention by distributing data across multiple shards -type shardedMap struct { - shards []*shard - shardCount int -} - -// newShardedMap 创建新的分片 Map -// newShardedMap creates a new sharded map -func newShardedMap(shardCount int) *shardedMap { - if shardCount <= 0 { - shardCount = DefaultShardCount - } - sm := &shardedMap{ - shards: make([]*shard, shardCount), - shardCount: shardCount, - } - for i := 0; i < shardCount; i++ { - sm.shards[i] = &shard{ - items: make(map[string]PacketInfo), - } - } - return sm -} - -// getShard 根据 key 获取对应的分片 -// getShard gets the shard for a given key -func (sm *shardedMap) getShard(key string) *shard { - hash := fnvHash(key) - return sm.shards[hash%uint32(sm.shardCount)] -} - -// fnvHash FNV-1a 哈希函数,用于快速计算字符串哈希 -// fnvHash FNV-1a hash function for fast string hashing -func fnvHash(key string) uint32 { - hash := uint32(2166136261) - for i := 0; i < len(key); i++ { - hash ^= uint32(key[i]) - hash *= 16777619 - } - return hash -} - -// Get 获取值 / Get value -func (sm *shardedMap) Get(key string) (PacketInfo, bool) { - shard := sm.getShard(key) - shard.RLock() - val, ok := shard.items[key] - shard.RUnlock() - return val, ok -} - -// Set 设置值 / Set value -func (sm *shardedMap) Set(key string, value PacketInfo) { - shard := sm.getShard(key) - shard.Lock() - shard.items[key] = value - shard.Unlock() -} - -// Delete 删除值 / Delete value -func (sm *shardedMap) Delete(key string) { - shard := sm.getShard(key) - shard.Lock() - delete(shard.items, key) - shard.Unlock() -} - -// DeleteBatch 批量删除 / Batch delete -func (sm *shardedMap) DeleteBatch(keys []string) { - // 按分片分组 keys / Group keys by shard - shardKeys := make(map[int][]string) - for _, key := range keys { - hash := fnvHash(key) - shardIdx := int(hash % uint32(sm.shardCount)) - shardKeys[shardIdx] = append(shardKeys[shardIdx], key) - } - - // 对每个分片批量删除 / Batch delete for each shard - for shardIdx, keys := range shardKeys { - shard := sm.shards[shardIdx] - shard.Lock() - for _, key := range keys { - delete(shard.items, key) - } - shard.Unlock() - } -} - -// Len 获取总长度 / Get total length -func (sm *shardedMap) Len() int { - total := 0 - for _, shard := range sm.shards { - shard.RLock() - total += len(shard.items) - shard.RUnlock() - } - return total -} - -// GetAll 获取所有项 / Get all items -func (sm *shardedMap) GetAll() []PacketInfo { - var result []PacketInfo - for _, shard := range sm.shards { - shard.RLock() - for _, v := range shard.items { - result = append(result, v) - } - shard.RUnlock() - } - return result -} - -// Range 遍历所有项 / Range over all items -func (sm *shardedMap) Range(f func(key string, value PacketInfo) bool) { - for _, shard := range sm.shards { - shard.RLock() - for k, v := range shard.items { - if !f(k, v) { - shard.RUnlock() - return - } - } - shard.RUnlock() - } -} - -// Clear 清空所有项 / Clear all items -func (sm *shardedMap) Clear() { - for _, shard := range sm.shards { - shard.Lock() - shard.items = make(map[string]PacketInfo) - shard.Unlock() - } -} - -// ════════════════════════════════════════════════ -// 对象池(减少内存分配)/ Object Pool (reduce memory allocation) -// ════════════════════════════════════════════════ - -// stringBuilderPool strings.Builder 对象池 -var stringBuilderPool = sync.Pool{ - New: func() interface{} { - return &strings.Builder{} - }, -} - -// getStringBuilder 从对象池获取 strings.Builder -// getStringBuilder gets strings.Builder from pool -func getStringBuilder() *strings.Builder { - return stringBuilderPool.Get().(*strings.Builder) -} - -// putStringBuilder 将 strings.Builder 放回对象池 -// putStringBuilder puts strings.Builder back to pool -func putStringBuilder(sb *strings.Builder) { - sb.Reset() - stringBuilderPool.Put(sb) -} - -// buildKey 使用对象池优化的 key 构建函数 -// buildKey optimized key building function using object pool -func buildKey(protocol, srcIP, srcPort, dstIP, dstPort string) string { - sb := getStringBuilder() - defer putStringBuilder(sb) - - sb.WriteString(protocol) - sb.WriteString("://") - sb.WriteString(srcIP) - if srcPort != "" { - sb.WriteString(":") - sb.WriteString(srcPort) - } - sb.WriteString("=") - sb.WriteString(dstIP) - if dstPort != "" { - sb.WriteString(":") - sb.WriteString(dstPort) - } - - return sb.String() -} - -// ════════════════════════════════════════════════ -// Packets 主结构体 / Packets Main Structure -// ════════════════════════════════════════════════ - -// Packets 数据包管理器,用于跟踪和解析网络数据包 -// Packets manager for tracking and parsing network packets -type Packets struct { - config *PacketsConfig // 配置 / Configuration - connections *shardedMap // 连接映射表 / Connection map - stats Stats // 统计信息 / Statistics - statsLock sync.RWMutex // 统计信息锁 / Statistics lock - startTime time.Time // 启动时间 / Start time - firstPacketTime time.Time // 首个数据包时间 / First packet time - cleanupTicker *time.Ticker // 清理定时器 / Cleanup ticker - stopCleanup chan struct{} // 停止清理信号 / Stop cleanup signal - cleanupOnce sync.Once // 确保只启动一次清理 / Ensure cleanup starts only once -} - -// NewPackets 创建新的数据包管理器实例(使用默认配置) -// NewPackets creates a new Packets manager instance (with default config) -func NewPackets() *Packets { - return NewPacketsWithConfig(DefaultConfig()) -} - -// NewPacketsWithConfig 使用自定义配置创建数据包管理器 -// NewPacketsWithConfig creates Packets manager with custom config -func NewPacketsWithConfig(config *PacketsConfig) *Packets { - if config == nil { - config = DefaultConfig() - } - - p := &Packets{ - config: config, - connections: newShardedMap(config.ShardCount), - startTime: time.Now(), - stopCleanup: make(chan struct{}), - } - - p.stats.StartTime = p.startTime - - // 启动自动清理 goroutine / Start auto cleanup goroutine - if config.CleanupInterval > 0 { - p.startAutoCleanup() - } - - return p -} - -// startAutoCleanup 启动自动清理 goroutine -// startAutoCleanup starts auto cleanup goroutine -func (p *Packets) startAutoCleanup() { - p.cleanupOnce.Do(func() { - p.cleanupTicker = time.NewTicker(p.config.CleanupInterval) - go func() { - for { - select { - case <-p.cleanupTicker.C: - p.CleanupExpiredConnections() - case <-p.stopCleanup: - p.cleanupTicker.Stop() - return - } - } - }() - }) -} - -// Stop 停止数据包管理器(停止自动清理) -// Stop stops the packets manager (stops auto cleanup) -func (p *Packets) Stop() { - close(p.stopCleanup) -} - -// ════════════════════════════════════════════════ -// 统计和查询方法 / Statistics and Query Methods -// ════════════════════════════════════════════════ - -// GetStats 获取统计信息 -// GetStats gets statistics -func (p *Packets) GetStats() Stats { - p.statsLock.RLock() - defer p.statsLock.RUnlock() - - stats := p.stats - stats.Connections.ActiveConnections = int64(p.connections.Len()) - return stats -} - -// GetConnectionCount 获取当前活跃连接数 -// GetConnectionCount gets current active connection count -func (p *Packets) GetConnectionCount() int { - return p.connections.Len() -} - -// GetAllConnections 获取所有活跃连接 -// GetAllConnections gets all active connections -func (p *Packets) GetAllConnections() []PacketInfo { - return p.connections.GetAll() -} - -// GetConnectionsByIP 根据 IP 地址查询连接 -// GetConnectionsByIP queries connections by IP address -func (p *Packets) GetConnectionsByIP(ip string) []PacketInfo { - var result []PacketInfo - p.connections.Range(func(key string, value PacketInfo) bool { - if value.SrcIP == ip || value.DstIP == ip { - result = append(result, value) - } - return true - }) - return result -} - -// GetConnectionsByPort 根据端口查询连接 -// GetConnectionsByPort queries connections by port -func (p *Packets) GetConnectionsByPort(port string) []PacketInfo { - var result []PacketInfo - p.connections.Range(func(key string, value PacketInfo) bool { - if value.SrcPort == port || value.DstPort == port { - result = append(result, value) - } - return true - }) - return result -} - -// GetConnectionsByType 根据协议类型查询连接 -// GetConnectionsByType queries connections by protocol type -func (p *Packets) GetConnectionsByType(protocolType string) []PacketInfo { - var result []PacketInfo - p.connections.Range(func(key string, value PacketInfo) bool { - if value.Type == protocolType { - result = append(result, value) - } - return true - }) - return result -} - -// Key 根据 key 获取连接信息 -// Key gets connection info by key -func (p *Packets) Key(key string) (PacketInfo, bool) { - return p.connections.Get(key) -} - -// SetComment 设置连接注释 -// SetComment sets connection comment -func (p *Packets) SetComment(key, comment string) { - if info, ok := p.connections.Get(key); ok { - info.comment = comment - p.connections.Set(key, info) - } -} - -// ════════════════════════════════════════════════ -// 连接清理方法 / Connection Cleanup Methods -// ════════════════════════════════════════════════ - -// CleanupExpiredConnections 清理过期连接 -// CleanupExpiredConnections cleans up expired connections -func (p *Packets) CleanupExpiredConnections() int { - now := time.Now() - timeout := p.config.ConnectionTimeout - var expiredKeys []string - - // 收集过期的 keys / Collect expired keys - p.connections.Range(func(key string, value PacketInfo) bool { - if now.Sub(value.LastSeen) > timeout { - expiredKeys = append(expiredKeys, key) - } - return true - }) - - // 批量删除 / Batch delete - if len(expiredKeys) > 0 { - p.connections.DeleteBatch(expiredKeys) - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.TimeoutConnections += uint64(len(expiredKeys)) - p.stats.LastCleanup = now - p.statsLock.Unlock() - } - - return len(expiredKeys) -} - -// ClearAllConnections 清空所有连接 -// ClearAllConnections clears all connections -func (p *Packets) ClearAllConnections() { - p.connections.Clear() - - p.statsLock.Lock() - p.stats.Connections.ActiveConnections = 0 - p.statsLock.Unlock() -} - -// ════════════════════════════════════════════════ -// 错误处理方法 / Error Handling Methods -// ════════════════════════════════════════════════ - -// recordError 记录错误统计 -// recordError records error statistics -func (p *Packets) recordError(errType ParseErrorType) { - p.statsLock.Lock() - defer p.statsLock.Unlock() - - p.stats.Errors.TotalErrors++ - switch errType { - case ErrTypeLinkLayer: - p.stats.Errors.LinkLayerErrors++ - case ErrTypeNetwork: - p.stats.Errors.NetworkErrors++ - case ErrTypeTransport: - p.stats.Errors.TransportErrors++ - case ErrTypeUnsupported: - p.stats.Errors.UnsupportedErrors++ - } -} - -// handleError 处理错误(记录统计并调用回调) -// handleError handles error (records statistics and calls callback) -func (p *Packets) handleError(parseErr *ParseError, packet gopacket.Packet) { - p.recordError(parseErr.Type) - - if p.config.EnableDebugLog { - fmt.Printf("[ERROR] %s\n", parseErr.Error()) - } - - if p.config.OnError != nil { - p.config.OnError(parseErr, packet) - } -} - -// ════════════════════════════════════════════════ -// 数据包解析方法 / Packet Parsing Methods -// ════════════════════════════════════════════════ - -// ParsePacket 解析数据包,支持 Ethernet、Linux SLL、Linux SLL2 等链路层格式 -// ParsePacket parses packets, supporting Ethernet, Linux SLL, Linux SLL2 and other link layer formats -// -// 参数 / Parameters: -// - packet: gopacket 数据包对象 / gopacket packet object -// - opts: 可选参数,opts[0] 可以是 *[]byte 类型的 MAC 地址(用于 nfqueue 等场景) -// / Optional parameters, opts[0] can be *[]byte type MAC address (for nfqueue scenarios) -// -// 返回 / Returns: -// - PacketInfo: 解析后的数据包信息 / Parsed packet information -// - error: 解析错误 / Parse error -func (p *Packets) ParsePacket(packet gopacket.Packet, opts ...interface{}) (PacketInfo, error) { - var info PacketInfo - - // 提取时间戳 / Extract timestamp - metadata := packet.Metadata() - if metadata != nil { - info.Timestamp = metadata.Timestamp - info.TimestampMicros = metadata.Timestamp.UnixMicro() - - // 记录首个数据包时间 / Record first packet time - if p.firstPacketTime.IsZero() { - p.firstPacketTime = metadata.Timestamp - } - - // 计算相对时间 / Calculate relative time - info.RelativeTime = metadata.Timestamp.Sub(p.firstPacketTime) - } else { - info.Timestamp = time.Now() - info.TimestampMicros = info.Timestamp.UnixMicro() - } - - // ──────────────────────────────────────────────── - // 链路层:提取 MAC 地址 / Link Layer: Extract MAC addresses - // 支持 Ethernet、Linux SLL、Linux SLL2 / Support Ethernet, Linux SLL, Linux SLL2 - // ──────────────────────────────────────────────── - var srcMac, dstMac net.HardwareAddr - - // 情况 1: 标准以太网层 / Case 1: Standard Ethernet layer - if ethLayer := packet.Layer(layers.LayerTypeEthernet); ethLayer != nil { - eth := ethLayer.(*layers.Ethernet) - srcMac = eth.SrcMAC - dstMac = eth.DstMAC - } else if sllLayer := packet.Layer(layers.LayerTypeLinuxSLL); sllLayer != nil { - // 情况 2: Linux SLL (cooked capture v1) / Case 2: Linux SLL (cooked capture v1) - sll := sllLayer.(*layers.LinuxSLL) - if sll.AddrType == 1 && sll.AddrLen == 6 { // 1 = ARPHRD_ETHER - srcMac = sll.Addr - } - } else if sll2Layer := packet.Layer(layers.LayerTypeLinuxSLL2); sll2Layer != nil { - // 情况 3: Linux SLL2 (cooked capture v2) / Case 3: Linux SLL2 (cooked capture v2) - sll2 := sll2Layer.(*layers.LinuxSLL2) - if sll2.ARPHardwareType == 1 && sll2.AddrLength == 6 { // 1 = ARPHRD_ETHER - srcMac = sll2.Addr - } - } - - // 可选:使用外部提供的 MAC 地址覆盖 / Optional: Override with externally provided MAC - for k, v := range opts { - if k == 0 { - if b, ok := v.(*[]byte); ok && b != nil && len(*b) == 6 { - srcMac = net.HardwareAddr(*b) - } - } - } - - info.SrcMac = srcMac - info.DstMac = dstMac - - // ──────────────────────────────────────────────── - // 网络层:提取 IP 地址 / Network Layer: Extract IP addresses - // ──────────────────────────────────────────────── - if nw := packet.NetworkLayer(); nw != nil { - srcp, dstp := nw.NetworkFlow().Endpoints() - info.SrcIP = srcp.String() - info.DstIP = dstp.String() - } else { - parseErr := NewParseError(ErrTypeNetwork, "Network", "no valid network layer found", nil) - p.handleError(parseErr, packet) - return info, parseErr - } - - // ──────────────────────────────────────────────── - // 传输层:解析 TCP/UDP/ICMP / Transport Layer: Parse TCP/UDP/ICMP - // ──────────────────────────────────────────────── - - // TCP 协议 / TCP protocol - if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { - if tcp, ok := tcpLayer.(*layers.TCP); ok { - return p.parseTcp(info, packet, tcpLayer, tcp) - } - } - - // UDP 协议 / UDP protocol - if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil { - if udp, ok := udpLayer.(*layers.UDP); ok { - return p.parseUdp(info, packet, udpLayer, udp) - } - } - - // ICMPv4 协议 / ICMPv4 protocol - if icmpLayer := packet.Layer(layers.LayerTypeICMPv4); icmpLayer != nil { - if icmp, ok := icmpLayer.(*layers.ICMPv4); ok { - return p.parseIcmp(info, packet, icmpLayer, icmp) - } - } - - // ICMPv6 协议 / ICMPv6 protocol - if icmpv6Layer := packet.Layer(layers.LayerTypeICMPv6); icmpv6Layer != nil { - if icmpv6, ok := icmpv6Layer.(*layers.ICMPv6); ok { - return p.parseIcmpv6(info, packet, icmpv6Layer, icmpv6) - } - } - - // 不支持的协议类型 / Unsupported protocol type - parseErr := NewParseError(ErrTypeUnsupported, "Transport", - "unsupported packet type (not TCP/UDP/ICMP/ICMPv6)", nil) - p.handleError(parseErr, packet) - return info, parseErr -} - -// parseTcp 解析 TCP 数据包并进行状态跟踪 -// parseTcp parses TCP packets and performs state tracking -func (p *Packets) parseTcp(info PacketInfo, packet gopacket.Packet, layer gopacket.Layer, tcp *layers.TCP) (PacketInfo, error) { - // 使用优化的 key 构建 / Use optimized key building - info.Key = buildKey("tcp", info.SrcIP, fmt.Sprintf("%d", tcp.SrcPort), info.DstIP, fmt.Sprintf("%d", tcp.DstPort)) - info.ReverseKey = buildKey("tcp", info.DstIP, fmt.Sprintf("%d", tcp.DstPort), info.SrcIP, fmt.Sprintf("%d", tcp.SrcPort)) - info.Type = "tcp" - info.SrcPort = fmt.Sprintf("%d", tcp.SrcPort) - info.DstPort = fmt.Sprintf("%d", tcp.DstPort) - info.packet = packet - info.tcpSeq = tcp.Seq - info.tcpAck = tcp.Ack - info.tcpPayloads = len(layer.LayerPayload()) - info.finState = tcp.FIN - info.synState = tcp.SYN - info.tcpWindow = tcp.Window - - // 获取上一个同方向的数据包 / Get the last packet in the same direction - lastPacket, exists := p.connections.Get(info.Key) - - // 如果是新连接,初始化状态 / If it's a new connection, initialize state - if !exists { - lastPacket = PacketInfo{ - Key: info.Key, - ReverseKey: info.ReverseKey, - Type: "tcp", - SrcIP: info.SrcIP, - SrcPort: info.SrcPort, - DstIP: info.DstIP, - DstPort: info.DstPort, - FirstSeen: info.Timestamp, - LastSeen: info.Timestamp, - PacketCount: 0, - ByteCount: 0, - tcpSeq: tcp.Seq, - tcpAck: tcp.Ack, - tcpWindow: tcp.Window, - tcpPayloads: len(layer.LayerPayload()), - finState: tcp.FIN, - synState: tcp.SYN, - isFirst: true, - stateDescript: StateUnknown, - } - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.TotalConnections++ - atomic.AddInt64(&p.stats.Connections.TcpConnections, 1) - p.statsLock.Unlock() - - // 触发新连接回调 / Trigger new connection callback - if p.config.OnNewConnection != nil { - p.config.OnNewConnection(lastPacket) - } - } - - // 更新连接统计 / Update connection statistics - info.FirstSeen = lastPacket.FirstSeen - info.LastSeen = info.Timestamp - info.PacketCount = lastPacket.PacketCount + 1 - info.ByteCount = lastPacket.ByteCount + uint64(len(packet.Data())) - - // 获取反向连接的数据包 / Get the reverse direction packet - lastReverse, _ := p.connections.Get(info.ReverseKey) - - // 继承上一个数据包的注释和 MAC 地址 / Inherit comment and MAC addresses from last packet - if !lastPacket.isFirst { - info.comment = lastPacket.comment - if lastPacket.SrcMac != nil && len(info.SrcMac) == 0 { - info.SrcMac = lastPacket.SrcMac - } - if lastPacket.DstMac != nil && len(info.DstMac) == 0 { - info.DstMac = lastPacket.DstMac - } - } - if lastReverse.SrcMac != nil && len(info.DstMac) == 0 { - info.DstMac = lastReverse.SrcMac - } - - // ──────────────────────────────────────────────── - // TCP 状态机判断 / TCP state machine - // ──────────────────────────────────────────────── - - connectionClosed := false - - // RST 标志:连接重置 / RST flag: connection reset - if tcp.RST { - info.stateDescript = StateTcpRst - p.connections.Delete(info.Key) - p.connections.Delete(info.ReverseKey) - connectionClosed = true - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.ClosedConnections++ - atomic.AddInt64(&p.stats.Connections.TcpConnections, -1) - p.statsLock.Unlock() - - // 触发连接关闭回调 / Trigger connection closed callback - if p.config.OnConnectionClosed != nil { - p.config.OnConnectionClosed(info) - } - - return info, nil - } - - // 三次握手 / Three-way handshake - if tcp.SYN && !tcp.ACK { - info.stateDescript = StateTcpConnect1 // SYN - } else if tcp.SYN && tcp.ACK { - info.stateDescript = StateTcpConnect2 // SYN-ACK - } else if tcp.ACK { - if !tcp.FIN { - // 三次握手第三步 / Three-way handshake step 3 - if lastReverse.tcpSeq+1 == tcp.Ack && lastReverse.stateDescript == StateTcpConnect2 { - info.stateDescript = StateTcpConnect3 - } else if tcp.CWR { - info.stateDescript = StateTcpCwr - } else if tcp.ECE { - info.stateDescript = StateTcpEce - } - - if info.stateDescript != StateUnknown { - goto savereturn - } - - // TCP 保活检测 / TCP keepalive detection - if info.tcpSeq == lastReverse.tcpAck-1 && info.tcpSeq == lastPacket.tcpSeq+uint32(lastPacket.tcpPayloads)-1 { - info.stateDescript = StateTcpKeepalive - goto savereturn - } - - // TCP 重传检测 / TCP retransmission detection - if !lastPacket.isFirst { - if info.tcpSeq < lastPacket.tcpSeq+uint32(lastPacket.tcpPayloads) { - info.stateDescript = StateTcpRetransmit - goto savereturn - } - } - - // 四次挥手完成 / Four-way handshake completed - if lastReverse.finState && lastPacket.finState { - info.stateDescript = StateTcpDisconnect4 - p.connections.Delete(info.Key) - p.connections.Delete(info.ReverseKey) - connectionClosed = true - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.ClosedConnections++ - atomic.AddInt64(&p.stats.Connections.TcpConnections, -1) - p.statsLock.Unlock() - - // 触发连接关闭回调 / Trigger connection closed callback - if p.config.OnConnectionClosed != nil { - p.config.OnConnectionClosed(info) - } - - return info, nil - } - - // 四次挥手第二步 / Four-way handshake step 2 - if lastReverse.finState && lastReverse.tcpSeq+1 == info.tcpAck { - info.stateDescript = StateTcpDisconnect2 - goto savereturn - } - - // 正常 ACK / Normal ACK - info.stateDescript = StateTcpAckOk - } else { - // 四次挥手 / Four-way handshake - if !lastReverse.finState { - info.stateDescript = StateTcpDisconnect1 // FIN - } else { - if lastReverse.finState && lastReverse.tcpSeq+1 == info.tcpAck && - lastPacket.tcpAck == info.tcpAck && lastPacket.tcpSeq == info.tcpSeq { - info.stateDescript = StateTcpDisconnect3 // FIN (step 3) - } else { - info.stateDescript = StateTcpDisconnect23 // FIN-ACK (step 2+3 combined) - } - } - } - } - -savereturn: - // 只有在连接未关闭时才保存 / Only save if connection is not closed - if !connectionClosed { - p.connections.Set(info.Key, info) - } - return info, nil -} - -// parseUdp 解析 UDP 数据包 -// parseUdp parses UDP packets -func (p *Packets) parseUdp(info PacketInfo, packet gopacket.Packet, layer gopacket.Layer, udp *layers.UDP) (PacketInfo, error) { - // 使用优化的 key 构建 / Use optimized key building - info.Key = buildKey("udp", info.SrcIP, fmt.Sprintf("%d", udp.SrcPort), info.DstIP, fmt.Sprintf("%d", udp.DstPort)) - info.ReverseKey = buildKey("udp", info.DstIP, fmt.Sprintf("%d", udp.DstPort), info.SrcIP, fmt.Sprintf("%d", udp.SrcPort)) - info.Type = "udp" - info.SrcPort = fmt.Sprintf("%d", udp.SrcPort) - info.DstPort = fmt.Sprintf("%d", udp.DstPort) - info.packet = packet - info.tcpPayloads = len(layer.LayerPayload()) - info.stateDescript = StateUdp - - // 获取已存在的连接信息 / Get existing connection info - lastPacket, exists := p.connections.Get(info.Key) - - if !exists { - // 新 UDP 连接 / New UDP connection - info.FirstSeen = info.Timestamp - info.LastSeen = info.Timestamp - info.PacketCount = 1 - info.ByteCount = uint64(len(packet.Data())) - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.TotalConnections++ - atomic.AddInt64(&p.stats.Connections.UdpConnections, 1) - p.statsLock.Unlock() - - // 触发新连接回调 / Trigger new connection callback - if p.config.OnNewConnection != nil { - p.config.OnNewConnection(info) - } - } else { - // 更新已存在的连接 / Update existing connection - info.FirstSeen = lastPacket.FirstSeen - info.LastSeen = info.Timestamp - info.PacketCount = lastPacket.PacketCount + 1 - info.ByteCount = lastPacket.ByteCount + uint64(len(packet.Data())) - info.comment = lastPacket.comment - - // 继承 MAC 地址 / Inherit MAC addresses - if lastPacket.SrcMac != nil && len(info.SrcMac) == 0 { - info.SrcMac = lastPacket.SrcMac - } - if lastPacket.DstMac != nil && len(info.DstMac) == 0 { - info.DstMac = lastPacket.DstMac - } - } - - p.connections.Set(info.Key, info) - return info, nil -} - -// parseIcmp 解析 ICMPv4 数据包 -// parseIcmp parses ICMPv4 packets -func (p *Packets) parseIcmp(info PacketInfo, packet gopacket.Packet, layer gopacket.Layer, icmp *layers.ICMPv4) (PacketInfo, error) { - info.Type = "icmp" - info.IcmpType = uint8(icmp.TypeCode.Type()) - info.IcmpCode = uint8(icmp.TypeCode.Code()) - info.IcmpChecksum = icmp.Checksum - info.IcmpId = icmp.Id - info.IcmpSeq = icmp.Seq - info.tcpPayloads = len(layer.LayerPayload()) - info.stateDescript = StateIcmp - - // ICMP 的 Key 格式:icmp://srcIP=dstIP:type:code:id:seq - // ICMP Key format: icmp://srcIP=dstIP:type:code:id:seq - sb := getStringBuilder() - defer putStringBuilder(sb) - - sb.WriteString("icmp://") - sb.WriteString(info.SrcIP) - sb.WriteString("=") - sb.WriteString(info.DstIP) - sb.WriteString(":") - sb.WriteString(fmt.Sprintf("%d:%d:%d:%d", info.IcmpType, info.IcmpCode, info.IcmpId, info.IcmpSeq)) - info.Key = sb.String() - - sb.Reset() - sb.WriteString("icmp://") - sb.WriteString(info.DstIP) - sb.WriteString("=") - sb.WriteString(info.SrcIP) - sb.WriteString(":") - sb.WriteString(fmt.Sprintf("%d:%d:%d:%d", info.IcmpType, info.IcmpCode, info.IcmpId, info.IcmpSeq)) - info.ReverseKey = sb.String() - - info.packet = packet - - // 获取已存在的连接信息 / Get existing connection info - lastPacket, exists := p.connections.Get(info.Key) - - if !exists { - // 新 ICMP 连接 / New ICMP connection - info.FirstSeen = info.Timestamp - info.LastSeen = info.Timestamp - info.PacketCount = 1 - info.ByteCount = uint64(len(packet.Data())) - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.TotalConnections++ - atomic.AddInt64(&p.stats.Connections.IcmpConnections, 1) - p.statsLock.Unlock() - - // 触发新连接回调 / Trigger new connection callback - if p.config.OnNewConnection != nil { - p.config.OnNewConnection(info) - } - } else { - // 更新已存在的连接 / Update existing connection - info.FirstSeen = lastPacket.FirstSeen - info.LastSeen = info.Timestamp - info.PacketCount = lastPacket.PacketCount + 1 - info.ByteCount = lastPacket.ByteCount + uint64(len(packet.Data())) - info.comment = lastPacket.comment - - // 继承 MAC 地址 / Inherit MAC addresses - if lastPacket.SrcMac != nil && len(info.SrcMac) == 0 { - info.SrcMac = lastPacket.SrcMac - } - if lastPacket.DstMac != nil && len(info.DstMac) == 0 { - info.DstMac = lastPacket.DstMac - } - } - - p.connections.Set(info.Key, info) - return info, nil -} - -// parseIcmpv6 解析 ICMPv6 数据包 -// parseIcmpv6 parses ICMPv6 packets -func (p *Packets) parseIcmpv6(info PacketInfo, packet gopacket.Packet, layer gopacket.Layer, icmpv6 *layers.ICMPv6) (PacketInfo, error) { - info.Type = "icmpv6" - info.IcmpType = uint8(icmpv6.TypeCode.Type()) - info.IcmpCode = uint8(icmpv6.TypeCode.Code()) - info.IcmpChecksum = icmpv6.Checksum - info.tcpPayloads = len(layer.LayerPayload()) - info.stateDescript = StateIcmpv6 - - // ICMPv6 的 Key 格式:icmpv6://srcIP=dstIP:type:code - // ICMPv6 Key format: icmpv6://srcIP=dstIP:type:code - sb := getStringBuilder() - defer putStringBuilder(sb) - - sb.WriteString("icmpv6://") - sb.WriteString(info.SrcIP) - sb.WriteString("=") - sb.WriteString(info.DstIP) - sb.WriteString(":") - sb.WriteString(fmt.Sprintf("%d:%d", info.IcmpType, info.IcmpCode)) - info.Key = sb.String() - - sb.Reset() - sb.WriteString("icmpv6://") - sb.WriteString(info.DstIP) - sb.WriteString("=") - sb.WriteString(info.SrcIP) - sb.WriteString(":") - sb.WriteString(fmt.Sprintf("%d:%d", info.IcmpType, info.IcmpCode)) - info.ReverseKey = sb.String() - - info.packet = packet - - // 获取已存在的连接信息 / Get existing connection info - lastPacket, exists := p.connections.Get(info.Key) - - if !exists { - // 新 ICMPv6 连接 / New ICMPv6 connection - info.FirstSeen = info.Timestamp - info.LastSeen = info.Timestamp - info.PacketCount = 1 - info.ByteCount = uint64(len(packet.Data())) - - // 更新统计 / Update statistics - p.statsLock.Lock() - p.stats.Connections.TotalConnections++ - atomic.AddInt64(&p.stats.Connections.IcmpConnections, 1) - p.statsLock.Unlock() - - // 触发新连接回调 / Trigger new connection callback - if p.config.OnNewConnection != nil { - p.config.OnNewConnection(info) - } - } else { - // 更新已存在的连接 / Update existing connection - info.FirstSeen = lastPacket.FirstSeen - info.LastSeen = info.Timestamp - info.PacketCount = lastPacket.PacketCount + 1 - info.ByteCount = lastPacket.ByteCount + uint64(len(packet.Data())) - info.comment = lastPacket.comment - - // 继承 MAC 地址 / Inherit MAC addresses - if lastPacket.SrcMac != nil && len(info.SrcMac) == 0 { - info.SrcMac = lastPacket.SrcMac - } - if lastPacket.DstMac != nil && len(info.DstMac) == 0 { - info.DstMac = lastPacket.DstMac - } - } - - p.connections.Set(info.Key, info) - return info, nil -} - -// ════════════════════════════════════════════════ -// 辅助方法和工具函数 / Helper Methods and Utility Functions -// ════════════════════════════════════════════════ - -// GetStateDescription 获取状态描述符的文本描述 -// GetStateDescription gets text description of state descriptor -func GetStateDescription(state uint8) string { - switch state { - case StateUnknown: - return "Unknown" - case StateTcpConnect1: - return "TCP SYN" - case StateTcpConnect2: - return "TCP SYN-ACK" - case StateTcpConnect3: - return "TCP ACK (Connected)" - case StateTcpDisconnect1: - return "TCP FIN (Step 1)" - case StateTcpDisconnect2: - return "TCP ACK (Step 2)" - case StateTcpDisconnect23: - return "TCP FIN-ACK (Step 2+3)" - case StateTcpDisconnect3: - return "TCP FIN (Step 3)" - case StateTcpDisconnect4: - return "TCP ACK (Closed)" - case StateTcpAckOk: - return "TCP ACK" - case StateTcpRetransmit: - return "TCP Retransmission" - case StateTcpEce: - return "TCP ECE" - case StateTcpCwr: - return "TCP CWR" - case StateTcpRst: - return "TCP RST" - case StateTcpKeepalive: - return "TCP Keepalive" - case StateUdp: - return "UDP" - case StateIcmp: - return "ICMP" - case StateIcmpv6: - return "ICMPv6" - default: - return fmt.Sprintf("Unknown(%d)", state) - } -} - -// FormatDuration 格式化时间间隔为人类可读格式 -// FormatDuration formats duration to human readable format -func FormatDuration(d time.Duration) string { - if d < time.Microsecond { - return fmt.Sprintf("%dns", d.Nanoseconds()) - } else if d < time.Millisecond { - return fmt.Sprintf("%.2fµs", float64(d.Nanoseconds())/1000.0) - } else if d < time.Second { - return fmt.Sprintf("%.2fms", float64(d.Microseconds())/1000.0) - } else if d < time.Minute { - return fmt.Sprintf("%.2fs", d.Seconds()) - } else if d < time.Hour { - return fmt.Sprintf("%.2fm", d.Minutes()) - } - return fmt.Sprintf("%.2fh", d.Hours()) -} - -// FormatBytes 格式化字节数为人类可读格式 -// FormatBytes formats bytes to human readable format -func FormatBytes(bytes uint64) string { - const unit = 1024 - if bytes < unit { - return fmt.Sprintf("%d B", bytes) - } - div, exp := uint64(unit), 0 - for n := bytes / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) -} - -// PrintStats 打印统计信息(用于调试) -// PrintStats prints statistics (for debugging) -func (p *Packets) PrintStats() { - stats := p.GetStats() - - fmt.Println("════════════════════════════════════════") - fmt.Println("Packet Statistics / 数据包统计") - fmt.Println("════════════════════════════════════════") - fmt.Printf("Start Time / 启动时间: %s\n", stats.StartTime.Format("2006-01-02 15:04:05")) - fmt.Printf("Uptime / 运行时间: %s\n", FormatDuration(time.Since(stats.StartTime))) - fmt.Println() - - fmt.Println("Connection Statistics / 连接统计:") - fmt.Printf(" Active / 活跃: %d\n", stats.Connections.ActiveConnections) - fmt.Printf(" Total / 总计: %d\n", stats.Connections.TotalConnections) - fmt.Printf(" Closed / 已关闭: %d\n", stats.Connections.ClosedConnections) - fmt.Printf(" Timeout / 超时: %d\n", stats.Connections.TimeoutConnections) - fmt.Printf(" TCP: %d\n", stats.Connections.TcpConnections) - fmt.Printf(" UDP: %d\n", stats.Connections.UdpConnections) - fmt.Printf(" ICMP: %d\n", stats.Connections.IcmpConnections) - fmt.Println() - - fmt.Println("Error Statistics / 错误统计:") - fmt.Printf(" Total / 总计: %d\n", stats.Errors.TotalErrors) - fmt.Printf(" Link Layer / 链路层: %d\n", stats.Errors.LinkLayerErrors) - fmt.Printf(" Network / 网络层: %d\n", stats.Errors.NetworkErrors) - fmt.Printf(" Transport / 传输层: %d\n", stats.Errors.TransportErrors) - fmt.Printf(" Unsupported / 不支持: %d\n", stats.Errors.UnsupportedErrors) - - if !stats.LastCleanup.IsZero() { - fmt.Println() - fmt.Printf("Last Cleanup / 上次清理: %s\n", stats.LastCleanup.Format("2006-01-02 15:04:05")) - } - - fmt.Println("════════════════════════════════════════") -} - -// ExportConnectionsToJSON 导出连接信息为 JSON 格式(示例) -// ExportConnectionsToJSON exports connections to JSON format (example) -func (p *Packets) ExportConnectionsToJSON() string { - connections := p.GetAllConnections() - - sb := getStringBuilder() - defer putStringBuilder(sb) - - sb.WriteString("[\n") - for i, conn := range connections { - if i > 0 { - sb.WriteString(",\n") - } - sb.WriteString(fmt.Sprintf(` { - "key": "%s", - "type": "%s", - "src_ip": "%s", - "src_port": "%s", - "dst_ip": "%s", - "dst_port": "%s", - "packet_count": %d, - "byte_count": %d, - "state": "%s", - "first_seen": "%s", - "last_seen": "%s", - "duration": "%s" - }`, - conn.Key, - conn.Type, - conn.SrcIP, - conn.SrcPort, - conn.DstIP, - conn.DstPort, - conn.PacketCount, - conn.ByteCount, - GetStateDescription(conn.stateDescript), - conn.FirstSeen.Format(time.RFC3339), - conn.LastSeen.Format(time.RFC3339), - FormatDuration(conn.LastSeen.Sub(conn.FirstSeen)), - )) - } - sb.WriteString("\n]") - - return sb.String() -} diff --git a/conn_map.go b/conn_map.go new file mode 100644 index 0000000..6fc9db9 --- /dev/null +++ b/conn_map.go @@ -0,0 +1,50 @@ +package bcap + +import ( + "strings" + "sync" +) + +func fnvHash(key string) uint32 { + hash := uint32(2166136261) + for i := 0; i < len(key); i++ { + hash ^= uint32(key[i]) + hash *= 16777619 + } + return hash +} + +var stringBuilderPool = sync.Pool{ + New: func() interface{} { + return &strings.Builder{} + }, +} + +func getStringBuilder() *strings.Builder { + return stringBuilderPool.Get().(*strings.Builder) +} + +func putStringBuilder(sb *strings.Builder) { + sb.Reset() + stringBuilderPool.Put(sb) +} + +func buildKey(protocol, srcIP, srcPort, dstIP, dstPort string) string { + sb := getStringBuilder() + defer putStringBuilder(sb) + + sb.WriteString(protocol) + sb.WriteString("://") + sb.WriteString(srcIP) + if srcPort != "" { + sb.WriteString(":") + sb.WriteString(srcPort) + } + sb.WriteString("=") + sb.WriteString(dstIP) + if dstPort != "" { + sb.WriteString(":") + sb.WriteString(dstPort) + } + return sb.String() +} diff --git a/decoder.go b/decoder.go new file mode 100644 index 0000000..907d547 --- /dev/null +++ b/decoder.go @@ -0,0 +1,227 @@ +package bcap + +import ( + "net" + "strconv" + "time" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +type DecodeOptions struct { + BaseTime time.Time + SrcMACOverride net.HardwareAddr +} + +type Decoder struct{} + +func NewDecoder() *Decoder { + return &Decoder{} +} + +func (d *Decoder) Decode(packet gopacket.Packet) (Packet, error) { + return d.DecodeWithOptions(packet, DecodeOptions{}) +} + +func (d *Decoder) DecodeWithOptions(packet gopacket.Packet, opts DecodeOptions) (Packet, error) { + var decoded Packet + + decoded.Raw.Packet = packet + if metadata := packet.Metadata(); metadata != nil { + decoded.Meta.Timestamp = metadata.Timestamp + decoded.Meta.TimestampMicros = metadata.Timestamp.UnixMicro() + decoded.Meta.CaptureLength = metadata.CaptureLength + decoded.Meta.Length = metadata.Length + if !opts.BaseTime.IsZero() { + decoded.Meta.RelativeTime = metadata.Timestamp.Sub(opts.BaseTime) + } + } else { + decoded.Meta.Timestamp = time.Now() + decoded.Meta.TimestampMicros = decoded.Meta.Timestamp.UnixMicro() + } + + decodeLinkLayer(&decoded, packet) + if len(opts.SrcMACOverride) > 0 { + decoded.Link.SrcMAC = append(net.HardwareAddr(nil), opts.SrcMACOverride...) + } + + if arpLayer := packet.Layer(layers.LayerTypeARP); arpLayer != nil { + arp, ok := arpLayer.(*layers.ARP) + if !ok { + return decoded, NewParseError(ErrTypeNetwork, "ARP", "invalid arp layer", nil) + } + decodeARP(&decoded, arp) + return decoded, nil + } + + if err := decodeNetworkLayer(&decoded, packet); err != nil { + return decoded, err + } + decodeTransportLayer(&decoded, packet) + return decoded, nil +} + +func decodeLinkLayer(decoded *Packet, packet gopacket.Packet) { + decoded.Link.Kind = LinkKindUnknown + + if ethLayer := packet.Layer(layers.LayerTypeEthernet); ethLayer != nil { + if eth, ok := ethLayer.(*layers.Ethernet); ok { + decoded.Link.Kind = LinkKindEthernet + decoded.Link.SrcMAC = append(net.HardwareAddr(nil), eth.SrcMAC...) + decoded.Link.DstMAC = append(net.HardwareAddr(nil), eth.DstMAC...) + return + } + } + if sllLayer := packet.Layer(layers.LayerTypeLinuxSLL); sllLayer != nil { + if sll, ok := sllLayer.(*layers.LinuxSLL); ok { + decoded.Link.Kind = LinkKindLinuxSLL + if sll.AddrType == 1 && sll.AddrLen == 6 { + decoded.Link.SrcMAC = append(net.HardwareAddr(nil), sll.Addr...) + } + return + } + } + if sll2Layer := packet.Layer(layers.LayerTypeLinuxSLL2); sll2Layer != nil { + if sll2, ok := sll2Layer.(*layers.LinuxSLL2); ok { + decoded.Link.Kind = LinkKindLinuxSLL2 + if sll2.ARPHardwareType == 1 && sll2.AddrLength == 6 { + decoded.Link.SrcMAC = append(net.HardwareAddr(nil), sll2.Addr...) + } + } + } +} + +func decodeARP(decoded *Packet, arp *layers.ARP) { + decoded.Network.Family = NetworkFamilyARP + decoded.Network.ProtocolNumber = uint16(arp.Protocol) + decoded.Network.ARP = &ARPFacts{ + Operation: arp.Operation, + SenderMAC: append(net.HardwareAddr(nil), arp.SourceHwAddress...), + TargetMAC: append(net.HardwareAddr(nil), arp.DstHwAddress...), + SenderIP: arpProtocolAddressString(arp.SourceProtAddress), + TargetIP: arpProtocolAddressString(arp.DstProtAddress), + } + decoded.Network.SrcIP = decoded.Network.ARP.SenderIP + decoded.Network.DstIP = decoded.Network.ARP.TargetIP + decoded.Transport.Kind = ProtocolARP +} + +func arpProtocolAddressString(raw []byte) string { + switch len(raw) { + case net.IPv4len: + return net.IP(raw).String() + case net.IPv6len: + return net.IP(raw).String() + default: + return "" + } +} + +func decodeNetworkLayer(decoded *Packet, packet gopacket.Packet) error { + nw := packet.NetworkLayer() + if nw == nil { + return NewParseError(ErrTypeNetwork, "Network", "no valid network layer found", nil) + } + + src, dst := nw.NetworkFlow().Endpoints() + decoded.Network.SrcIP = src.String() + decoded.Network.DstIP = dst.String() + + switch layer := nw.(type) { + case *layers.IPv4: + decoded.Network.Family = NetworkFamilyIPv4 + decoded.Network.TTL = layer.TTL + decoded.Network.ProtocolNumber = uint16(layer.Protocol) + case *layers.IPv6: + decoded.Network.Family = NetworkFamilyIPv6 + decoded.Network.HopLimit = layer.HopLimit + decoded.Network.ProtocolNumber = uint16(layer.NextHeader) + default: + decoded.Network.Family = NetworkFamilyUnknown + } + return nil +} + +func decodeTransportLayer(decoded *Packet, packet gopacket.Packet) { + if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { + if tcp, ok := tcpLayer.(*layers.TCP); ok { + payloadLen := len(tcpLayer.LayerPayload()) + decoded.Transport.Kind = ProtocolTCP + decoded.Transport.Payload = payloadLen + decoded.Transport.TCP = &TCPFacts{ + SrcPort: strconv.Itoa(int(tcp.SrcPort)), + DstPort: strconv.Itoa(int(tcp.DstPort)), + Seq: tcp.Seq, + Ack: tcp.Ack, + Window: tcp.Window, + SYN: tcp.SYN, + ACK: tcp.ACK, + FIN: tcp.FIN, + RST: tcp.RST, + ECE: tcp.ECE, + CWR: tcp.CWR, + PSH: tcp.PSH, + Checksum: tcp.Checksum, + Payload: payloadLen, + } + return + } + } + if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil { + if udp, ok := udpLayer.(*layers.UDP); ok { + payloadLen := len(udpLayer.LayerPayload()) + decoded.Transport.Kind = ProtocolUDP + decoded.Transport.Payload = payloadLen + decoded.Transport.UDP = &UDPFacts{ + SrcPort: strconv.Itoa(int(udp.SrcPort)), + DstPort: strconv.Itoa(int(udp.DstPort)), + Length: udp.Length, + Payload: payloadLen, + } + return + } + } + if icmpLayer := packet.Layer(layers.LayerTypeICMPv4); icmpLayer != nil { + if icmp, ok := icmpLayer.(*layers.ICMPv4); ok { + payloadLen := len(icmpLayer.LayerPayload()) + decoded.Transport.Kind = ProtocolICMPv4 + decoded.Transport.Payload = payloadLen + decoded.Transport.ICMP = &ICMPFacts{ + Version: 4, + Type: uint8(icmp.TypeCode.Type()), + Code: uint8(icmp.TypeCode.Code()), + Checksum: icmp.Checksum, + ID: icmp.Id, + Seq: icmp.Seq, + Payload: payloadLen, + } + return + } + } + if icmpLayer := packet.Layer(layers.LayerTypeICMPv6); icmpLayer != nil { + if icmp, ok := icmpLayer.(*layers.ICMPv6); ok { + payloadLen := len(icmpLayer.LayerPayload()) + decoded.Transport.Kind = ProtocolICMPv6 + decoded.Transport.Payload = payloadLen + decoded.Transport.ICMP = &ICMPFacts{ + Version: 6, + Type: uint8(icmp.TypeCode.Type()), + Code: uint8(icmp.TypeCode.Code()), + Checksum: icmp.Checksum, + Payload: payloadLen, + } + return + } + } + + var payloadLen int + if app := packet.ApplicationLayer(); app != nil { + payloadLen = len(app.Payload()) + } else if nw := packet.NetworkLayer(); nw != nil { + payloadLen = len(nw.LayerPayload()) + } + decoded.Transport.Kind = ProtocolUnknown + decoded.Transport.Payload = payloadLen + decoded.Transport.Unknown = &UnknownTransportFacts{Payload: payloadLen} +} diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..fa87d8c --- /dev/null +++ b/doc/api.md @@ -0,0 +1,726 @@ +# bcap API 手册 + +开发者参考文档。快速接入说明见 [`../README.md`](../README.md)。 + +## 1. 主包职责 + +主包负责: + +- 统一的报文事实层 +- 统一的 flow 表达 +- 统一的轻量 hint 层 +- 以 TCP 为重点的轻状态跟踪 + +主包不负责: + +- 报告生成 +- 业务规则判断 +- 用户侧展示文案 +- 动作编排 +- 深度流重组 +- 应用层协议解析框架 + +核心对象: + +- `Decoder` + - 单包事实解码 +- `Tracker` + - flow 状态与轻量提示 +- `Analyzer` + - `Decoder + Tracker` 组合入口 + +## 2. 公开入口 + +### 2.1 `Decoder` + +构造: + +```go +decoder := bcap.NewDecoder() +``` + +主要方法: + +```go +Decode(packet gopacket.Packet) (Packet, error) +DecodeWithOptions(packet gopacket.Packet, opts DecodeOptions) (Packet, error) +``` + +使用时机: + +- 只想获得单包事实 +- 自己维护状态机 +- 想把 `bcap` 当作标准化事实提取层 + +### 2.2 `Tracker` + +构造: + +```go +tracker := bcap.NewTracker() +tracker := bcap.NewTrackerWithConfig(cfg) +``` + +主要方法: + +```go +Observe(packet Packet) (Observation, error) +CleanupExpiredFlows() int +ActiveFlowCount() int +Stop() +``` + +使用时机: + +- 已经在别处完成解码 +- 只想复用 `bcap` 的 flow / hint 跟踪能力 + +### 2.3 `Analyzer` + +构造: + +```go +analyzer := bcap.NewAnalyzer() +analyzer := bcap.NewAnalyzerWithConfig(cfg) +``` + +主要方法: + +```go +ObservePacket(packet gopacket.Packet) (Observation, error) +ObservePacketWithOptions(packet gopacket.Packet, opts DecodeOptions) (Observation, error) +Decoder() *Decoder +Tracker() *Tracker +Stop() +``` + +使用时机: + +- 在线抓包 +- 离线遍历 +- 绝大多数直接接入型工具 + +## 3. `DecodeOptions` + +`DecodeOptions` 目前包含两个字段: + +```go +type DecodeOptions struct { + BaseTime time.Time + SrcMACOverride net.HardwareAddr +} +``` + +字段: + +- `BaseTime` + - 用于计算 `Packet.Meta.RelativeTime` + - `Analyzer.ObservePacket(...)` 未显式传入时,会自动以首包时间作为基准 +- `SrcMACOverride` + - 用于覆盖源 MAC + +## 4. 数据模型 + +### 4.1 `Packet` + +`Packet` 表示单包事实。 + +```go +type Packet struct { + Meta Meta + Link LinkFacts + Network NetworkFacts + Transport TransportFacts + Raw RawFacts +} +``` + +不包含跨包推断结果。 + +### 4.2 `Meta` + +```go +type Meta struct { + Timestamp time.Time + TimestampMicros int64 + RelativeTime time.Duration + CaptureLength int + Length int +} +``` + +字段: + +- `Timestamp` / `TimestampMicros` + - 捕获时间 +- `RelativeTime` + - 相对 `BaseTime` 的时间差 +- `CaptureLength` + - 实际捕获长度 +- `Length` + - 原始报文长度 + +### 4.3 `LinkFacts` + +```go +type LinkFacts struct { + Kind LinkKind + SrcMAC net.HardwareAddr + DstMAC net.HardwareAddr +} +``` + +当前支持的链路类型: + +- `LinkKindEthernet` +- `LinkKindLinuxSLL` +- `LinkKindLinuxSLL2` +- `LinkKindUnknown` + +### 4.4 `NetworkFacts` + +```go +type NetworkFacts struct { + Family NetworkFamily + SrcIP string + DstIP string + TTL uint8 + HopLimit uint8 + ProtocolNumber uint16 + ARP *ARPFacts +} +``` + +当前支持的网络族: + +- `NetworkFamilyIPv4` +- `NetworkFamilyIPv6` +- `NetworkFamilyARP` +- `NetworkFamilyUnknown` + +备注: + +- IPv4 报文主要填 `TTL` +- IPv6 报文主要填 `HopLimit` +- ARP 报文会同时填充 `ARP` + +### 4.5 `TransportFacts` + +```go +type TransportFacts struct { + Kind ProtocolKind + Payload int + TCP *TCPFacts + UDP *UDPFacts + ICMP *ICMPFacts + Unknown *UnknownTransportFacts +} +``` + +当前支持协议: + +- `ProtocolTCP` +- `ProtocolUDP` +- `ProtocolICMPv4` +- `ProtocolICMPv6` +- `ProtocolARP` +- `ProtocolUnknown` + +### 4.6 协议事实结构 + +#### `TCPFacts` + +```go +type TCPFacts struct { + SrcPort string + DstPort string + Seq uint32 + Ack uint32 + Window uint16 + SYN bool + ACK bool + FIN bool + RST bool + ECE bool + CWR bool + PSH bool + Checksum uint16 + Payload int +} +``` + +#### `UDPFacts` + +```go +type UDPFacts struct { + SrcPort string + DstPort string + Length uint16 + Payload int +} +``` + +#### `ICMPFacts` + +```go +type ICMPFacts struct { + Version int + Type uint8 + Code uint8 + Checksum uint16 + ID uint16 + Seq uint16 + Payload int +} +``` + +#### `ARPFacts` + +```go +type ARPFacts struct { + Operation uint16 + SenderMAC net.HardwareAddr + TargetMAC net.HardwareAddr + SenderIP string + TargetIP string +} +``` + +### 4.7 `FlowKey` / `FlowRef` + +推荐使用结构化 flow,不把字符串 key 当成唯一公共接口。 + +```go +type Endpoint struct { + IP string + Port string +} + +type FlowKey struct { + Family NetworkFamily + Protocol ProtocolKind + Src Endpoint + Dst Endpoint +} + +type FlowRef struct { + Forward FlowKey + Reverse FlowKey + Stable string +} +``` + +方法: + +```go +func (f FlowKey) StableString() string +``` + +字段: + +- `Forward` + - 当前方向 +- `Reverse` + - 反向方向 +- `Stable` + - 稳定字符串表达,可用于日志和 map key + +### 4.8 `Observation` + +```go +type Observation struct { + Packet Packet + Flow FlowRef + Hints HintSet +} +``` + +- `Packet` + - 报文事实 +- `Flow` + - 流引用 +- `Hints` + - 推断结果 + +## 5. Hint 模型 + +### 5.1 `HintSet` + +```go +type HintSet struct { + Summary SummaryHint + Tags []Tag + + TCP *TCPHint + UDP *UDPHint + ICMP *ICMPHint + ARP *ARPHint +} +``` + +- `Summary` + - 摘要代码 +- `Tags` + - 标签集合 +- 协议专属 hint + - 放在对应子结构中 + +### 5.2 TCP hints + +```go +type TCPHint struct { + Phase TCPPhase + Event TCPEvent + LegacyState uint8 + Seq uint32 + Ack uint32 + Window uint16 + Payload int + Retransmission bool + Keepalive bool + KeepaliveResponse bool + RST bool + ECE bool + CWR bool +} +``` + +Phase: + +- `TCPPhaseHandshake` +- `TCPPhaseEstablished` +- `TCPPhaseTeardown` +- `TCPPhaseSpecial` +- `TCPPhaseUnknown` + +Event: + +- `TCPEventSYN` +- `TCPEventSYNACK` +- `TCPEventHandshakeACK` +- `TCPEventACK` +- `TCPEventRetransmission` +- `TCPEventKeepalive` +- `TCPEventKeepaliveResp` +- `TCPEventFIN` +- `TCPEventFINACK` +- `TCPEventTeardownACK` +- `TCPEventRST` +- `TCPEventECE` +- `TCPEventCWR` +- `TCPEventUnknown` + +常见 Tag: + +- `TagTCPHandshakeSYN` +- `TagTCPHandshakeSYNACK` +- `TagTCPHandshakeACK` +- `TagTCPTeardownFIN` +- `TagTCPTeardownFINACK` +- `TagTCPTeardownACK` +- `TagTCPPacket` +- `TagTCPRetransmit` +- `TagTCPKeepalive` +- `TagTCPKeepaliveResp` +- `TagTCPRst` +- `TagTCPEce` +- `TagTCPCwr` + +### 5.3 UDP / ICMP / ARP hints + +#### `UDPHint` + +```go +type UDPHint struct { + Payload int +} +``` + +对应 Tag: + +- `TagUDPPacket` + +#### `ICMPHint` + +```go +type ICMPHint struct { + Version int + Type uint8 + Code uint8 + IsEcho bool + IsEchoReply bool + IsUnreachable bool + IsTimeExceeded bool +} +``` + +对应 Tag: + +- `TagICMPPacket` +- `TagICMPEchoRequest` +- `TagICMPEchoReply` +- `TagICMPUnreachable` +- `TagICMPTimeExceeded` + +#### `ARPHint` + +```go +type ARPHint struct { + Operation uint16 + Request bool + Reply bool +} +``` + +对应 Tag: + +- `TagARPRequest` +- `TagARPReply` + +## 6. TCP 跟踪边界 + +`Tracker` 对 TCP 只做轻量跟踪,不做完整 TCP 栈模拟。 + +当前覆盖: + +- 握手识别 +- 挥手识别 +- 普通 ACK 识别 +- 重传识别 +- keepalive 识别 +- keepalive response 识别 +- RST / ECE / CWR 识别 + +实现说明: + +- 基于 flow 跟踪 +- 使用有限 segment 记忆辅助判断重传 +- keepalive 使用启发式判断 +- 支持超时清理 + +不覆盖: + +- 严格 TCP 协议验证 +- 深度重组 +- 业务级根因结论 + +## 7. 配置 + +### 7.1 `PacketsConfig` + +```go +type PacketsConfig struct { + ConnectionTimeout time.Duration + CleanupInterval time.Duration +} +``` + +字段: + +- `ConnectionTimeout` + - flow 状态保留时长 +- `CleanupInterval` + - 自动清理周期 + +默认值: + +```go +const ( + DefaultConnectionTimeout = 5 * time.Minute + DefaultCleanupInterval = 1 * time.Minute +) +``` + +默认配置: + +```go +cfg := bcap.DefaultConfig() +``` + +### 7.2 生命周期方法 + +`Tracker`: + +- `CleanupExpiredFlows() int` +- `ActiveFlowCount() int` +- `Stop()` + +`Analyzer`: + +- `Decoder() *Decoder` +- `Tracker() *Tracker` +- `Stop()` + +备注: + +- 长生命周期进程退出前调用 `Stop()` +- 如需自行控制清理节奏,可将 `CleanupInterval` 设为 `0`,再手动调用 `CleanupExpiredFlows()` + +## 8. 错误模型 + +`bcap` 使用 `*ParseError` 表达解析问题: + +```go +type ParseError struct { + Type ParseErrorType + Layer string + Message string + Err error +} +``` + +错误类型: + +- `ErrTypeLinkLayer` +- `ErrTypeNetwork` +- `ErrTypeTransport` +- `ErrTypeUnsupported` + +常见触发条件: + +- 没有有效网络层 +- 协议层对象类型不匹配 +- 跟踪阶段缺少必须的协议事实 + +## 9. 子包 + +### 9.1 `libpcap` + +`libpcap` 子包负责在线抓包输入。 + +入口: + +- `FindAllDevs()` +- `NewCatch(host, filter)` +- `NewCatchEth(eth, filter)` +- `SetRecall(func(gopacket.Packet))` +- `Run()` +- `Stop()` + +常见组合: + +- 子包接收 `gopacket.Packet` +- 主包 `Analyzer` 做解析和提示 +- 上层工具做展示和统计 + +### 9.2 `nfq` + +`nfq` 子包负责 NFQUEUE 输入适配。 + +入口: + +- `NewNfQueue(ctx, queid, maxqueue)` +- `SetRecall(func(id uint32, q *nfqueue.Nfqueue, p Packet))` +- `Run()` +- `Stop()` + +备注: + +- `nfq.Packet` 只是 NFQUEUE 输入包装 +- 它不是主包的 `bcap.Packet` +- 一般仍然从其中取 `gopacket.Packet`,再交给 `Analyzer` 或 `Decoder` + +## 10. 示例 + +### 10.1 直接用 `Analyzer` + +```go +analyzer := bcap.NewAnalyzer() +defer analyzer.Stop() + +obs, err := analyzer.ObservePacket(packet) +if err != nil { + return err +} + +fmt.Println(obs.Flow.Stable) +fmt.Println(obs.Packet.Transport.Kind) +fmt.Println(obs.Hints.Summary.Code) +``` + +### 10.2 自己管理解码和跟踪 + +```go +decoder := bcap.NewDecoder() +tracker := bcap.NewTracker() +defer tracker.Stop() + +decoded, err := decoder.DecodeWithOptions(packet, bcap.DecodeOptions{ + BaseTime: firstPacketTS, +}) +if err != nil { + return err +} + +obs, err := tracker.Observe(decoded) +if err != nil { + return err +} +``` + +### 10.3 典型离线遍历 + +```go +f, err := os.Open("sample.pcap") +if err != nil { + return err +} +defer f.Close() + +reader, err := pcapgo.NewReader(f) +if err != nil { + return err +} + +source := gopacket.NewPacketSource(reader, reader.LinkType()) +analyzer := bcap.NewAnalyzer() +defer analyzer.Stop() + +for packet := range source.Packets() { + obs, err := analyzer.ObservePacket(packet) + if err != nil { + continue + } + fmt.Println(obs.Packet.Network.SrcIP, obs.Hints.Summary.Code) +} +``` + +## 11. 辅助函数 + +主包还提供两个轻量格式化函数: + +- `FormatDuration(time.Duration) string` +- `FormatBytes(uint64) string` + +用于轻量日志与终端展示。 + +## 12. 迁移说明 + +主包旧接口已经被清理,删除项包括: + +- `Packets` +- `PacketInfo` +- `ParsePacket` +- `NewPackets` +- `NewPacketsWithConfig` +- `LegacyPacketInfoFromObservation` +- `GetStateDescription` +- `PrintStats` +- `ExportConnectionsToJSON` + +迁移方向: + +- 旧的单包解析入口迁到 `Decoder` +- 旧的“包 + 状态”混合对象迁到 `Observation` +- 旧的连接跟踪职责迁到 `Tracker` +- 大部分调用场景直接迁到 `Analyzer` + +旧代码如果大量依赖字符串 key: + +- 新代码优先改为使用 `FlowRef.Forward` / `FlowRef.Reverse` +- 如果只是为了日志或 map key,可以继续使用 `FlowRef.Stable` + +## 13. 相关文档 + +- 快速入口见 [`../README.md`](../README.md) +- 设计备忘见 [`dev.md`](./dev.md) diff --git a/doc/dev.md b/doc/dev.md new file mode 100644 index 0000000..10fe63d --- /dev/null +++ b/doc/dev.md @@ -0,0 +1,518 @@ +# 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。 + +在这个顺序下,主架构会先稳定下来,后续迁移也更可控。 diff --git a/format.go b/format.go new file mode 100644 index 0000000..76e26d6 --- /dev/null +++ b/format.go @@ -0,0 +1,34 @@ +package bcap + +import ( + "fmt" + "time" +) + +func FormatDuration(d time.Duration) string { + if d < time.Microsecond { + return fmt.Sprintf("%dns", d.Nanoseconds()) + } else if d < time.Millisecond { + return fmt.Sprintf("%.2fµs", float64(d.Nanoseconds())/1000.0) + } else if d < time.Second { + return fmt.Sprintf("%.2fms", float64(d.Microseconds())/1000.0) + } else if d < time.Minute { + return fmt.Sprintf("%.2fs", d.Seconds()) + } else if d < time.Hour { + return fmt.Sprintf("%.2fm", d.Minutes()) + } + return fmt.Sprintf("%.2fh", d.Hours()) +} + +func FormatBytes(bytes uint64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := uint64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..791e3a9 --- /dev/null +++ b/format_test.go @@ -0,0 +1,24 @@ +package bcap + +import ( + "testing" + "time" +) + +func TestFormatHelpers(t *testing.T) { + if got := FormatDuration(500 * time.Nanosecond); got != "500ns" { + t.Fatalf("FormatDuration(ns) = %q", got) + } + if got := FormatDuration(1500 * time.Microsecond); got != "1.50ms" { + t.Fatalf("FormatDuration(ms) = %q", got) + } + if got := FormatDuration(2 * time.Second); got != "2.00s" { + t.Fatalf("FormatDuration(s) = %q", got) + } + if got := FormatBytes(512); got != "512 B" { + t.Fatalf("FormatBytes(bytes) = %q", got) + } + if got := FormatBytes(1536); got != "1.50 KB" { + t.Fatalf("FormatBytes(kb) = %q", got) + } +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..fb73243 --- /dev/null +++ b/model.go @@ -0,0 +1,258 @@ +package bcap + +import ( + "net" + "time" + + "github.com/gopacket/gopacket" +) + +type ProtocolKind string + +const ( + ProtocolUnknown ProtocolKind = "unknown" + ProtocolTCP ProtocolKind = "tcp" + ProtocolUDP ProtocolKind = "udp" + ProtocolICMPv4 ProtocolKind = "icmp" + ProtocolICMPv6 ProtocolKind = "icmpv6" + ProtocolARP ProtocolKind = "arp" +) + +type NetworkFamily string + +const ( + NetworkFamilyUnknown NetworkFamily = "unknown" + NetworkFamilyIPv4 NetworkFamily = "ipv4" + NetworkFamilyIPv6 NetworkFamily = "ipv6" + NetworkFamilyARP NetworkFamily = "arp" +) + +type LinkKind string + +const ( + LinkKindUnknown LinkKind = "unknown" + LinkKindEthernet LinkKind = "ethernet" + LinkKindLinuxSLL LinkKind = "linux_sll" + LinkKindLinuxSLL2 LinkKind = "linux_sll2" +) + +type Tag string + +const ( + TagTransportUnknown Tag = "transport.unknown" + TagTCPHandshakeSYN Tag = "tcp.handshake.syn" + TagTCPHandshakeSYNACK Tag = "tcp.handshake.synack" + TagTCPHandshakeACK Tag = "tcp.handshake.ack" + TagTCPTeardownFIN Tag = "tcp.teardown.fin" + TagTCPTeardownFINACK Tag = "tcp.teardown.finack" + TagTCPTeardownACK Tag = "tcp.teardown.ack" + TagTCPPacket Tag = "tcp.packet" + TagTCPRetransmit Tag = "tcp.retransmit" + TagTCPKeepalive Tag = "tcp.keepalive" + TagTCPKeepaliveResp Tag = "tcp.keepalive.response" + TagTCPRst Tag = "tcp.rst" + TagTCPEce Tag = "tcp.ece" + TagTCPCwr Tag = "tcp.cwr" + TagUDPPacket Tag = "udp.packet" + TagICMPPacket Tag = "icmp.packet" + TagICMPEchoRequest Tag = "icmp.echo-request" + TagICMPEchoReply Tag = "icmp.echo-reply" + TagICMPUnreachable Tag = "icmp.unreachable" + TagICMPTimeExceeded Tag = "icmp.time-exceeded" + TagARPRequest Tag = "arp.request" + TagARPReply Tag = "arp.reply" +) + +type Packet struct { + Meta Meta + Link LinkFacts + Network NetworkFacts + Transport TransportFacts + Raw RawFacts +} + +type Meta struct { + Timestamp time.Time + TimestampMicros int64 + RelativeTime time.Duration + CaptureLength int + Length int +} + +type LinkFacts struct { + Kind LinkKind + SrcMAC net.HardwareAddr + DstMAC net.HardwareAddr +} + +type NetworkFacts struct { + Family NetworkFamily + SrcIP string + DstIP string + TTL uint8 + HopLimit uint8 + ProtocolNumber uint16 + ARP *ARPFacts +} + +type ARPFacts struct { + Operation uint16 + SenderMAC net.HardwareAddr + TargetMAC net.HardwareAddr + SenderIP string + TargetIP string +} + +type TransportFacts struct { + Kind ProtocolKind + Payload int + TCP *TCPFacts + UDP *UDPFacts + ICMP *ICMPFacts + Unknown *UnknownTransportFacts +} + +type TCPFacts struct { + SrcPort string + DstPort string + Seq uint32 + Ack uint32 + Window uint16 + SYN bool + ACK bool + FIN bool + RST bool + ECE bool + CWR bool + PSH bool + Checksum uint16 + Payload int +} + +type UDPFacts struct { + SrcPort string + DstPort string + Length uint16 + Payload int +} + +type ICMPFacts struct { + Version int + Type uint8 + Code uint8 + Checksum uint16 + ID uint16 + Seq uint16 + Payload int +} + +type UnknownTransportFacts struct { + Payload int +} + +type RawFacts struct { + Packet gopacket.Packet +} + +type Endpoint struct { + IP string + Port string +} + +type FlowKey struct { + Family NetworkFamily + Protocol ProtocolKind + Src Endpoint + Dst Endpoint +} + +type FlowRef struct { + Forward FlowKey + Reverse FlowKey + Stable string +} + +type Observation struct { + Packet Packet + Flow FlowRef + Hints HintSet +} + +type SummaryHint struct { + Code string +} + +type HintSet struct { + Summary SummaryHint + Tags []Tag + + TCP *TCPHint + UDP *UDPHint + ICMP *ICMPHint + ARP *ARPHint +} + +type TCPPhase string + +const ( + TCPPhaseUnknown TCPPhase = "unknown" + TCPPhaseHandshake TCPPhase = "handshake" + TCPPhaseEstablished TCPPhase = "established" + TCPPhaseTeardown TCPPhase = "teardown" + TCPPhaseSpecial TCPPhase = "special" +) + +type TCPEvent string + +const ( + TCPEventUnknown TCPEvent = "unknown" + TCPEventSYN TCPEvent = "syn" + TCPEventSYNACK TCPEvent = "synack" + TCPEventHandshakeACK TCPEvent = "handshake_ack" + TCPEventACK TCPEvent = "ack" + TCPEventRetransmission TCPEvent = "retransmission" + TCPEventKeepalive TCPEvent = "keepalive" + TCPEventKeepaliveResp TCPEvent = "keepalive_response" + TCPEventFIN TCPEvent = "fin" + TCPEventFINACK TCPEvent = "finack" + TCPEventTeardownACK TCPEvent = "teardown_ack" + TCPEventRST TCPEvent = "rst" + TCPEventECE TCPEvent = "ece" + TCPEventCWR TCPEvent = "cwr" +) + +type TCPHint struct { + Phase TCPPhase + Event TCPEvent + LegacyState uint8 + Seq uint32 + Ack uint32 + Window uint16 + Payload int + Retransmission bool + Keepalive bool + KeepaliveResponse bool + RST bool + ECE bool + CWR bool +} + +type UDPHint struct { + Payload int +} + +type ICMPHint struct { + Version int + Type uint8 + Code uint8 + IsEcho bool + IsEchoReply bool + IsUnreachable bool + IsTimeExceeded bool +} + +type ARPHint struct { + Operation uint16 + Request bool + Reply bool +} diff --git a/nfq/nfqueue.go b/nfq/nfqueue.go index 6a7c7bf..7277ca0 100644 --- a/nfq/nfqueue.go +++ b/nfq/nfqueue.go @@ -57,7 +57,7 @@ func (n *NfQueue) Run() error { } nfq, err := nfqueue.Open(&cfg) if err != nil { - return fmt.Errorf("failed to open nfqueue, err:", err) + return fmt.Errorf("failed to open nfqueue: %w", err) } if err := nfq.RegisterWithErrorFunc(n.ctx, func(a nfqueue.Attribute) int { @@ -65,7 +65,7 @@ func (n *NfQueue) Run() error { }, func(e error) int { return 0 }); err != nil { - return fmt.Errorf("failed to register handlers, err:", err) + return fmt.Errorf("failed to register handlers: %w", err) } <-n.ctx.Done() return nil diff --git a/state.go b/state.go new file mode 100644 index 0000000..6db0bb5 --- /dev/null +++ b/state.go @@ -0,0 +1,12 @@ +package bcap + +import "time" + +const repeatedKeepaliveMinInterval = time.Second +const trackedTCPSegments = 8 +const keepaliveResponseMaxDelay = 5 * time.Second + +type tcpSegmentRange struct { + seq uint32 + end uint32 +} diff --git a/tcp_seq.go b/tcp_seq.go new file mode 100644 index 0000000..a103f5b --- /dev/null +++ b/tcp_seq.go @@ -0,0 +1,15 @@ +package bcap + +func tcpSeqLess(a, b uint32) bool { return int32(a-b) < 0 } + +func tcpSeqLEQ(a, b uint32) bool { + return a == b || tcpSeqLess(a, b) +} + +func tcpSeqAdd(seq, delta uint32) uint32 { + return seq + delta +} + +func tcpSeqPrev(seq uint32) uint32 { + return tcpSeqAdd(seq, ^uint32(0)) +} diff --git a/tcp_test.go b/tcp_test.go new file mode 100644 index 0000000..9b8c67f --- /dev/null +++ b/tcp_test.go @@ -0,0 +1,747 @@ +package bcap + +import ( + "testing" + "time" + + "github.com/gopacket/gopacket" +) + +func TestObserveTCPStateTransitions(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000000, 0) + + cases := []struct { + name string + packet func(time.Time) observedPacket + want uint8 + wantLen int + }{ + { + name: "syn", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 100, + syn: true, + window: 4096, + })) + }, + want: StateTcpConnect1, + wantLen: 0, + }, + { + name: "synack", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.2", + dstIP: "10.0.0.1", + srcPort: 3306, + dstPort: 40000, + seq: 500, + ack: 101, + syn: true, + ackFlag: true, + window: 4096, + })) + }, + want: StateTcpConnect2, + wantLen: 0, + }, + { + name: "ack", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + })) + }, + want: StateTcpConnect3, + wantLen: 0, + }, + { + name: "server-ack", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.2", + dstIP: "10.0.0.1", + srcPort: 3306, + dstPort: 40000, + seq: 501, + ack: 101, + ackFlag: true, + window: 4096, + })) + }, + want: StateTcpAckOk, + wantLen: 0, + }, + { + name: "client-data", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("hello"), + })) + }, + want: StateTcpAckOk, + wantLen: 5, + }, + { + name: "retransmission", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("hello"), + })) + }, + want: StateTcpRetransmit, + wantLen: 5, + }, + { + name: "server-ack-after-data", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.2", + dstIP: "10.0.0.1", + srcPort: 3306, + dstPort: 40000, + seq: 501, + ack: 106, + ackFlag: true, + window: 4096, + })) + }, + want: StateTcpAckOk, + wantLen: 0, + }, + { + name: "keepalive", + packet: func(ts time.Time) observedPacket { + return mustObservePacket(t, analyzer, mustBuildTCPPacket(t, ts, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 105, + ack: 501, + ackFlag: true, + window: 4096, + })) + }, + want: StateTcpKeepalive, + wantLen: 0, + }, + } + + for i, tc := range cases { + info := tc.packet(base.Add(time.Duration(i) * time.Millisecond)) + if info.StateDescript() != tc.want { + t.Fatalf("%s: state = %d, want %d", tc.name, info.StateDescript(), tc.want) + } + if info.TcpPayloads() != tc.wantLen { + t.Fatalf("%s: payload len = %d, want %d", tc.name, info.TcpPayloads(), tc.wantLen) + } + } +} + +func TestObserveTCPRepeatedKeepaliveWithoutReverseResponse(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000500, 0) + + first := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if first.StateDescript() != StateTcpAckOk { + t.Fatalf("first probe state = %d, want %d", first.StateDescript(), StateTcpAckOk) + } + + second := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(75*time.Second), tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if second.StateDescript() != StateTcpKeepalive { + t.Fatalf("second probe state = %d, want %d", second.StateDescript(), StateTcpKeepalive) + } + + third := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(150*time.Second), tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if third.StateDescript() != StateTcpKeepalive { + t.Fatalf("third probe state = %d, want %d", third.StateDescript(), StateTcpKeepalive) + } +} + +func TestObserveTCPKeepaliveResponseAck(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000530, 0) + + _ = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + + probe := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(75*time.Second), tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if probe.StateDescript() != StateTcpKeepalive { + t.Fatalf("probe state = %d, want %d", probe.StateDescript(), StateTcpKeepalive) + } + + resp := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(75*time.Second+50*time.Millisecond), tcpPacketSpec{ + srcIP: "122.210.110.99", + dstIP: "122.210.105.240", + srcPort: 60818, + dstPort: 3306, + seq: 2951532891, + ack: 172126746, + ackFlag: true, + window: 1024, + })) + if resp.StateDescript() != StateTcpKeepalive { + t.Fatalf("keepalive response state = %d, want %d", resp.StateDescript(), StateTcpKeepalive) + } + if resp.Hints.TCP == nil || resp.Hints.TCP.Event != TCPEventKeepaliveResp { + t.Fatalf("response event = %#v, want %q", resp.Hints.TCP, TCPEventKeepaliveResp) + } +} + +func TestObserveTCPKeepaliveResponseAfterWindowFallsBackToAck(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000540, 0) + + _ = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + + probe := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(75*time.Second), tcpPacketSpec{ + srcIP: "122.210.105.240", + dstIP: "122.210.110.99", + srcPort: 3306, + dstPort: 60818, + seq: 172126745, + ack: 2951532891, + ackFlag: true, + window: 258, + })) + if probe.StateDescript() != StateTcpKeepalive { + t.Fatalf("probe state = %d, want %d", probe.StateDescript(), StateTcpKeepalive) + } + + resp := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(75*time.Second+keepaliveResponseMaxDelay+time.Second), tcpPacketSpec{ + srcIP: "122.210.110.99", + dstIP: "122.210.105.240", + srcPort: 60818, + dstPort: 3306, + seq: 2951532891, + ack: 172126746, + ackFlag: true, + window: 1024, + })) + if resp.StateDescript() != StateTcpAckOk { + t.Fatalf("late keepalive response state = %d, want %d", resp.StateDescript(), StateTcpAckOk) + } +} + +func TestObserveTCPRepeatedAckWithoutGapStaysAck(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000550, 0) + + _ = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.10.0.1", + dstIP: "10.10.0.2", + srcPort: 3306, + dstPort: 60818, + seq: 5000, + ack: 9000, + ackFlag: true, + window: 512, + })) + + next := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(100*time.Millisecond), tcpPacketSpec{ + srcIP: "10.10.0.1", + dstIP: "10.10.0.2", + srcPort: 3306, + dstPort: 60818, + seq: 5000, + ack: 9000, + ackFlag: true, + window: 512, + })) + if next.StateDescript() != StateTcpAckOk { + t.Fatalf("short-gap ack state = %d, want %d", next.StateDescript(), StateTcpAckOk) + } +} + +func TestObserveTCPPostHandshakeDataIsNotConnect3Again(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000565, 0) + + syn := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.11.0.1", + dstIP: "10.11.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 100, + syn: true, + window: 4096, + })) + if syn.StateDescript() != StateTcpConnect1 { + t.Fatalf("syn state = %d, want %d", syn.StateDescript(), StateTcpConnect1) + } + + synack := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(time.Millisecond), tcpPacketSpec{ + srcIP: "10.11.0.2", + dstIP: "10.11.0.1", + srcPort: 3306, + dstPort: 40000, + seq: 500, + ack: 101, + syn: true, + ackFlag: true, + window: 4096, + })) + if synack.StateDescript() != StateTcpConnect2 { + t.Fatalf("synack state = %d, want %d", synack.StateDescript(), StateTcpConnect2) + } + + ack := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(2*time.Millisecond), tcpPacketSpec{ + srcIP: "10.11.0.1", + dstIP: "10.11.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + })) + if ack.StateDescript() != StateTcpConnect3 { + t.Fatalf("handshake ack state = %d, want %d", ack.StateDescript(), StateTcpConnect3) + } + + data := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(3*time.Millisecond), tcpPacketSpec{ + srcIP: "10.11.0.1", + dstIP: "10.11.0.2", + srcPort: 40000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("hello"), + })) + if data.StateDescript() != StateTcpAckOk { + t.Fatalf("post-handshake data state = %d, want %d", data.StateDescript(), StateTcpAckOk) + } +} + +func TestObserveTCPHandshakeAcrossSequenceWraparound(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000575, 0) + + syn := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.12.0.1", + dstIP: "10.12.0.2", + srcPort: 40001, + dstPort: 3306, + seq: ^uint32(0), + syn: true, + window: 4096, + })) + if syn.StateDescript() != StateTcpConnect1 { + t.Fatalf("wrap syn state = %d, want %d", syn.StateDescript(), StateTcpConnect1) + } + + synack := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(time.Millisecond), tcpPacketSpec{ + srcIP: "10.12.0.2", + dstIP: "10.12.0.1", + srcPort: 3306, + dstPort: 40001, + seq: 500, + ack: 0, + syn: true, + ackFlag: true, + window: 4096, + })) + if synack.StateDescript() != StateTcpConnect2 { + t.Fatalf("wrap synack state = %d, want %d", synack.StateDescript(), StateTcpConnect2) + } + + ack := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(2*time.Millisecond), tcpPacketSpec{ + srcIP: "10.12.0.1", + dstIP: "10.12.0.2", + srcPort: 40001, + dstPort: 3306, + seq: 0, + ack: 501, + ackFlag: true, + window: 4096, + })) + if ack.StateDescript() != StateTcpConnect3 { + t.Fatalf("wrap handshake ack state = %d, want %d", ack.StateDescript(), StateTcpConnect3) + } + + data := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(3*time.Millisecond), tcpPacketSpec{ + srcIP: "10.12.0.1", + dstIP: "10.12.0.2", + srcPort: 40001, + dstPort: 3306, + seq: 0, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("hi"), + })) + if data.StateDescript() != StateTcpAckOk { + t.Fatalf("wrap post-handshake data state = %d, want %d", data.StateDescript(), StateTcpAckOk) + } +} + +func TestObserveTCPGapFillDoesNotLookLikeRetransmission(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700000580, 0) + + _, _ = mustEstablishTCPConnection( + t, + analyzer, + base, + "10.20.0.1", + 44000, + "10.20.0.2", + 3306, + 100, + 500, + ) + + _ = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(3*time.Millisecond), tcpPacketSpec{ + srcIP: "10.20.0.2", + dstIP: "10.20.0.1", + srcPort: 3306, + dstPort: 44000, + seq: 501, + ack: 101, + ackFlag: true, + window: 4096, + })) + + first := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(4*time.Millisecond), tcpPacketSpec{ + srcIP: "10.20.0.1", + dstIP: "10.20.0.2", + srcPort: 44000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("AAAAA"), + })) + if first.StateDescript() != StateTcpAckOk { + t.Fatalf("first payload state = %d, want %d", first.StateDescript(), StateTcpAckOk) + } + + later := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(5*time.Millisecond), tcpPacketSpec{ + srcIP: "10.20.0.1", + dstIP: "10.20.0.2", + srcPort: 44000, + dstPort: 3306, + seq: 111, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("CCCCC"), + })) + if later.StateDescript() != StateTcpAckOk { + t.Fatalf("later payload state = %d, want %d", later.StateDescript(), StateTcpAckOk) + } + + gapFill := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(6*time.Millisecond), tcpPacketSpec{ + srcIP: "10.20.0.1", + dstIP: "10.20.0.2", + srcPort: 44000, + dstPort: 3306, + seq: 106, + ack: 501, + ackFlag: true, + window: 4096, + payload: []byte("BBBBB"), + })) + if gapFill.StateDescript() != StateTcpAckOk { + t.Fatalf("gap-fill state = %d, want %d", gapFill.StateDescript(), StateTcpAckOk) + } +} + +func TestObserveTCPRSTClearsTrackedFlows(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700001000, 0) + + inputs := []gopacket.Packet{ + mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40001, + dstPort: 3306, + seq: 100, + syn: true, + window: 4096, + }), + mustBuildTCPPacket(t, base.Add(time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.0.2", + dstIP: "10.0.0.1", + srcPort: 3306, + dstPort: 40001, + seq: 500, + ack: 101, + syn: true, + ackFlag: true, + window: 4096, + }), + mustBuildTCPPacket(t, base.Add(2*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.0.1", + dstIP: "10.0.0.2", + srcPort: 40001, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + window: 4096, + }), + } + + for _, packet := range inputs { + _ = mustObservePacket(t, analyzer, packet) + } + + info := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(3*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.0.2", + dstIP: "10.0.0.1", + srcPort: 3306, + dstPort: 40001, + seq: 501, + ack: 101, + ackFlag: true, + rst: true, + window: 4096, + })) + if info.StateDescript() != StateTcpRst { + t.Fatalf("rst state = %d, want %d", info.StateDescript(), StateTcpRst) + } + if got := analyzer.Tracker().ActiveFlowCount(); got != 0 { + t.Fatalf("active flow count = %d, want 0", got) + } +} + +func TestObserveTCPFourWayCloseSequence(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700004000, 0) + + _, _ = mustEstablishTCPConnection( + t, + analyzer, + base, + "10.0.1.1", + 41000, + "10.0.1.2", + 3306, + 100, + 500, + ) + + info := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(3*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.1.1", + dstIP: "10.0.1.2", + srcPort: 41000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + fin: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect1 { + t.Fatalf("client fin state = %d, want %d", info.StateDescript(), StateTcpDisconnect1) + } + + info = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(4*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.1.2", + dstIP: "10.0.1.1", + srcPort: 3306, + dstPort: 41000, + seq: 501, + ack: 102, + ackFlag: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect2 { + t.Fatalf("server ack state = %d, want %d", info.StateDescript(), StateTcpDisconnect2) + } + + info = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(5*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.1.2", + dstIP: "10.0.1.1", + srcPort: 3306, + dstPort: 41000, + seq: 501, + ack: 102, + ackFlag: true, + fin: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect3 { + t.Fatalf("server fin state = %d, want %d", info.StateDescript(), StateTcpDisconnect3) + } + + info = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(6*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.1.1", + dstIP: "10.0.1.2", + srcPort: 41000, + dstPort: 3306, + seq: 102, + ack: 502, + ackFlag: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect4 { + t.Fatalf("final ack state = %d, want %d", info.StateDescript(), StateTcpDisconnect4) + } + if got := analyzer.Tracker().ActiveFlowCount(); got != 0 { + t.Fatalf("active flow count after close = %d, want 0", got) + } +} + +func TestObserveTCPDisconnect23CombinedFinAck(t *testing.T) { + analyzer := newTestAnalyzer() + defer analyzer.Stop() + base := time.Unix(1700005000, 0) + + _, _ = mustEstablishTCPConnection( + t, + analyzer, + base, + "10.0.2.1", + 42000, + "10.0.2.2", + 3306, + 100, + 500, + ) + + info := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(3*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.2.1", + dstIP: "10.0.2.2", + srcPort: 42000, + dstPort: 3306, + seq: 101, + ack: 501, + ackFlag: true, + fin: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect1 { + t.Fatalf("client fin state = %d, want %d", info.StateDescript(), StateTcpDisconnect1) + } + + info = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(4*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.2.2", + dstIP: "10.0.2.1", + srcPort: 3306, + dstPort: 42000, + seq: 600, + ack: 102, + ackFlag: true, + fin: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect23 { + t.Fatalf("combined fin-ack state = %d, want %d", info.StateDescript(), StateTcpDisconnect23) + } + + info = mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(5*time.Millisecond), tcpPacketSpec{ + srcIP: "10.0.2.1", + dstIP: "10.0.2.2", + srcPort: 42000, + dstPort: 3306, + seq: 102, + ack: 601, + ackFlag: true, + window: 4096, + })) + if info.StateDescript() != StateTcpDisconnect4 { + t.Fatalf("final ack state = %d, want %d", info.StateDescript(), StateTcpDisconnect4) + } + if got := analyzer.Tracker().ActiveFlowCount(); got != 0 { + t.Fatalf("active flow count after combined close = %d, want 0", got) + } +} diff --git a/test_helpers_test.go b/test_helpers_test.go new file mode 100644 index 0000000..74ec709 --- /dev/null +++ b/test_helpers_test.go @@ -0,0 +1,354 @@ +package bcap + +import ( + "net" + "testing" + "time" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +type observedPacket struct { + Observation + Key string + ReverseKey string +} + +func wrapObservedPacket(obs Observation) observedPacket { + return observedPacket{ + Observation: obs, + Key: obs.Flow.Forward.StableString(), + ReverseKey: obs.Flow.Reverse.StableString(), + } +} + +func (p observedPacket) StateDescript() uint8 { + return legacyStateFromObservation(p.Observation) +} + +func (p observedPacket) TcpPayloads() int { + if tcp := p.Packet.Transport.TCP; tcp != nil { + return tcp.Payload + } + return p.Packet.Transport.Payload +} + +func (p observedPacket) TcpSeq() uint32 { + if tcp := p.Packet.Transport.TCP; tcp != nil { + return tcp.Seq + } + return 0 +} + +func (p observedPacket) TcpAck() uint32 { + if tcp := p.Packet.Transport.TCP; tcp != nil { + return tcp.Ack + } + return 0 +} + +func (p observedPacket) TcpWindow() uint16 { + if tcp := p.Packet.Transport.TCP; tcp != nil { + return tcp.Window + } + return 0 +} + +func legacyStateFromObservation(obs Observation) uint8 { + switch obs.Packet.Transport.Kind { + case ProtocolTCP: + if obs.Hints.TCP != nil { + return obs.Hints.TCP.LegacyState + } + case ProtocolUDP: + return StateUdp + case ProtocolICMPv4: + return StateIcmp + case ProtocolICMPv6: + return StateIcmpv6 + } + return StateUnknown +} + +type tcpPacketSpec struct { + srcIP string + dstIP string + srcPort uint16 + dstPort uint16 + seq uint32 + ack uint32 + syn bool + ackFlag bool + fin bool + rst bool + ece bool + cwr bool + window uint16 + payload []byte +} + +type udpPacketSpec struct { + srcIP string + dstIP string + srcPort uint16 + dstPort uint16 + payload []byte +} + +type icmpPacketSpec struct { + srcIP string + dstIP string + typ uint8 + code uint8 + id uint16 + seq uint16 + payload []byte +} + +type arpPacketSpec struct { + srcMAC string + dstMAC string + senderMAC string + targetMAC string + senderIP string + targetIP string + operation uint16 +} + +func newTestAnalyzer() *Analyzer { + cfg := DefaultConfig() + cfg.CleanupInterval = 0 + return NewAnalyzerWithConfig(cfg) +} + +func mustObservePacket(t *testing.T, analyzer *Analyzer, packet gopacket.Packet) observedPacket { + t.Helper() + + obs, err := analyzer.ObservePacket(packet) + if err != nil { + t.Fatalf("observe packet: %v", err) + } + return wrapObservedPacket(obs) +} + +func mustEstablishTCPConnection( + t *testing.T, + analyzer *Analyzer, + base time.Time, + clientIP string, + clientPort uint16, + serverIP string, + serverPort uint16, + clientSeq uint32, + serverSeq uint32, +) (string, string) { + t.Helper() + + mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base, tcpPacketSpec{ + srcIP: clientIP, + dstIP: serverIP, + srcPort: clientPort, + dstPort: serverPort, + seq: clientSeq, + syn: true, + window: 4096, + })) + + serverInfo := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(time.Millisecond), tcpPacketSpec{ + srcIP: serverIP, + dstIP: clientIP, + srcPort: serverPort, + dstPort: clientPort, + seq: serverSeq, + ack: clientSeq + 1, + syn: true, + ackFlag: true, + window: 4096, + })) + + clientInfo := mustObservePacket(t, analyzer, mustBuildTCPPacket(t, base.Add(2*time.Millisecond), tcpPacketSpec{ + srcIP: clientIP, + dstIP: serverIP, + srcPort: clientPort, + dstPort: serverPort, + seq: clientSeq + 1, + ack: serverSeq + 1, + ackFlag: true, + window: 4096, + })) + + return clientInfo.Key, serverInfo.Key +} + +func mustBuildTCPPacket(t *testing.T, ts time.Time, spec tcpPacketSpec) gopacket.Packet { + t.Helper() + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.ParseIP(spec.srcIP).To4(), + DstIP: net.ParseIP(spec.dstIP).To4(), + Protocol: layers.IPProtocolTCP, + } + tcp := &layers.TCP{ + SrcPort: layers.TCPPort(spec.srcPort), + DstPort: layers.TCPPort(spec.dstPort), + Seq: spec.seq, + Ack: spec.ack, + SYN: spec.syn, + ACK: spec.ackFlag, + FIN: spec.fin, + RST: spec.rst, + ECE: spec.ece, + CWR: spec.cwr, + Window: spec.window, + } + tcp.SetNetworkLayerForChecksum(ip) + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, ip, tcp, gopacket.Payload(spec.payload)); err != nil { + t.Fatalf("serialize tcp packet: %v", err) + } + + packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeIPv4, gopacket.Default) + packet.Metadata().Timestamp = ts + return packet +} + +func mustBuildUDPPacket(t *testing.T, ts time.Time, spec udpPacketSpec) gopacket.Packet { + t.Helper() + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.ParseIP(spec.srcIP).To4(), + DstIP: net.ParseIP(spec.dstIP).To4(), + Protocol: layers.IPProtocolUDP, + } + udp := &layers.UDP{ + SrcPort: layers.UDPPort(spec.srcPort), + DstPort: layers.UDPPort(spec.dstPort), + } + udp.SetNetworkLayerForChecksum(ip) + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, ip, udp, gopacket.Payload(spec.payload)); err != nil { + t.Fatalf("serialize udp packet: %v", err) + } + + packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeIPv4, gopacket.Default) + packet.Metadata().Timestamp = ts + return packet +} + +func mustBuildICMPPacket(t *testing.T, ts time.Time, spec icmpPacketSpec) gopacket.Packet { + t.Helper() + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.ParseIP(spec.srcIP).To4(), + DstIP: net.ParseIP(spec.dstIP).To4(), + Protocol: layers.IPProtocolICMPv4, + } + icmp := &layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(spec.typ, spec.code), + Id: spec.id, + Seq: spec.seq, + } + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, ip, icmp, gopacket.Payload(spec.payload)); err != nil { + t.Fatalf("serialize icmp packet: %v", err) + } + + packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeIPv4, gopacket.Default) + packet.Metadata().Timestamp = ts + return packet +} + +func mustBuildIPv4OnlyPacket(t *testing.T, ts time.Time, srcIP, dstIP string) gopacket.Packet { + t.Helper() + + ip := &layers.IPv4{ + Version: 4, + IHL: 5, + TTL: 64, + SrcIP: net.ParseIP(srcIP).To4(), + DstIP: net.ParseIP(dstIP).To4(), + Protocol: layers.IPProtocolNoNextHeader, + } + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, ip); err != nil { + t.Fatalf("serialize ipv4-only packet: %v", err) + } + + packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeIPv4, gopacket.Default) + packet.Metadata().Timestamp = ts + return packet +} + +func mustBuildARPPacket(t *testing.T, ts time.Time, spec arpPacketSpec) gopacket.Packet { + t.Helper() + + eth := &layers.Ethernet{ + SrcMAC: mustMAC(t, spec.srcMAC), + DstMAC: mustMAC(t, spec.dstMAC), + EthernetType: layers.EthernetTypeARP, + } + arp := &layers.ARP{ + AddrType: layers.LinkTypeEthernet, + Protocol: layers.EthernetTypeIPv4, + HwAddressSize: 6, + ProtAddressSize: 4, + Operation: spec.operation, + SourceHwAddress: []byte(mustMAC(t, spec.senderMAC)), + SourceProtAddress: []byte(net.ParseIP(spec.senderIP).To4()), + DstHwAddress: []byte(mustMAC(t, spec.targetMAC)), + DstProtAddress: []byte(net.ParseIP(spec.targetIP).To4()), + } + + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + if err := gopacket.SerializeLayers(buf, opts, eth, arp); err != nil { + t.Fatalf("serialize arp packet: %v", err) + } + + packet := gopacket.NewPacket(buf.Bytes(), layers.LayerTypeEthernet, gopacket.Default) + packet.Metadata().Timestamp = ts + return packet +} + +func mustMAC(t *testing.T, raw string) net.HardwareAddr { + t.Helper() + + hw, err := net.ParseMAC(raw) + if err != nil { + t.Fatalf("parse mac %q: %v", raw, err) + } + return hw +} diff --git a/tracker.go b/tracker.go new file mode 100644 index 0000000..90df85b --- /dev/null +++ b/tracker.go @@ -0,0 +1,348 @@ +package bcap + +import ( + "sync" + "time" +) + +type Tracker struct { + config *PacketsConfig + + mu sync.Mutex + tcpStates map[string]trackerTCPState + cleanupTicker *time.Ticker + stopCleanup chan struct{} + cleanupOnce sync.Once + stopOnce sync.Once +} + +func NewTracker() *Tracker { + return NewTrackerWithConfig(nil) +} + +func NewTrackerWithConfig(config *PacketsConfig) *Tracker { + if config == nil { + config = DefaultConfig() + } + tracker := &Tracker{ + config: config, + tcpStates: make(map[string]trackerTCPState), + stopCleanup: make(chan struct{}), + } + if config.CleanupInterval > 0 { + tracker.startAutoCleanup() + } + return tracker +} + +func (t *Tracker) Stop() { + t.stopOnce.Do(func() { + close(t.stopCleanup) + }) +} + +func (t *Tracker) startAutoCleanup() { + t.cleanupOnce.Do(func() { + t.cleanupTicker = time.NewTicker(t.config.CleanupInterval) + go func() { + for { + select { + case <-t.cleanupTicker.C: + t.CleanupExpiredFlows() + case <-t.stopCleanup: + t.cleanupTicker.Stop() + return + } + } + }() + }) +} + +func (t *Tracker) CleanupExpiredFlows() int { + timeout := t.config.ConnectionTimeout + if timeout <= 0 { + return 0 + } + now := time.Now() + removed := 0 + + t.mu.Lock() + defer t.mu.Unlock() + + for key, state := range t.tcpStates { + if state.lastSeen.IsZero() { + continue + } + if now.Sub(state.lastSeen) <= timeout { + continue + } + delete(t.tcpStates, key) + removed++ + } + return removed +} + +func (t *Tracker) ActiveFlowCount() int { + t.mu.Lock() + defer t.mu.Unlock() + return len(t.tcpStates) +} + +func (t *Tracker) Observe(packet Packet) (Observation, error) { + obs := newObservation(packet) + + switch packet.Transport.Kind { + case ProtocolTCP: + hints, err := t.observeTCP(packet, obs.Flow) + if err != nil { + return Observation{}, err + } + obs.Hints = hints + return obs, nil + case ProtocolUDP: + obs.Hints = newUDPHints(packet) + return obs, nil + case ProtocolICMPv4, ProtocolICMPv6: + obs.Hints = newICMPHints(packet) + return obs, nil + case ProtocolARP: + obs.Hints = newARPHints(packet) + return obs, nil + default: + obs.Hints = HintSet{ + Summary: SummaryHint{Code: string(TagTransportUnknown)}, + Tags: []Tag{TagTransportUnknown}, + } + return obs, nil + } +} + +func newObservation(packet Packet) Observation { + return Observation{ + Packet: packet, + Flow: newFlowRef(packet), + } +} + +func newFlowRef(packet Packet) FlowRef { + forward := FlowKey{ + Family: packet.Network.Family, + Protocol: packet.Transport.Kind, + Src: Endpoint{ + IP: packet.Network.SrcIP, + Port: packetSourcePort(packet), + }, + Dst: Endpoint{ + IP: packet.Network.DstIP, + Port: packetDestinationPort(packet), + }, + } + reverse := FlowKey{ + Family: packet.Network.Family, + Protocol: packet.Transport.Kind, + Src: forward.Dst, + Dst: forward.Src, + } + return FlowRef{ + Forward: forward, + Reverse: reverse, + Stable: stableFlowKey(forward), + } +} + +func stableFlowKey(key FlowKey) string { + return buildKey(string(key.Protocol), key.Src.IP, key.Src.Port, key.Dst.IP, key.Dst.Port) +} + +func packetSourcePort(packet Packet) string { + switch packet.Transport.Kind { + case ProtocolTCP: + if packet.Transport.TCP != nil { + return packet.Transport.TCP.SrcPort + } + case ProtocolUDP: + if packet.Transport.UDP != nil { + return packet.Transport.UDP.SrcPort + } + } + return "" +} + +func packetDestinationPort(packet Packet) string { + switch packet.Transport.Kind { + case ProtocolTCP: + if packet.Transport.TCP != nil { + return packet.Transport.TCP.DstPort + } + case ProtocolUDP: + if packet.Transport.UDP != nil { + return packet.Transport.UDP.DstPort + } + } + return "" +} + +func primaryTag(tags []Tag, fallback Tag) Tag { + if len(tags) == 0 { + return fallback + } + return tags[0] +} + +type tcpHintOptions struct { + keepaliveResponse bool +} + +func newTCPHints(packet Packet, legacyState uint8, opts tcpHintOptions) HintSet { + tags, phase, event := legacyTCPTags(legacyState, opts) + hints := HintSet{ + Summary: SummaryHint{Code: string(primaryTag(tags, TagTCPPacket))}, + Tags: tags, + } + + if packet.Transport.TCP != nil { + hints.TCP = &TCPHint{ + Phase: phase, + Event: event, + LegacyState: legacyState, + Seq: packet.Transport.TCP.Seq, + Ack: packet.Transport.TCP.Ack, + Window: packet.Transport.TCP.Window, + Payload: packet.Transport.TCP.Payload, + Retransmission: legacyState == StateTcpRetransmit, + Keepalive: legacyState == StateTcpKeepalive, + KeepaliveResponse: opts.keepaliveResponse, + RST: legacyState == StateTcpRst, + ECE: legacyState == StateTcpEce, + CWR: legacyState == StateTcpCwr, + } + } + return hints +} + +func legacyTCPTags(state uint8, opts tcpHintOptions) ([]Tag, TCPPhase, TCPEvent) { + switch state { + case StateTcpConnect1: + return []Tag{TagTCPHandshakeSYN}, TCPPhaseHandshake, TCPEventSYN + case StateTcpConnect2: + return []Tag{TagTCPHandshakeSYNACK}, TCPPhaseHandshake, TCPEventSYNACK + case StateTcpConnect3: + return []Tag{TagTCPHandshakeACK}, TCPPhaseHandshake, TCPEventHandshakeACK + case StateTcpDisconnect1: + return []Tag{TagTCPTeardownFIN}, TCPPhaseTeardown, TCPEventFIN + case StateTcpDisconnect2, StateTcpDisconnect4: + return []Tag{TagTCPTeardownACK}, TCPPhaseTeardown, TCPEventTeardownACK + case StateTcpDisconnect23: + return []Tag{TagTCPTeardownFINACK}, TCPPhaseTeardown, TCPEventFINACK + case StateTcpDisconnect3: + return []Tag{TagTCPTeardownFIN}, TCPPhaseTeardown, TCPEventFIN + case StateTcpAckOk: + return []Tag{TagTCPPacket}, TCPPhaseEstablished, TCPEventACK + case StateTcpRetransmit: + return []Tag{TagTCPRetransmit, TagTCPPacket}, TCPPhaseEstablished, TCPEventRetransmission + case StateTcpKeepalive: + if opts.keepaliveResponse { + return []Tag{TagTCPKeepaliveResp, TagTCPKeepalive, TagTCPPacket}, TCPPhaseEstablished, TCPEventKeepaliveResp + } + return []Tag{TagTCPKeepalive, TagTCPPacket}, TCPPhaseEstablished, TCPEventKeepalive + case StateTcpRst: + return []Tag{TagTCPRst, TagTCPPacket}, TCPPhaseSpecial, TCPEventRST + case StateTcpEce: + return []Tag{TagTCPEce, TagTCPPacket}, TCPPhaseSpecial, TCPEventECE + case StateTcpCwr: + return []Tag{TagTCPCwr, TagTCPPacket}, TCPPhaseSpecial, TCPEventCWR + default: + return []Tag{TagTCPPacket}, TCPPhaseUnknown, TCPEventUnknown + } +} + +func newUDPHints(packet Packet) HintSet { + return HintSet{ + Summary: SummaryHint{Code: string(TagUDPPacket)}, + Tags: []Tag{TagUDPPacket}, + UDP: &UDPHint{ + Payload: packet.Transport.Payload, + }, + } +} + +func newICMPHints(packet Packet) HintSet { + hints := HintSet{ + Summary: SummaryHint{Code: string(TagICMPPacket)}, + Tags: []Tag{TagICMPPacket}, + } + if packet.Transport.ICMP == nil { + return hints + } + + item := &ICMPHint{ + Version: packet.Transport.ICMP.Version, + Type: packet.Transport.ICMP.Type, + Code: packet.Transport.ICMP.Code, + } + switch packet.Transport.Kind { + case ProtocolICMPv4: + switch packet.Transport.ICMP.Type { + case 8: + item.IsEcho = true + hints.Summary.Code = string(TagICMPEchoRequest) + hints.Tags = append(hints.Tags, TagICMPEchoRequest) + case 0: + item.IsEchoReply = true + hints.Summary.Code = string(TagICMPEchoReply) + hints.Tags = append(hints.Tags, TagICMPEchoReply) + case 3: + item.IsUnreachable = true + hints.Summary.Code = string(TagICMPUnreachable) + hints.Tags = append(hints.Tags, TagICMPUnreachable) + case 11: + item.IsTimeExceeded = true + hints.Summary.Code = string(TagICMPTimeExceeded) + hints.Tags = append(hints.Tags, TagICMPTimeExceeded) + } + case ProtocolICMPv6: + switch packet.Transport.ICMP.Type { + case 128: + item.IsEcho = true + hints.Summary.Code = string(TagICMPEchoRequest) + hints.Tags = append(hints.Tags, TagICMPEchoRequest) + case 129: + item.IsEchoReply = true + hints.Summary.Code = string(TagICMPEchoReply) + hints.Tags = append(hints.Tags, TagICMPEchoReply) + case 1: + item.IsUnreachable = true + hints.Summary.Code = string(TagICMPUnreachable) + hints.Tags = append(hints.Tags, TagICMPUnreachable) + case 3: + item.IsTimeExceeded = true + hints.Summary.Code = string(TagICMPTimeExceeded) + hints.Tags = append(hints.Tags, TagICMPTimeExceeded) + } + } + hints.ICMP = item + return hints +} + +func newARPHints(packet Packet) HintSet { + hints := HintSet{ + Summary: SummaryHint{Code: string(TagTransportUnknown)}, + Tags: []Tag{TagTransportUnknown}, + } + if packet.Network.ARP == nil { + return hints + } + item := &ARPHint{Operation: packet.Network.ARP.Operation} + switch packet.Network.ARP.Operation { + case 1: + item.Request = true + hints.Summary.Code = string(TagARPRequest) + hints.Tags = []Tag{TagARPRequest} + case 2: + item.Reply = true + hints.Summary.Code = string(TagARPReply) + hints.Tags = []Tag{TagARPReply} + } + hints.ARP = item + return hints +} diff --git a/tracker_tcp.go b/tracker_tcp.go new file mode 100644 index 0000000..d173948 --- /dev/null +++ b/tracker_tcp.go @@ -0,0 +1,278 @@ +package bcap + +import "time" + +type trackerTCPState struct { + firstSeen time.Time + lastSeen time.Time + packetCount uint64 + byteCount uint64 + + seq uint32 + ack uint32 + window uint16 + payload int + finState bool + synState bool + isFirst bool + state uint8 + segments [trackedTCPSegments]tcpSegmentRange + segmentCount int + segmentNext int +} + +func (t *Tracker) observeTCP(packet Packet, flow FlowRef) (HintSet, error) { + tcp := packet.Transport.TCP + if tcp == nil { + return HintSet{}, NewParseError(ErrTypeTransport, "TCP", "missing tcp facts", nil) + } + + forwardKey := flow.Forward.StableString() + reverseKey := flow.Reverse.StableString() + if forwardKey == "" { + forwardKey = flow.Stable + } + if reverseKey == "" { + reverseKey = stableFlowKey(flow.Reverse) + } + + t.mu.Lock() + defer t.mu.Unlock() + + lastState, exists := t.tcpStates[forwardKey] + if !exists { + lastState = trackerTCPState{ + firstSeen: packet.Meta.Timestamp, + lastSeen: packet.Meta.Timestamp, + isFirst: true, + } + } + lastReverse := t.tcpStates[reverseKey] + + payloadLen := tcp.Payload + seqEnd := tcpSeqAdvanceFacts(tcp.Seq, tcp.SYN, tcp.FIN, payloadLen) + state := StateUnknown + hintOpts := tcpHintOptions{} + connectionClosed := false + + if tcp.RST { + state = StateTcpRst + connectionClosed = true + delete(t.tcpStates, forwardKey) + delete(t.tcpStates, reverseKey) + } else if tcp.SYN && !tcp.ACK { + state = StateTcpConnect1 + } else if tcp.SYN && tcp.ACK { + state = StateTcpConnect2 + } else if tcp.ACK && !tcp.FIN { + state, hintOpts, connectionClosed = classifyTrackerAckNoFIN(packet, tcp, seqEnd, lastState, lastReverse) + if connectionClosed { + delete(t.tcpStates, forwardKey) + delete(t.tcpStates, reverseKey) + } + } else if tcp.ACK && tcp.FIN { + state = classifyTrackerAckFIN(tcp, lastState, lastReverse) + } + + if !connectionClosed { + next := trackerTCPState{ + firstSeen: firstSeenOrNow(lastState.firstSeen, packet.Meta.Timestamp), + lastSeen: packet.Meta.Timestamp, + packetCount: lastState.packetCount + 1, + byteCount: lastState.byteCount + packetWireLength(packet), + seq: tcp.Seq, + ack: tcp.Ack, + window: tcp.Window, + payload: payloadLen, + finState: tcp.FIN, + synState: tcp.SYN, + isFirst: false, + state: state, + segments: lastState.segments, + segmentCount: lastState.segmentCount, + segmentNext: lastState.segmentNext, + } + next.rememberSegment(tcp.Seq, seqEnd) + t.tcpStates[forwardKey] = next + } + + return newTCPHints(packet, state, hintOpts), nil +} + +func (f FlowKey) StableString() string { + return stableFlowKey(f) +} + +func firstSeenOrNow(firstSeen, fallback time.Time) time.Time { + if firstSeen.IsZero() { + return fallback + } + return firstSeen +} + +func packetWireLength(packet Packet) uint64 { + switch { + case packet.Meta.Length > 0: + return uint64(packet.Meta.Length) + case packet.Meta.CaptureLength > 0: + return uint64(packet.Meta.CaptureLength) + case packet.Raw.Packet != nil: + return uint64(len(packet.Raw.Packet.Data())) + default: + return 0 + } +} + +func tcpSeqAdvanceFacts(seq uint32, syn, fin bool, payloadLen int) uint32 { + span := payloadLen + if syn { + span++ + } + if fin { + span++ + } + return tcpSeqAdd(seq, uint32(span)) +} + +func classifyTrackerAckNoFIN( + packet Packet, + tcp *TCPFacts, + seqEnd uint32, + lastState, lastReverse trackerTCPState, +) (uint8, tcpHintOptions, bool) { + if isTrackerHandshakeAck(lastState, lastReverse, tcp.Ack) { + return StateTcpConnect3, tcpHintOptions{}, false + } + if tcp.CWR { + return StateTcpCwr, tcpHintOptions{}, false + } + if tcp.ECE { + return StateTcpEce, tcpHintOptions{}, false + } + if state, opts, ok := classifyTrackerTCPKeepalive(packet, tcp, lastState, lastReverse); ok { + return state, opts, false + } + if lastState.hasSeenSegment(tcp.Seq, seqEnd) { + return StateTcpRetransmit, tcpHintOptions{}, false + } + if lastReverse.finState && lastState.finState { + return StateTcpDisconnect4, tcpHintOptions{}, true + } + if lastReverse.finState && tcpSeqAdd(lastReverse.seq, 1) == tcp.Ack { + return StateTcpDisconnect2, tcpHintOptions{}, false + } + return StateTcpAckOk, tcpHintOptions{}, false +} + +func classifyTrackerAckFIN(tcp *TCPFacts, lastState, lastReverse trackerTCPState) uint8 { + if !lastReverse.finState { + return StateTcpDisconnect1 + } + if lastReverse.finState && tcpSeqAdd(lastReverse.seq, 1) == tcp.Ack && + lastState.ack == tcp.Ack && lastState.seq == tcp.Seq { + return StateTcpDisconnect3 + } + return StateTcpDisconnect23 +} + +func isTrackerHandshakeAck(lastState, lastReverse trackerTCPState, ack uint32) bool { + if lastReverse.state != StateTcpConnect2 { + return false + } + if tcpSeqAdd(lastReverse.seq, 1) != ack { + return false + } + return lastState.state == StateTcpConnect1 && lastState.synState +} + +func classifyTrackerTCPKeepalive(packet Packet, tcp *TCPFacts, lastState, lastReverse trackerTCPState) (uint8, tcpHintOptions, bool) { + if isTrackerRepeatedKeepaliveProbe(packet, tcp, lastState, lastReverse) { + return StateTcpKeepalive, tcpHintOptions{}, true + } + if lastReverse.matchesKeepaliveResponse(packet, tcp) { + return StateTcpKeepalive, tcpHintOptions{keepaliveResponse: true}, true + } + if tcp.Seq == tcpSeqPrev(lastReverse.ack) && + tcp.Seq == tcpSeqPrev(tcpSeqAdd(lastState.seq, uint32(lastState.payload))) { + return StateTcpKeepalive, tcpHintOptions{}, true + } + return StateUnknown, tcpHintOptions{}, false +} + +func isTrackerRepeatedKeepaliveProbe(packet Packet, tcp *TCPFacts, lastState, lastReverse trackerTCPState) bool { + if lastState.isFirst { + return false + } + if tcp == nil || !tcp.ACK || tcp.SYN || tcp.FIN || tcp.RST || tcp.ECE || tcp.CWR { + return false + } + if tcp.Payload != 0 { + return false + } + if lastState.synState || lastState.finState || lastState.payload != 0 { + return false + } + switch lastState.state { + case StateTcpAckOk, StateTcpKeepalive, StateTcpConnect3, StateTcpDisconnect2: + default: + return false + } + if tcp.Seq != lastState.seq || tcp.Ack != lastState.ack || tcp.Window != lastState.window { + return false + } + if packet.Meta.Timestamp.Sub(lastState.lastSeen) < repeatedKeepaliveMinInterval { + return false + } + if !lastReverse.lastSeen.IsZero() && lastReverse.lastSeen.After(lastState.lastSeen) { + return false + } + return true +} + +func (s trackerTCPState) matchesKeepaliveResponse(packet Packet, tcp *TCPFacts) bool { + if tcp == nil || s.state != StateTcpKeepalive { + return false + } + if !tcp.ACK || tcp.SYN || tcp.FIN || tcp.RST || tcp.ECE || tcp.CWR { + return false + } + if tcp.Payload != 0 { + return false + } + if packet.Meta.Timestamp.Sub(s.lastSeen) > keepaliveResponseMaxDelay { + return false + } + return tcp.Seq == s.ack && tcp.Ack == tcpSeqAdd(s.seq, 1) +} + +func (s trackerTCPState) hasSeenSegment(seq, end uint32) bool { + if !tcpSeqLess(seq, end) { + return false + } + for i := 0; i < s.segmentCount; i++ { + seg := s.segments[i] + if !tcpSeqLess(seg.seq, seg.end) { + continue + } + if !tcpSeqLess(seq, seg.seq) && tcpSeqLEQ(end, seg.end) { + return true + } + } + return false +} + +func (s *trackerTCPState) rememberSegment(seq, end uint32) { + if !tcpSeqLess(seq, end) { + return + } + if s.hasSeenSegment(seq, end) { + return + } + if s.segmentCount < trackedTCPSegments { + s.segments[s.segmentCount] = tcpSegmentRange{seq: seq, end: end} + s.segmentCount++ + return + } + s.segments[s.segmentNext] = tcpSegmentRange{seq: seq, end: end} + s.segmentNext = (s.segmentNext + 1) % trackedTCPSegments +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..7ef2380 --- /dev/null +++ b/types.go @@ -0,0 +1,102 @@ +package bcap + +import ( + "fmt" + "time" +) + +type ParseErrorType int + +const ( + ErrTypeNone ParseErrorType = iota + ErrTypeLinkLayer + ErrTypeNetwork + ErrTypeTransport + ErrTypeUnsupported +) + +type ParseError struct { + Type ParseErrorType + Layer string + Message string + Err error +} + +func (e *ParseError) Error() string { + if e.Err != nil { + return fmt.Sprintf("[%s] %s: %v", e.Layer, e.Message, e.Err) + } + return fmt.Sprintf("[%s] %s", e.Layer, e.Message) +} + +func NewParseError(errType ParseErrorType, layer, message string, err error) *ParseError { + return &ParseError{ + Type: errType, + Layer: layer, + Message: message, + Err: err, + } +} + +const ( + StateUnknown uint8 = 0 + StateTcpConnect1 uint8 = 1 + StateTcpConnect2 uint8 = 2 + StateTcpConnect3 uint8 = 3 + StateTcpDisconnect1 uint8 = 4 + StateTcpDisconnect2 uint8 = 5 + StateTcpDisconnect23 uint8 = 6 + StateTcpDisconnect3 uint8 = 7 + StateTcpDisconnect4 uint8 = 8 + StateTcpAckOk uint8 = 9 + StateTcpRetransmit uint8 = 10 + StateTcpEce uint8 = 11 + StateTcpCwr uint8 = 12 + StateTcpRst uint8 = 13 + StateTcpKeepalive uint8 = 14 + StateUdp uint8 = 20 + StateIcmp uint8 = 30 + StateIcmpv6 uint8 = 31 +) + +const ( + DefaultConnectionTimeout = 5 * time.Minute + DefaultCleanupInterval = 1 * time.Minute +) + +type PacketsConfig struct { + ConnectionTimeout time.Duration + CleanupInterval time.Duration +} + +func DefaultConfig() *PacketsConfig { + return &PacketsConfig{ + ConnectionTimeout: DefaultConnectionTimeout, + CleanupInterval: DefaultCleanupInterval, + } +} + +type ErrorStats struct { + LinkLayerErrors uint64 + NetworkErrors uint64 + TransportErrors uint64 + UnsupportedErrors uint64 + TotalErrors uint64 +} + +type ConnectionStats struct { + ActiveConnections int64 + TotalConnections uint64 + ClosedConnections uint64 + TimeoutConnections uint64 + TcpConnections int64 + UdpConnections int64 + IcmpConnections int64 +} + +type Stats struct { + Errors ErrorStats + Connections ConnectionStats + StartTime time.Time + LastCleanup time.Time +}