Skip to content

Commit 07a596d

Browse files
RunoobACMerweimch
andauthored
【腾讯犀牛鸟开源课题实战】wireshark协议解析 (#36)
* feat: add wireshark support for trpc * docs: add wireshark usage --------- Co-authored-by: weimch <[email protected]>
1 parent 1b97ae0 commit 07a596d

13 files changed

+322
-0
lines changed

README.zh_CN.md

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ tRPC是基于插件化理念设计的一款支持多语言、高性能的RPC框
4949
- [用户指南](https://github.com/trpc-group/trpc-go/tree/main/docs/README.zh_CN.md)
5050
- [代码示例](https://github.com/trpc-group/trpc-go/tree/main/examples)
5151

52+
## 如何用wireshark分析tRPC协议
53+
54+
参考 [docs/zh/wireshark_trpc.md](docs/zh/wireshark_trpc.md)
55+
5256
## 如何参与贡献
5357

5458
非常欢迎大家给tRPC做贡献!

docs/images/wireshark/pic1.png

486 KB
Loading

docs/images/wireshark/pic10.png

313 KB
Loading

docs/images/wireshark/pic2.png

28.8 KB
Loading

docs/images/wireshark/pic3.png

261 KB
Loading

docs/images/wireshark/pic4.png

314 KB
Loading

docs/images/wireshark/pic5.png

70.9 KB
Loading

docs/images/wireshark/pic6.png

221 KB
Loading

docs/images/wireshark/pic7.png

328 KB
Loading

docs/images/wireshark/pic8.png

303 KB
Loading

docs/images/wireshark/pic9.png

250 KB
Loading

docs/zh/wireshark_trpc.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# tRPC Wireshark解析器
2+
3+
## 前言
4+
5+
`tool/wireshark_trpc.lua` 是 tRPC 协议的 Wireshark 解析器,提供了对 tRPC 协议头、业务 pb 的解析能力。
6+
7+
当前使用有如下限制:
8+
9+
1. 暂不支持解析tRPC流式协议;
10+
2. 暂不支持attachment;
11+
3. 暂不支持UDP;
12+
4. 不支持解析开启压缩后的pb数据(业务pb数据会被压缩);
13+
14+
## 用法
15+
16+
### tcpdump抓包
17+
18+
执行下面的tcpdump抓包
19+
20+
```bash
21+
# 10001 替换为服务端端口
22+
tcpdump -iany port 10001 -w trpc_packet.pcap
23+
```
24+
25+
**注意:** 有时可能因为机器环境问题,导致tcpdump抓包被截断,比如只抓了请求包的部分,wireshark使用lua脚本解析会失败,这时可以抓包时使用 `-s xxx` 防止截断,代表调整低于xxx字节的packet不截断。
26+
27+
### Wireshark配置
28+
29+
1. 从选项卡 `About Wireshark` -> `Floders` 查看 `Personal Lua Plugins` 的目录。
30+
31+
<img src="../images/wireshark/pic1.png" width="600" />
32+
33+
2. 进入 `Personal Lua Plugins` 目录,放入 [wireshark_trpc.lua](../../tool/wireshark_trpc.lua)
34+
35+
<img src="../images/wireshark/pic2.png" width="400" />
36+
37+
3. 设置proto扫描文件目录,并开启基于protobuf的解析。
38+
39+
- 此文件目录下需要放置 [trpc.proto](../../trpc/trpc.proto)以及业务pb,比如此处我们使用 tRPC-Cpp 示例的 [helloworld.proto](https://github.com/trpc-group/trpc-cpp/blob/main/examples/helloworld/helloworld.proto)
40+
- 需要勾选 `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解析器来解析字段。
41+
42+
<img src="../images/wireshark/pic3.png" width="400" />
43+
44+
<img src="../images/wireshark/pic4.png" width="600" />
45+
46+
<img src="../images/wireshark/pic5.png" width="400" />
47+
48+
### Wireshark加载tcpdump抓包
49+
50+
加载包之后,如果没有看到 `Protocol` 显示 `tRPC`,需要强制加载下lua脚本。
51+
52+
<img src="../images/wireshark/pic6.png" width="200" />
53+
54+
如果都成功,能看如下图所示tRPC协议以及业务pb都被正常解析了。
55+
56+
<img src="../images/wireshark/pic7.png" width="600" />
57+
58+
<img src="../images/wireshark/pic8.png" width="600" />
59+
60+
### 筛选指定请求和响应
61+
62+
有时会发现并发很多个请求,可能有某几个请求会调用失败,为了进一步排查,需要更多tRPC协议/业务pb字段等信息,这时候可以通过 protobuf 字段来筛选出指定请求/响应。
63+
64+
Wireshark 支持通过 protobuf 字段做筛选,业务可以选择使用trpc协议头的 request_id 或者业务 pb 某个字段做匹配来查看指定请求和响应的交互情况。
65+
66+
下面是根据trpc协议头的 `requet_id=3` 筛选指定请求和响应的示例,大家可根据自身情况指定筛选条件。
67+
68+
```text
69+
protobuf.field.name == "request_id" and protobuf.field.value == 3
70+
```
71+
72+
<img src="../images/wireshark/pic9.png" width="600" />
73+
74+
### 忽略tcp的控制帧
75+
76+
我们更多的会关注协议包的交互情况,而不关注tcp控制帧(tcp握手/挥手等),这时可以通过设置下面的筛选条件清除tcp控制帧的显示。
77+
78+
```text
79+
protobuf
80+
```
81+
82+
<img src="../images/wireshark/pic10.png" width="600" />

tool/wireshark_trpc.lua

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
--
2+
--
3+
-- Tencent is pleased to support the open source community by making tRPC available.
4+
--
5+
-- Copyright (C) 2024 THL A29 Limited, a Tencent company.
6+
-- All rights reserved.
7+
--
8+
-- If you have downloaded a copy of the tRPC source code from Tencent,
9+
-- please note that tRPC source code is licensed under the Apache 2.0 License,
10+
-- A copy of the Apache 2.0 License is included in this file.
11+
--
12+
--
13+
-- tRPC is licensed under the Apache 2.0 License, and includes source codes from
14+
-- the following components:
15+
-- 1. incubator-brpc
16+
-- Copyright (C) 2019 The Apache Software Foundation
17+
-- incubator-brpc is licensed under the Apache 2.0 License.
18+
--
19+
--
20+
21+
local protocol_name = "trpc"
22+
local trpc_proto = Proto(protocol_name, "tRPC Protocol Dissector")
23+
24+
local field_magic = ProtoField.uint16(protocol_name .. ".magic", "Magic", base.HEX)
25+
local field_type = ProtoField.uint8(protocol_name .. ".type", "Packet Type", base.DEC)
26+
local field_stream = ProtoField.uint8(protocol_name .. ".stream", "Stream Type", base.DEC)
27+
local field_total_size = ProtoField.uint32(protocol_name .. ".total_size", "Total Size", base.DEC)
28+
local field_header_size= ProtoField.uint16(protocol_name .. ".header_size", "Header Size", base.DEC)
29+
local field_unique_id = ProtoField.uint32(protocol_name .. ".unique_id", "Unique ID", base.DEC)
30+
local field_version = ProtoField.uint8(protocol_name .. ".version", "Version", base.DEC)
31+
local field_reserved = ProtoField.uint8(protocol_name .. ".reserved", "Reserved", base.DEC)
32+
trpc_proto.fields = {field_magic, field_type, field_stream, field_total_size, field_header_size, field_unique_id, field_version, field_reserved}
33+
34+
local MAGIC_CODE_TRPC = "0930"
35+
local PROTO_HEADER_LENGTH = 16
36+
37+
local tcp_src_port = Field.new("tcp.srcport")
38+
local tcp_dst_port = Field.new("tcp.dstport")
39+
local tcp_stream = Field.new("tcp.stream")
40+
41+
local proto_f_protobuf_field_name = Field.new("protobuf.field.name")
42+
local proto_f_protobuf_field_value = Field.new("protobuf.field.value")
43+
44+
local data_dissector = Dissector.get("data")
45+
local protobuf_dissector = Dissector.get("protobuf")
46+
47+
----------------------------------------
48+
-- declare functions
49+
local check_length = function() end
50+
local dissect_proto = function() end
51+
52+
----------------------------------------
53+
-- main dissector
54+
function trpc_proto.dissector(tvbuf, pktinfo, root)
55+
local pktlen = tvbuf:len()
56+
57+
local bytes_consumed = 0
58+
59+
while bytes_consumed < pktlen do
60+
local result = dissect_proto(tvbuf, pktinfo, root, bytes_consumed)
61+
62+
if result > 0 then
63+
bytes_consumed = bytes_consumed + result
64+
elseif result == 0 then
65+
-- hit an error
66+
return 0
67+
else
68+
pktinfo.desegment_offset = bytes_consumed
69+
-- require more bytes
70+
pktinfo.desegment_len = -result
71+
72+
return pktlen
73+
end
74+
end
75+
76+
return bytes_consumed
77+
end
78+
79+
--------------------------------------------------------------------------------
80+
-- heuristic
81+
-- tcp_stream_id <-> {client_port, server_port, {request_id<->method_name}}
82+
local stream_map = {}
83+
local function heur_dissect_proto(tvbuf, pktinfo, root)
84+
-- dynmaic decide client or server data
85+
-- by first tcp syn frame
86+
local f_src_port = tcp_src_port()()
87+
local f_dst_port = tcp_dst_port()()
88+
local stream_n = tcp_stream().value
89+
if stream_map[stream_n] == nil then
90+
stream_map[stream_n] = {f_src_port, f_dst_port, {}}
91+
end
92+
93+
if (tvbuf:len() < PROTO_HEADER_LENGTH) then
94+
return false
95+
end
96+
97+
local magic = tvbuf:range(0, 2):bytes():tohex()
98+
-- for range dissectors
99+
if magic ~= MAGIC_CODE_TRPC then
100+
return false
101+
end
102+
103+
trpc_proto.dissector(tvbuf, pktinfo, root)
104+
105+
pktinfo.conversation = trpc_proto
106+
107+
return true
108+
end
109+
110+
trpc_proto:register_heuristic("tcp", heur_dissect_proto)
111+
112+
--------------------------------------------------------------------------------
113+
114+
-- check packet length, return length of packet if valid
115+
check_length = function(tvbuf, offset)
116+
local msglen = tvbuf:len() - offset
117+
118+
if msglen ~= tvbuf:reported_length_remaining(offset) then
119+
-- captured packets are being sliced/cut-off, so don't try to desegment/reassemble
120+
LM_WARN("Captured packet was shorter than original, can't reassemble")
121+
return 0
122+
end
123+
124+
if msglen < PROTO_HEADER_LENGTH then
125+
-- we need more bytes, so tell the main dissector function that we
126+
-- didn't dissect anything, and we need an unknown number of more
127+
-- bytes (which is what "DESEGMENT_ONE_MORE_SEGMENT" is used for)
128+
return -DESEGMENT_ONE_MORE_SEGMENT
129+
end
130+
131+
-- if we got here, then we know we have enough bytes in the Tvb buffer
132+
-- to at least figure out whether this is valid trpc packet
133+
134+
local magic = tvbuf:range(offset, 2):bytes():tohex()
135+
if magic ~= MAGIC_CODE_TRPC then
136+
return 0
137+
end
138+
139+
local packet_size = tvbuf:range(offset+4, 4):uint()
140+
if msglen < packet_size then
141+
-- Need more bytes to desegment full trpc packet
142+
return -(packet_size - msglen)
143+
end
144+
145+
return packet_size
146+
end
147+
148+
--------------------------------------------------------------------------------
149+
150+
dissect_proto = function(tvbuf, pktinfo, root, offset)
151+
local len = check_length(tvbuf, offset)
152+
if len <= 0 then
153+
return len
154+
end
155+
156+
-- update 'Protocol' field
157+
if offset == 0 then
158+
pktinfo.cols.protocol:set("tRPC")
159+
end
160+
161+
local f_src_port = tcp_src_port()()
162+
local f_dst_port = tcp_dst_port()()
163+
164+
local direction
165+
local stream_n = tcp_stream().value
166+
if f_src_port == stream_map[stream_n][1] then
167+
pktinfo.private["pb_msg_type"] = "message,trpc.RequestProtocol"
168+
direction = "request"
169+
end
170+
if f_src_port == stream_map[stream_n][2] then
171+
pktinfo.private["pb_msg_type"] = "message,trpc.ResponseProtocol"
172+
direction = "response"
173+
end
174+
175+
-- check packet length,
176+
local magic_value = tvbuf(offset, 2)
177+
local type_value = tvbuf(offset+2, 1)
178+
local stream_value = tvbuf(offset+3, 1)
179+
local total_size_value = tvbuf(offset+4, 4)
180+
local header_size_value = tvbuf(offset+8, 2)
181+
local unique_id_value = tvbuf(offset+10, 4)
182+
local version_value = tvbuf(offset+14, 1)
183+
local reserved_value = tvbuf(offset+15, 1)
184+
185+
local header_length = header_size_value:uint()
186+
local total_length = total_size_value:uint()
187+
local tree = root:add(trpc_proto, tvbuf:range(offset, len), "tRPC Protocol Data")
188+
189+
data_dissector:call(tvbuf, pktinfo, tree)
190+
191+
local t = tree:add(trpc_proto, tvbuf)
192+
t:add(field_magic, magic_value)
193+
t:add(field_type, type_value)
194+
t:add(field_stream, stream_value)
195+
t:add(field_total_size, total_size_value)
196+
t:add(field_header_size, header_size_value)
197+
t:add(field_unique_id, unique_id_value)
198+
t:add(field_version, version_value)
199+
t:add(field_reserved, reserved_value)
200+
201+
-- solve the problem of parsing errors when multiple RPCs are included in a packet
202+
local protobuf_field_names = { proto_f_protobuf_field_name() }
203+
local pre_field_nums = #protobuf_field_names
204+
pcall(Dissector.call, protobuf_dissector, tvbuf(offset+16, header_length):tvb(), pktinfo, tree)
205+
206+
-- Add bussiness rpc pb
207+
-- Get invoke rpc method name from trpc.RequestProtocol
208+
protobuf_field_names = { proto_f_protobuf_field_name() }
209+
local cur_field_nums = #protobuf_field_names
210+
local protobuf_field_values = { proto_f_protobuf_field_value() }
211+
local method
212+
-- default request id
213+
local request_id = 0
214+
for k = pre_field_nums + 1, cur_field_nums do
215+
local v = protobuf_field_names[k]
216+
if v.value == "func" then
217+
method = protobuf_field_values[k].range:string(ENC_UTF8)
218+
elseif v.value == "request_id" then
219+
request_id = protobuf_field_values[k].range:uint()
220+
end
221+
end
222+
223+
local tvb_body = tvbuf:range(offset + 16 + header_length, total_length - header_length - 16):tvb()
224+
if method ~= nil then
225+
-- only req contains method, correlate it with request id so that response protocol can use.
226+
stream_map[stream_n][3][request_id] = method
227+
pktinfo.private["pb_msg_type"] = "application/trpc," .. method .. "," .. direction
228+
else
229+
-- get method for the same request id
230+
method = stream_map[stream_n][3][request_id]
231+
pktinfo.private["pb_msg_type"] = "application/trpc," .. method .. "," .. direction
232+
end
233+
pcall(Dissector.call, protobuf_dissector, tvb_body, pktinfo, tree)
234+
235+
return total_length
236+
end

0 commit comments

Comments
 (0)