摘要
wireshark 是一个 流行的抓取网络报文的工具,他不仅自己可以抓包,也可以解析tcpdump抓包的文件。
gRPC 是Google开发的一个高性能RPC框架,基于HTTP/2协议+protobuf序列化协议.
本文主要介绍如何使用wireshark抓取gRPC的报文,并解析报文内容。
This article is first published in the medium MPP plan. If you are a medium user, please follow me in medium. Thank you very much.
Wireshark version: 4.2.2
配置
因为gRPC 是基于protobuf序列化协议,所以我们需要先添加protobuf的文件地址。
点击 Wireshark -> Preferences… -> Protocols -> Protobuf -> Protobuf search paths -> Edit…
点击+ 添加您要抓包的protobuf 文件路径,不要忘记勾选右边的 Load all files
具体操作
首先我们写一个最简单的gRPC服务,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
syntax = "proto3";
option go_package = "example.com/hxzhouh/go-example/grpc/helloworld/api";
package api;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
|
它仅仅就一个函数 Greeter
,补充完服务端代码,把它运行起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type server struct {
api.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *api.HelloRequest) (*api.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &api.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
api.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
|
然后我们打开 wireshark ,选择本地网卡,监听 tcp.port == 50051
如果您以前没接触过 wireshark,我建议您先看看这篇文章:https://www.lifewire.com/wireshark-tutorial-4143298
一元函数
现在我们有一个gRPC 服务运行再本地的50051
端口, 我们可以使用BloomRPC
或者其他您任何喜欢的工具对服务端发起一个RPC请求,或者直接像我一样使用下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func Test_server_SayHello(t *testing.T) {
// Set up a connection to the server.
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := api.NewGreeterClient(conn)
// Contact the server and print out its response.
name := "Hello"
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &api.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
|
这个时候,wireshark 应该就能抓到流量包了。
前面我们说过,gRPC = http2+protobuf, 并且我们前面已经加载了protobuf 文件,理论上我们现在已经能解析报文了。
使用wireshark快捷键 shift+command+U
或者 用鼠标点击 Analyze -> Decode As...
然后设置一下将报文解析成HTTP2 格式。
这个时候,我们就能很清晰的看到这个请求了
我们知道 gRPC 的metadata 是通过 http2 的header 来传递的。 现在我们通过抓包来验证一下。
稍微改造一下客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
|
func Test_server_SayHello(t *testing.T) {
// Set up a connection to the server.
.....
// add md
md := map[string][]string{"timestamp": {time.Now().Format(time.Stamp)}}
md["testmd"] = []string{"testmd"}
ctx := metadata.NewOutgoingContext(context.Background(), md)
// Contact the server and print out its response.
name := "Hello"
ctx, cancel := context.WithTimeout(ctx, time.Second)
....
}
|
然后重新抓包。 我们就能看到 md 确实放在 header 里面。
并且我们还在header 看到了grpc-timeout
可见请求超时操作也是房子啊header 里面的。里面涉及的具体细节,我可能会出一篇专门的文章来说明,今天我们只关注抓包。
TLS
上面使用的例子都是明文 传输的 我们再Dial 的时候使用了 grpc.WithInsecure()
,但是在生产环境中,我们一般使用TLS 对进行加密传输。具体的细节可以参考我以前写的文章。
https://medium.com/gitconnected/secure-communication-with-grpc-from-ssl-tls-certification-to-san-certification-d9464c3d706f
我们改造一下 服务端代码
https://gist.github.com/hxzhouh/e08546cf0457d28a614d59ec28870b11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
func main() {
certificate, err := tls.LoadX509KeyPair("./keys/server.crt", "./keys/server.key")
if err != nil {
log.Fatalf("Failed to load key pair: %v", err)
}
certPool := x509.NewCertPool()
ca, err := os.ReadFile("./keys/ca.crt")
if err != nil {
log.Fatalf("Failed to read ca: %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatalf("Failed to append ca certificate")
}
opts := []grpc.ServerOption{
grpc.Creds( // 为所有传入的连接启用TLS
credentials.NewTLS(&tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{certificate},
ClientCAs: certPool,
},
)),
}
listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", 50051))
if err != nil {
log.Fatalf("failed to listen %d port", 50051)
}
// 通过传入的TLS服务器凭证创建新的gRPC服务实例
s := grpc.NewServer(opts...)
api.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", listen.Addr())
if err := s.Serve(listen); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
|
client
https://gist.github.com/hxzhouh/46a7a31e2696b87fe6fb83c8ce7e036c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
func Test_server_SayHello(t *testing.T) {
certificate, err := tls.LoadX509KeyPair("./keys/client.crt", "./keys/client.key")
if err != nil {
log.Fatalf("Failed to load client key pair, %v", err)
}
certPool := x509.NewCertPool()
ca, err := os.ReadFile("./keys/ca.crt")
if err != nil {
log.Fatalf("Failed to read %s, error: %v", "./keys/ca.crt", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatalf("Failed to append ca certs")
}
opts := []grpc.DialOption{
grpc.WithTransportCredentials(credentials.NewTLS(
&tls.Config{
ServerName: "localhost",
Certificates: []tls.Certificate{certificate},
RootCAs: certPool,
})),
}
// conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
conn, err := grpc.Dial("localhost:50051", opts...)
if err != nil {
log.Fatalf("Connect to %s failed", "localhost:50051")
}
defer conn.Close()
client := api.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
r, err := client.SayHello(ctx, &api.HelloRequest{Name: "Hello"})
if err != nil {
log.Printf("Failed to greet, error: %v", err)
} else {
log.Printf("Greeting: %v", r.GetMessage())
}
}
|
这个时候我们再抓包,然后使用相同的方式解析。但是,我们会发现,使用HTTP2 已经无法解密了,但是可以解码成 TLS1.3
总结
这篇文章,首先总结了使用 Wireshark 抓gRPC 包的一个基本流程。
然后我们通过抓包知道了gRPC的参数传递是通过 HTTP2 的data-frame,CTX 等meta 是通过 header 传递的。这些知识我们以前肯定听过,但是只有动手实验才能加深理解。
通过TLS 我们可以实现 安全的gRPC 通信,下一篇文章,我们将尝试解密TLS 报文。
参考资料