Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【腾讯犀牛鸟开源课题实战】wireshark协议解析 #36

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ tRPC是基于插件化理念设计的一款支持多语言、高性能的RPC框
- [用户指南](https://github.com/trpc-group/trpc-go/tree/main/docs/README.zh_CN.md)
- [代码示例](https://github.com/trpc-group/trpc-go/tree/main/examples)

## 如何用wireshark分析tRPC协议

参考 [docs/zh/wireshark_trpc.md](docs/zh/wireshark_trpc.md)

## 如何参与贡献

非常欢迎大家给tRPC做贡献!
Expand Down
Binary file added docs/images/wireshark/pic1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/wireshark/pic9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions docs/zh/wireshark_trpc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# tRPC Wireshark解析器

## 前言

`tool/wireshark_trpc.lua` 是 tRPC 协议的 Wireshark 解析器,提供了对 tRPC 协议头、业务 pb 的解析能力。

当前使用有如下限制:

1. 暂不支持解析tRPC流式协议;
2. 暂不支持attachment;
3. 暂不支持UDP;
4. 不支持解析开启压缩后的pb数据(业务pb数据会被压缩);

## 用法

### tcpdump抓包

执行下面的tcpdump抓包

```bash
# 10001 替换为服务端端口
tcpdump -iany port 10001 -w trpc_packet.pcap
```

**注意:** 有时可能因为机器环境问题,导致tcpdump抓包被截断,比如只抓了请求包的部分,wireshark使用lua脚本解析会失败,这时可以抓包时使用 `-s xxx` 防止截断,代表调整低于xxx字节的packet不截断。

### Wireshark配置

1. 从选项卡 `About Wireshark` -> `Floders` 查看 `Personal Lua Plugins` 的目录。

<img src="../images/wireshark/pic1.png" width="600" />

2. 进入 `Personal Lua Plugins` 目录,放入 [wireshark_trpc.lua](../../tool/wireshark_trpc.lua)。

<img src="../images/wireshark/pic2.png" width="400" />

3. 设置proto扫描文件目录,并开启基于protobuf的解析。

- 此文件目录下需要放置 [trpc.proto](../../trpc/trpc.proto)以及业务pb,比如此处我们使用 tRPC-Cpp 示例的 [helloworld.proto](https://github.com/trpc-group/trpc-cpp/blob/main/examples/helloworld/helloworld.proto)。
- 需要勾选 `Load .proto files on startup.`、`Dissect Protobuf fields as Wireshark fields.`、`Show details of message, fields and enums.`、`Show all fields of bytes type as string.` 以通过protobuf解析器来解析字段。

<img src="../images/wireshark/pic3.png" width="400" />

<img src="../images/wireshark/pic4.png" width="600" />

<img src="../images/wireshark/pic5.png" width="400" />

### Wireshark加载tcpdump抓包

加载包之后,如果没有看到 `Protocol` 显示 `tRPC`,需要强制加载下lua脚本。

<img src="../images/wireshark/pic6.png" width="200" />

如果都成功,能看如下图所示tRPC协议以及业务pb都被正常解析了。

<img src="../images/wireshark/pic7.png" width="600" />

<img src="../images/wireshark/pic8.png" width="600" />

### 筛选指定请求和响应

有时会发现并发很多个请求,可能有某几个请求会调用失败,为了进一步排查,需要更多tRPC协议/业务pb字段等信息,这时候可以通过 protobuf 字段来筛选出指定请求/响应。

Wireshark 支持通过 protobuf 字段做筛选,业务可以选择使用trpc协议头的 request_id 或者业务 pb 某个字段做匹配来查看指定请求和响应的交互情况。

下面是根据trpc协议头的 `requet_id=3` 筛选指定请求和响应的示例,大家可根据自身情况指定筛选条件。

```text
protobuf.field.name == "request_id" and protobuf.field.value == 3
```

<img src="../images/wireshark/pic9.png" width="600" />

### 忽略tcp的控制帧

我们更多的会关注协议包的交互情况,而不关注tcp控制帧(tcp握手/挥手等),这时可以通过设置下面的筛选条件清除tcp控制帧的显示。

```text
protobuf
```

<img src="../images/wireshark/pic10.png" width="600" />
236 changes: 236 additions & 0 deletions tool/wireshark_trpc.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
--
--
-- Tencent is pleased to support the open source community by making tRPC available.
--
-- Copyright (C) 2024 THL A29 Limited, a Tencent company.
-- All rights reserved.
--
-- If you have downloaded a copy of the tRPC source code from Tencent,
-- please note that tRPC source code is licensed under the Apache 2.0 License,
-- A copy of the Apache 2.0 License is included in this file.
--
--
-- tRPC is licensed under the Apache 2.0 License, and includes source codes from
-- the following components:
-- 1. incubator-brpc
-- Copyright (C) 2019 The Apache Software Foundation
-- incubator-brpc is licensed under the Apache 2.0 License.
--
--

local protocol_name = "trpc"
local trpc_proto = Proto(protocol_name, "tRPC Protocol Dissector")

local field_magic = ProtoField.uint16(protocol_name .. ".magic", "Magic", base.HEX)
local field_type = ProtoField.uint8(protocol_name .. ".type", "Packet Type", base.DEC)
local field_stream = ProtoField.uint8(protocol_name .. ".stream", "Stream Type", base.DEC)
local field_total_size = ProtoField.uint32(protocol_name .. ".total_size", "Total Size", base.DEC)
local field_header_size= ProtoField.uint16(protocol_name .. ".header_size", "Header Size", base.DEC)
local field_unique_id = ProtoField.uint32(protocol_name .. ".unique_id", "Unique ID", base.DEC)
local field_version = ProtoField.uint8(protocol_name .. ".version", "Version", base.DEC)
local field_reserved = ProtoField.uint8(protocol_name .. ".reserved", "Reserved", base.DEC)
trpc_proto.fields = {field_magic, field_type, field_stream, field_total_size, field_header_size, field_unique_id, field_version, field_reserved}

local MAGIC_CODE_TRPC = "0930"
local PROTO_HEADER_LENGTH = 16

local tcp_src_port = Field.new("tcp.srcport")
local tcp_dst_port = Field.new("tcp.dstport")
local tcp_stream = Field.new("tcp.stream")

local proto_f_protobuf_field_name = Field.new("protobuf.field.name")
local proto_f_protobuf_field_value = Field.new("protobuf.field.value")

local data_dissector = Dissector.get("data")
local protobuf_dissector = Dissector.get("protobuf")

----------------------------------------
-- declare functions
local check_length = function() end
local dissect_proto = function() end

----------------------------------------
-- main dissector
function trpc_proto.dissector(tvbuf, pktinfo, root)
local pktlen = tvbuf:len()

local bytes_consumed = 0

while bytes_consumed < pktlen do
local result = dissect_proto(tvbuf, pktinfo, root, bytes_consumed)

if result > 0 then
bytes_consumed = bytes_consumed + result
elseif result == 0 then
-- hit an error
return 0
else
pktinfo.desegment_offset = bytes_consumed
-- require more bytes
pktinfo.desegment_len = -result

return pktlen
end
end

return bytes_consumed
end

--------------------------------------------------------------------------------
-- heuristic
-- tcp_stream_id <-> {client_port, server_port, {request_id<->method_name}}
local stream_map = {}
local function heur_dissect_proto(tvbuf, pktinfo, root)
-- dynmaic decide client or server data
-- by first tcp syn frame
local f_src_port = tcp_src_port()()
local f_dst_port = tcp_dst_port()()
local stream_n = tcp_stream().value
if stream_map[stream_n] == nil then
stream_map[stream_n] = {f_src_port, f_dst_port, {}}
end

if (tvbuf:len() < PROTO_HEADER_LENGTH) then
return false
end

local magic = tvbuf:range(0, 2):bytes():tohex()
-- for range dissectors
if magic ~= MAGIC_CODE_TRPC then
return false
end

trpc_proto.dissector(tvbuf, pktinfo, root)

pktinfo.conversation = trpc_proto

return true
end

trpc_proto:register_heuristic("tcp", heur_dissect_proto)

--------------------------------------------------------------------------------

-- check packet length, return length of packet if valid
check_length = function(tvbuf, offset)
local msglen = tvbuf:len() - offset

if msglen ~= tvbuf:reported_length_remaining(offset) then
-- captured packets are being sliced/cut-off, so don't try to desegment/reassemble
LM_WARN("Captured packet was shorter than original, can't reassemble")
return 0
end

if msglen < PROTO_HEADER_LENGTH then
-- we need more bytes, so tell the main dissector function that we
-- didn't dissect anything, and we need an unknown number of more
-- bytes (which is what "DESEGMENT_ONE_MORE_SEGMENT" is used for)
return -DESEGMENT_ONE_MORE_SEGMENT
end

-- if we got here, then we know we have enough bytes in the Tvb buffer
-- to at least figure out whether this is valid trpc packet

local magic = tvbuf:range(offset, 2):bytes():tohex()
if magic ~= MAGIC_CODE_TRPC then
return 0
end

local packet_size = tvbuf:range(offset+4, 4):uint()
if msglen < packet_size then
-- Need more bytes to desegment full trpc packet
return -(packet_size - msglen)
end

return packet_size
end

--------------------------------------------------------------------------------

dissect_proto = function(tvbuf, pktinfo, root, offset)
local len = check_length(tvbuf, offset)
if len <= 0 then
return len
end

-- update 'Protocol' field
if offset == 0 then
pktinfo.cols.protocol:set("tRPC")
end

local f_src_port = tcp_src_port()()
local f_dst_port = tcp_dst_port()()

local direction
local stream_n = tcp_stream().value
if f_src_port == stream_map[stream_n][1] then
pktinfo.private["pb_msg_type"] = "message,trpc.RequestProtocol"
direction = "request"
end
if f_src_port == stream_map[stream_n][2] then
pktinfo.private["pb_msg_type"] = "message,trpc.ResponseProtocol"
direction = "response"
end

-- check packet length,
local magic_value = tvbuf(offset, 2)
local type_value = tvbuf(offset+2, 1)
local stream_value = tvbuf(offset+3, 1)
local total_size_value = tvbuf(offset+4, 4)
local header_size_value = tvbuf(offset+8, 2)
local unique_id_value = tvbuf(offset+10, 4)
local version_value = tvbuf(offset+14, 1)
local reserved_value = tvbuf(offset+15, 1)

local header_length = header_size_value:uint()
local total_length = total_size_value:uint()
local tree = root:add(trpc_proto, tvbuf:range(offset, len), "tRPC Protocol Data")

data_dissector:call(tvbuf, pktinfo, tree)

local t = tree:add(trpc_proto, tvbuf)
t:add(field_magic, magic_value)
t:add(field_type, type_value)
t:add(field_stream, stream_value)
t:add(field_total_size, total_size_value)
t:add(field_header_size, header_size_value)
t:add(field_unique_id, unique_id_value)
t:add(field_version, version_value)
t:add(field_reserved, reserved_value)

-- solve the problem of parsing errors when multiple RPCs are included in a packet
local protobuf_field_names = { proto_f_protobuf_field_name() }
local pre_field_nums = #protobuf_field_names
pcall(Dissector.call, protobuf_dissector, tvbuf(offset+16, header_length):tvb(), pktinfo, tree)

-- Add bussiness rpc pb
-- Get invoke rpc method name from trpc.RequestProtocol
protobuf_field_names = { proto_f_protobuf_field_name() }
local cur_field_nums = #protobuf_field_names
local protobuf_field_values = { proto_f_protobuf_field_value() }
local method
-- default request id
local request_id = 0
for k = pre_field_nums + 1, cur_field_nums do
local v = protobuf_field_names[k]
if v.value == "func" then
method = protobuf_field_values[k].range:string(ENC_UTF8)
elseif v.value == "request_id" then
request_id = protobuf_field_values[k].range:uint()
end
end

local tvb_body = tvbuf:range(offset + 16 + header_length, total_length - header_length - 16):tvb()
if method ~= nil then
-- only req contains method, correlate it with request id so that response protocol can use.
stream_map[stream_n][3][request_id] = method
pktinfo.private["pb_msg_type"] = "application/trpc," .. method .. "," .. direction
else
-- get method for the same request id
method = stream_map[stream_n][3][request_id]
pktinfo.private["pb_msg_type"] = "application/trpc," .. method .. "," .. direction
end
pcall(Dissector.call, protobuf_dissector, tvb_body, pktinfo, tree)

return total_length
end
Loading