Introduce
Wireshark is a popular tool for capturing network packets. It can not only capture packets itself but also parse packet files captured by tcpdump.
gRPC is a high-performance RPC framework developed by Google, based on the HTTP/2 protocol and the protobuf
serialization protocol.
This article mainly introduces how to capture gRPC packets using Wireshark and parse the packet content.
This article was first published in the medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.
Wireshark version: 4.2.2
Configuration
Since gRPC uses the protobuf
serialization protocol, we need to add the protobuf
file path.
Click Wireshark -> Preferences... -> Protocols -> Protobuf -> Protobuf search paths -> Edit...
Click + to add the path of your protobuf
file. Don’t forget to check the Load all files on the right side.
Specific Operations
First, we write a simple gRPC service,
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;
}
|
It has just one function Greeter
. After completing the server-side code, run it.
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)
}
}
|
Then open Wireshark, select the local network card, and listen to tcp.port == 50051
.
If you are new to Wireshark, I recommend reading this article first: https://www.lifewire.com/wireshark-tutorial-4143298
Unary Function
Now we have a gRPC service running on the local 50051
port. We can use BloomRPC
or any other tool you like to initiate an RPC request to the server, or use the following code:
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())
}
|
At this point, Wireshark should be able to capture the traffic packets.
As mentioned earlier, gRPC = HTTP2 + protobuf
, and since we have already loaded the protobuf
file, we should now be able to parse the packet.
Use the Wireshark shortcut shift+command+U
or click Analyze -> Decode As...
and set the packet to be decoded as HTTP2 format.
At this point, we can see the request clearly.
We know that gRPC metadata is transmitted through HTTP2 headers. Now let’s verify this by capturing packets.
Slightly modify the client code:
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)
....
}
|
Then capture packets again. We can see that md
is indeed placed in the header.
We also see grpc-timeout
in the header, indicating that the request timeout is also placed in the header. The specific details may be covered in a dedicated article, but today we focus on packet capturing.
TLS
The examples above use plaintext transmission, and we used grpc.WithInsecure()
when dialing. However, in a production environment, we generally use TLS for encrypted transmission. Detailed information can be found in my previous article.
https://medium.com/gitconnected/secure-communication-with-grpc-from-ssl-tls-certification-to-san-certification-d9464c3d706f
Let’s modify the server-side code:
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( // Enable TLS for all incoming connections
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)
}
// Create a new gRPC server instance with the provided TLS server credentials
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)
}
}
|
Now let’s also modify the client code:
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,
})),
}
// Establish connection to the server
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())
}
}
|
At this point, we can capture packets again and use the same method to decode them. However, we will find that decoding as HTTP2 is no longer possible, but we can decode it as TLS1.3.
Conclusion
This article summarizes the basic process of using Wireshark to capture gRPC packets.
By capturing packets, we learn that gRPC parameter transmission is done through HTTP2 data frames, and CTX and other metadata are transmitted through headers. These concepts might be familiar, but hands-on experiments enhance understanding.
With TLS, we can achieve secure gRPC communication. In the next article, we will attempt to decrypt TLS packets.
References