Skip to content

Commit

Permalink
【腾讯犀牛鸟开源课题实战】wireshark协议解析 (#36)
Browse files Browse the repository at this point in the history
* feat: add wireshark support for trpc

* docs: add wireshark usage

---------

Co-authored-by: weimch <[email protected]>
  • Loading branch information
RunoobACMer and weimch authored Nov 20, 2024
1 parent 1b97ae0 commit 07a596d
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 0 deletions.
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

0 comments on commit 07a596d

Please sign in to comment.