重构代码
This commit is contained in:
parent
11f9fc2893
commit
744ac8c2e9
201
LICENSE
Normal file
201
LICENSE
Normal 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
218
README.md
Normal 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
73
analyzer.go
Normal 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
315
api_test.go
Normal 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
|
||||
}
|
||||
50
conn_map.go
Normal file
50
conn_map.go
Normal 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
227
decoder.go
Normal 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
726
doc/api.md
Normal 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
518
doc/dev.md
Normal 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
34
format.go
Normal 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
24
format_test.go
Normal 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
258
model.go
Normal 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
|
||||
}
|
||||
@ -57,7 +57,7 @@ func (n *NfQueue) Run() error {
|
||||
}
|
||||
nfq, err := nfqueue.Open(&cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open nfqueue, err:", err)
|
||||
return fmt.Errorf("failed to open nfqueue: %w", err)
|
||||
}
|
||||
|
||||
if err := nfq.RegisterWithErrorFunc(n.ctx, func(a nfqueue.Attribute) int {
|
||||
@ -65,7 +65,7 @@ func (n *NfQueue) Run() error {
|
||||
}, func(e error) int {
|
||||
return 0
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to register handlers, err:", err)
|
||||
return fmt.Errorf("failed to register handlers: %w", err)
|
||||
}
|
||||
<-n.ctx.Done()
|
||||
return nil
|
||||
|
||||
12
state.go
Normal file
12
state.go
Normal 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
15
tcp_seq.go
Normal 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
747
tcp_test.go
Normal 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
354
test_helpers_test.go
Normal 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
348
tracker.go
Normal 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
278
tracker_tcp.go
Normal 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
102
types.go
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user