Featured image of post 使用 wireshark 抓包GRPC

使用 wireshark 抓包GRPC

捕获 gRPC 数据包的分步指南

 

摘要

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
Pasted image 20240511225144

具体操作

首先我们写一个最简单的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

Pasted image 20240511230443
Pasted image 20240511230512

一元函数

现在我们有一个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 应该就能抓到流量包了。
Pasted image 20240511232027
前面我们说过,gRPC = http2+protobuf, 并且我们前面已经加载了protobuf 文件,理论上我们现在已经能解析报文了。
使用wireshark快捷键 shift+command+U 或者 用鼠标点击 Analyze -> Decode As... 然后设置一下将报文解析成HTTP2 格式。
Pasted image 20240511232316
这个时候,我们就能很清晰的看到这个请求了

Pasted image 20240511232448
Pasted image 20240511232507
Pasted image 20240511232539

metadata

我们知道 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 里面。
Pasted image 20240512154517

并且我们还在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
Pasted image 20240519210518
Pasted image 20240519211040

总结

这篇文章,首先总结了使用 Wireshark 抓gRPC 包的一个基本流程。
然后我们通过抓包知道了gRPC的参数传递是通过 HTTP2 的data-frame,CTX 等meta 是通过 header 传递的。这些知识我们以前肯定听过,但是只有动手实验才能加深理解。
通过TLS 我们可以实现 安全的gRPC 通信,下一篇文章,我们将尝试解密TLS 报文。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计