ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ecapture Protobuf 协议详解:从 ecaptureq.proto 到 WebSocket 事件推送的完整链路

ecapture Protobuf 协议详解:从 ecaptureq.proto 到 WebSocket 事件推送的完整链路 ecapture Protobuf 协议详解从 ecaptureq.proto 到 WebSocket 事件推送的完整链路【免费下载链接】ecaptureCapturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64.项目地址: https://gitcode.com/GitHub_Trending/ec/ecaptureeCapture 通过一套精简的 Protobuf 协议v1/ecaptureq.proto将 eBPF 捕获到的 TLS/HTTP 明文事件、进程日志与心跳统一封装经 WebSocket 推送给收集器与调试工具。本文以 protobuf/PROTOCOLS-zh_Hans.md 为主线完整覆盖LogType枚举、Event/Heartbeat/LogEntry三个消息结构的字段定义并结合 服务端发送实现 与 官方示例客户端 逐层剖析序列化与分发链路读完后可独立编写接收端并正确解析任意一条推送消息。命名空间与文件布局协议的核心定义集中在 protobuf/proto/v1/ecaptureq.proto由protoc生成对应 Go 代码到 protobuf/gen/v1/ecaptureq.pb.go。关键布局信息如下项目取值位置Proto 包名eventecaptureq.proto 第 3 行Go 包路径./pboption go_package ./pb;同上第 5 行源文件protobuf/proto/v1/ecaptureq.proto仓库根目录下生成代码protobuf/gen/v1/ecaptureq.pb.go由 protoc-gen-go 生成ecaptureq.proto的完整内容仅 40 余行结构非常紧凑syntax proto3; package event; option go_package ./pb; enum LogType { LOG_TYPE_HEARTBEAT 0; LOG_TYPE_PROCESS_LOG 1; LOG_TYPE_EVENT 2; } // Base 消息定义 message Event { int64 timestamp 1; string uuid 2; string src_ip 3; uint32 src_port 4; string dst_ip 5; uint32 dst_port 6; int64 pid 7; string pname 8; uint32 type 9; uint32 length 10; bytes payload 11; } message Heartbeat { int64 timestamp 1; int64 count 2; string message 3; } message LogEntry { LogType log_type 1; oneof payload { Event event_payload 2; Heartbeat heartbeat_payload 3; string run_log 4; } }注意option go_package ./pb的语义生成文件的 import 路径为github.com/gojue/ecapture/protobuf/gen/v1而 Go 包名为pb因此客户端代码中一律以pb github.com/gojue/ecapture/protobuf/gen/v1的形式引入所有类型写作pb.LogEntry、pb.Event、pb.Heartbeat。LogType 枚举区分三类推送数据LogType是接收端消息分发的第一级路由键共三个值枚举值数值语义对应 oneof 分支LOG_TYPE_HEARTBEAT0心跳信息用于健康检查和连接保活Heartbeat heartbeat_payloadLOG_TYPE_PROCESS_LOG1进程运行日志如系统事件、错误信息等string run_logLOG_TYPE_EVENT2实际捕获到的业务数据事件如 TLS/HTTP 数据Event event_payload在生成代码中该枚举映射为 Go 常量pb.LogType_LOG_TYPE_HEARTBEAT0、pb.LogType_LOG_TYPE_PROCESS_LOG1、pb.LogType_LOG_TYPE_EVENT2见 ecaptureq.pb.go。由于 proto3 枚举默认值取第一个值GetLogType()在未设置时返回LOG_TYPE_HEARTBEAT0这是 proto3 的固有行为解析时不需要特别处理但接收端仍应以switch logEntry.LogType做显式分支。Event 消息单条捕获事件的统一表示Event对应 Go 结构体pb.Event是 eCapture 捕获到的单条事件数据包/会话片段的统一表示。字段号、类型与含义完整继承自协议文档并对照源码标注字段号字段proto3 类型含义1timestampint64事件时间戳2uuidstring事件唯一标识用于关联与去重3src_ipstring源 IP 地址4src_portuint32源端口5dst_ipstring目标 IP 地址6dst_portuint32目标端口7pidint64进程 ID8pnamestring进程名称例如curl、nginx等9typeuint32事件/协议类型枚举值见下表10lengthuint32payload的有效长度字节数11payloadbytes实际载荷数据type字段取值约定值含义0Unknown1HTTP/1.x Request2HTTP/2 Request3HTTP/1.x Response4HTTP/2 Response说明在 eCaptureQ 后端内部模型中还会派生出is_binary、payload_utf8、payload_binary等字段用于区分文本/二进制展示但这些是服务端内部和 UI 使用的扩展字段不直接出现在 ProtobufEvent消息中。从源码结构看事件进入推送链路的方式是先有载荷、后填元数据server.go 的 WriteEvent 接收到的data []byte就是 HTTP 解析层产出的事件文本已由上层事件处理器序列化它被直接塞入Event.Payload并同步设置Length: uint32(len(data))随后封装为LogEntry经proto.Marshal广播func (s *Server) WriteEvent(data []byte) (n int, e error) { le : pb.LogEntry{ LogType: pb.LogType_LOG_TYPE_EVENT, Payload: pb.LogEntry_EventPayload{ EventPayload: pb.Event{ Payload: data, Length: uint32(len(data)), }, }, } encodedData, err : proto.Marshal(le) if err ! nil { return 0, err } s.hub.broadcastMessage(encodedData) return len(data), nil }这也解释了协议中length与payload并存的设计length是 wire 上载荷长度的冗余声明接收端可用它做快速校验无需二次拷贝字节流。Heartbeat 消息保活与统计Heartbeat用于连接保活和统计信息对应 Go 结构体pb.Heartbeat字段号字段类型含义1timestampint64发送心跳的时间戳2countint64心跳计数或累计事件数量等统计值3messagestring附加信息如版本号、状态描述等从源码看心跳的实际节奏client.go 的 writePump 中每个客户端连接启动time.NewTicker(15 * time.Second)连接建立时立即发送一次心跳此后每 15 秒发送一次。Heartbeat() 的具体填法值得对照协议理解三个字段func (c *Client) Heartbeat() error { hb : new(pb.Heartbeat) hb.Message fmt.Sprintf(heartbeat:%d, c.heartBeatCount) hb.Timestamp time.Now().Unix() hb.Count int64(c.heartBeatCount) le : new(pb.LogEntry) le.LogType pb.LogType_LOG_TYPE_HEARTBEAT le.Payload pb.LogEntry_HeartbeatPayload{HeartbeatPayload: hb} encodedData, err : proto.Marshal(le) ... }即Timestamp是 Unix 秒级时间戳Count是逐次自增的心跳序号Message携带形如heartbeat:n的描述文本。接收端可用Timestamp判断链路延迟、用Count检测心跳丢失。LogEntry 消息顶层封装与 oneof 设计LogEntry是每条 WebSocket 帧的实际载荷——顶层日志封装结构统一承载不同类型的业务数据对应 Go 结构体pb.LogEntryLogType log_type字段号 1日志类型取LOG_TYPE_HEARTBEAT/LOG_TYPE_PROCESS_LOG/LOG_TYPE_EVENT之一oneof payload根据log_type不同携带以下三种之一Event event_payload字段号 2当log_type LOG_TYPE_EVENT时承载事件数据Heartbeat heartbeat_payload字段号 3当log_type LOG_TYPE_HEARTBEAT时承载心跳信息string run_log字段号 4当log_type LOG_TYPE_PROCESS_LOG时承载普通运行日志字符串。oneof在 Go 侧被生成为接口isLogEntry_Payload加三个包装结构体LogEntry_EventPayload、LogEntry_HeartbeatPayload、LogEntry_RunLog见 ecaptureq.pb.go并配套了安全的类型断言 getterGetEventPayload()、GetHeartbeatPayload()、GetRunLog()L316-L341断言类型不匹配时返回零值而非 panic。推荐的接收端写法正是基于这组 getter 按LogType分发这也是 官方示例客户端 采用的模式func handleLogEntry(logEntry *pb.LogEntry) { switch logEntry.LogType { case pb.LogType_LOG_TYPE_HEARTBEAT: handleHeartbeat(logEntry) case pb.LogType_LOG_TYPE_PROCESS_LOG: handleProcessLog(logEntry) case pb.LogType_LOG_TYPE_EVENT: handleEvent(logEntry) default: log.Printf(Unknown log type: %v, logEntry.LogType) } }运行日志的发送路径同样体现了LogEntry的封装作用server.go 的 WriteLog 将任意io.Writer写出的日志字节流直接包进LogEntry_RunLog{RunLog: string(data)}标记LOG_TYPE_PROCESS_LOG后广播值得注意的是它还维护了一个容量为 128 的logbuffLogBuffLen 128新客户端接入时会通过sendLogBuff补发缓存的早期日志避免先连上再丢日志的问题。集成示例连接并解析推送消息协议文档给出的最小集成示例是编写任意语言接收端的参考骨架连接 eCapture 启用的 WebSocket 转发地址逐帧反序列化LogEntry并按LogType分发import ( pb github.com/gojue/ecapture/protobuf/gen/v1 golang.org/x/net/websocket google.golang.org/protobuf/proto ) // Connect (连接) ws, err : websocket.Dial(ws://127.0.0.1:28257/, , http://localhost/) if err ! nil { // Handle error (处理错误) } defer ws.Close() // Receive messages (接收消息) for { var msgData []byte err : websocket.Message.Receive(ws, msgData) if err ! nil { break } var logEntry pb.LogEntry err proto.Unmarshal(msgData, logEntry) if err ! nil { continue } // Process logEntry based on logEntry.LogType // 根据 logEntry.LogType 处理日志条目 }仓库中 examples/ecaptureq_client 目录提供了该协议更完整的可运行参考实现在最小示例之上补充了工程实践中常用的几类处理优雅退出通过signal.Notify捕获os.Interrupt/SIGTERM后再断开连接事件可读化handleEvent按isPrintable可打印字符占比超过 0.9 阈值决定以文本直出或 hex dump 展示payload并附 Base64 编码方便人工核对二进制载荷字段条件展示Timestamp 0才打印时间、SrcIp ! SrcPort 0才打印源地址与 proto3 零值语义对应。该示例默认连接ws://127.0.0.1:28257/可通过-server参数覆盖与 cli/cmd/root.go 中ecaptureq.NewServer(parsedURL.Host, os.Stdout)的监听地址约定一致——eCapture 在启用转发输出时会以转发目标 URL 的 host 作为 WebSocket 服务监听地址日志写入器接ecaptureQLogWriter、事件写入器接ecaptureQEventWriter见 cli/cmd/ecaptureq.go二者最终都落到上述WriteLog/WriteEvent两条协议序列化路径。重新生成 Go 代码如果修改了.proto定义需按 protobuf/README-zh_Hans.md 的指南重新生成。当前仓库中的生成文件由protoc-gen-go v1.36.6与protoc v6.32.1编译得到版本头见 ecaptureq.pb.go建议使用不低于该版本重新编译以避免兼容性问题。从仓库根目录执行protoc --proto_pathprotobuf/proto \ --go_outprotobuf/gen --go_optpathssource_relative \ --go-grpc_outprotobuf/gen --go-grpc_optpathssource_relative \ protobuf/proto/v1/ecaptureq.proto该命令从protobuf/proto读取.proto文件在protobuf/gen下按源文件相对路径生成 Go 代码同时生成 gRPC 相关代码若.proto中定义了 service——当前协议未定义 service仅含消息与枚举。生成完成后应将protobuf/gen/v1下的更新文件一并纳入版本控制。小结eCapture 的 Protobuf 协议以一个顶层封装 三类载荷的最小设计把事件、运行日志与心跳统一成单一帧格式LogEntry.log_type决定 oneof 分支接收端只需一个switch完成一级分发Event的 11 个字段时间戳、五元组、进程信息、类型、长度、载荷覆盖了明文捕获事件的全部关键上下文type字段区分 HTTP/1.x 与 HTTP/2 的请求/响应传输层是 WebSocket默认ws://127.0.0.1:28257/序列化层是 proto3服务端由 pkg/ecaptureq 的 Hub/Client 广播模型负责多客户端分发并带 15 秒心跳与 128 条早期日志补发机制。对照 protobuf/PROTOCOLS.md英文版本与本文再结合 pkg/ecaptureq/server.go、pkg/ecaptureq/client.go 与 examples/ecaptureq_client/main.go 三处源码即可在不依赖 UI 的情况下完整实现 eCapture 事件的接入、解析与展示。【免费下载链接】ecaptureCapturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64.项目地址: https://gitcode.com/GitHub_Trending/ec/ecapture创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进