重构代码

This commit is contained in:
兔子 2026-03-24 23:39:55 +08:00
parent 11f9fc2893
commit 744ac8c2e9
Signed by: b612
GPG Key ID: 99DD2222B612B612
20 changed files with 4502 additions and 1361 deletions

201
LICENSE Normal file
View File

@ -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.

218
README.md Normal file
View File

@ -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)

73
analyzer.go Normal file
View File

@ -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
}

315
api_test.go Normal file
View File

@ -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
}

1359
bcap.go

File diff suppressed because it is too large Load Diff

50
conn_map.go Normal file
View File

@ -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()
}

227
decoder.go Normal file
View File

@ -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}
}

726
doc/api.md Normal file
View File

@ -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)

518
doc/dev.md Normal file
View File

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

34
format.go Normal file
View File

@ -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])
}

24
format_test.go Normal file
View File

@ -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)
}
}

258
model.go Normal file
View File

@ -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
}

View File

@ -57,7 +57,7 @@ func (n *NfQueue) Run() error {
} }
nfq, err := nfqueue.Open(&cfg) nfq, err := nfqueue.Open(&cfg)
if err != nil { 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 { if err := nfq.RegisterWithErrorFunc(n.ctx, func(a nfqueue.Attribute) int {
@ -65,7 +65,7 @@ func (n *NfQueue) Run() error {
}, func(e error) int { }, func(e error) int {
return 0 return 0
}); err != nil { }); err != nil {
return fmt.Errorf("failed to register handlers, err:", err) return fmt.Errorf("failed to register handlers: %w", err)
} }
<-n.ctx.Done() <-n.ctx.Done()
return nil return nil

12
state.go Normal file
View File

@ -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
}

15
tcp_seq.go Normal file
View File

@ -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))
}

747
tcp_test.go Normal file
View File

@ -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)
}
}

354
test_helpers_test.go Normal file
View File

@ -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
}

348
tracker.go Normal file
View File

@ -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
}

278
tracker_tcp.go Normal file
View File

@ -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
}

102
types.go Normal file
View File

@ -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
}