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