gRPC (gRPC Remote Procedure Calls) is a modern, high-performance RPC framework that can run in any environment. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features such as authentication, load balancing, and more. In this blog post, we’ll walk through building a simple gRPC application in Rust and then capture and analyze the network traffic using tcpdump
.
Prerequisites
Before we begin, make sure you have the following installed:
- Rust: Install Rust from rustup.rs.
- tcpdump: Install
tcpdump
using your package manager - grpcurl or postman for making grpc requests.
Setting Up the Rust Project
First, let’s create a new Rust project:
cargo new grpc-examplecd grpc-example
Next, add the necessary dependencies to your Cargo.toml
file:
[package]name = "grpc-example"version = "0.1.0"edition = "2021"
[dependencies]prost = "0.13.5"tonic = "0.12.3"tokio = { version = "1.43.0", features = ["full"] }
[build-dependencies]tonic-build = "0.12.3"
Here, we’re using tonic
, a gRPC implementation for Rust, and prost
for Protocol Buffers.
Defining the gRPC Service
Create a proto
directory in your project root and add a hello.proto
file.
syntax = "proto3";
package hello;
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply);}
message HelloRequest { string name = 1;}
message HelloReply { string message = 3;}
This defines a simple gRPC service with a single SayHello
method. Mainly, there are 4 different ways to define a method by adding or omitting the stream
keyword.
- Unary (client sends a request and server sends the response)
- Server Streaming (client sends a request, but server sends a stream of messages back)
- Client Streaming (client sends a stream of messages, but server sends a single response)
- Bidirectional streaming (both client and server sends streams of messages)
In this post we only implement a unary method as our focus it to understand how protobuf serialization works.
Generating Rust Code
Next, we’ll generate Rust code from the .proto
file. Create a build.rs
file in the root of your project.
fn main() { tonic_build::compile_protos("proto/hello.proto").unwrap();}
This will generate the necessary Rust code when you build your project using the tonic-build crate.
Implementing the gRPC Server
Now, let’s implement the gRPC server. Modify src/main.rs
as follows:
use tonic::{transport::Server, Request, Response, Status};use hello::greeter_server::{Greeter, GreeterServer};use hello::{HelloRequest, HelloReply};
pub mod hello { tonic::include_proto!("hello");}
#[derive(Debug, Default)]pub struct MyGreeter {}
#[tonic::async_trait]impl Greeter for MyGreeter { async fn say_hello( &self, request: Request<HelloRequest>, ) -> Result<Response<HelloReply>, Status> { let name = request.into_inner().name; let reply = HelloReply { message: format!("Hello, {}!", name), }; Ok(Response::new(reply)) }}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default();
println!("GreeterServer listening on {}", addr);
Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?;
Ok(())}
This code sets up a gRPC server that listens on localhost:50051
and responds to SayHello
requests.
Our plan is to use grpcurl or postman as the client, but you are free to try writing a gRPC client using rust. Give that a try, it is easy.
Running the Server
Run the server using:
cargo run
The server should now be running and listening for incoming gRPC requests.
Capturing Packets with tcpdump
To capture the network traffic, open a new terminal and run tcpdump
:
tcpdump -i lo -w grpc_traffic.pcap port 50051
This command captures all traffic on the loopback interface (lo
) on port 50051
and writes it to a file called grpc_traffic.pcap
.
Making a gRPC Request
In another terminal, let’s make a gRPC request to our server. You can use a tool like grpcurl
:
grpcurl -proto ./hello.proto -plaintext -d '{"name": "World"}' localhost:50051 hello.Greeter/SayHello
You should see a response like.
{ "message": "Hello, World!"}
Analyzing the Captured Packets
Stop the tcpdump
process by pressing Ctrl+C
. Now, let’s analyze the captured packets using Wireshark
or tcpdump
itself.
To analyze with tcpdump
:
tcpdump -r grpc_traffic.pcaptcpdump -qns 0 -X -r grpc_traffic.pcap
21:52:33.263066 IP6 ::1.55746 > ::1.50051: tcp 1306002 967e 00a2 0640 0000 0000 0000 0000 `..~...@........0000 0000 0000 0001 0000 0000 0000 0000 ................0000 0000 0000 0001 d9c2 c383 25c2 a109 ............%...6cf1 19cf 8018 0200 00aa 0000 0101 080a l...............37a4 b2bc 37a4 b2bc 0000 6401 0400 0000 7...7.....d.....0183 8645 9162 72d1 41d7 c561 4a92 d8c6 ...E.br.A..aJ...e1fa c65a 283f 418b a0e4 1d13 9d09 b8d8 ...Z(?A.........00d8 7f5f 8b1d 75d0 620d 263d 4c4d 6564 ..._..u.b.&=LMed7a94 9aca c96d 9430 15df 5c4a 4d65 645a z....m.0..\JMedZ63b0 15dc 0ae0 4002 7465 864d 8335 05b1 c.....@.te.M.5..1f40 8e9a cac8 b0c8 42d6 958b 510f 21aa .@......B...Q.!.9b83 9bd9 ab00 000c 0001 0000 0001 0000 ................0000 070a 0557 6f72 6c64 .....World
21:52:33.263753 IP6 ::1.50051 > ::1.55746: tcp 97600f 17bc 0081 0640 0000 0000 0000 0000 `......@........0000 0000 0000 0001 0000 0000 0000 0000 ................0000 0000 0000 0001 c383 d9c2 6cf1 19cf ............l...25c2 a18b 8018 0200 0089 0000 0101 080a %...............37a4 b2bc 37a4 b2bc 0000 2601 0400 0000 7...7.....&.....0188 5f8b 1d75 d062 0d26 3d4c 4d65 6461 .._..u.b.&=LMeda96c3 61be 9410 54c2 58d4 1004 da81 72e0 ..a...T.X.....r.8571 9654 c5a3 7f00 0014 0000 0000 0001 .q.T............0000 0000 0f1a 0d48 656c 6c6f 2c20 576f .......Hello,.Wo726c 6421 0000 0c01 0500 0000 0140 889a rld!.........@..cac8 b212 34da 8f81 07 ....4....
You should see the HTTP/2 frames, including the gRPC request and response. Look for frames that contain what you send in the request and what contained in the response.
First, let’s take a look at the request made from the client, if you recall, we made the request using this command.
grpcurl -proto ./hello.proto -plaintext -d '{"name": "World"}' localhost:50051 hello.Greeter/SayHello
Find the right frame is easy, we know our server listens on port 50051, therefore, first we can look for packets towards port 50051.
And then by looking at the ASCII representation on the right we can select the right packet with the right message sent from the client. In our case the string World
.
0000 070a 0557 6f72 6c64 .....World
gRPC uses the Type–length–value schema. The first hexadecimal value represents the tag, and the subsequent hexadecimal values represent the length(n). And the next n
no. of hexadecimal values represent the actual value stored under the field number.
In our case, we already know the data we send, and we can visually inspect it with ease by looking at the ASCII representation on the side. But sending other types of data will make it a bit difficult.
World
→ Length: 5
With that understanding, if we count 5 bytes from right to left. We get 57
. 0x57
in ASCII represents W
. There we go, That where value starts. Therefore, the byte before that should represent the length of the value. Yes, it is 05
.
Now, with that we know, the byte before the length byte, represent the tag. We can identify the tag as 0a
.
We can find the wire type and the field number by converting this to a binary value.
0x0a
→ 0b1010
The first three bits from the right represent the wire type, and the rest of the bits represent the field number.
So, That’s how we figure out the tag, length and the value from the serialize protobuf data. But what if we send something other than a string. It will be a little tricky to find them. But there is a better way.
gRPC uses the following formula to create the tag for each field in the protocol buffer.
(field_number << 3) | wire_type
if we take a quick look at the .proto
file and the following table, we can generate the tag.
ID | Name | Used For |
---|---|---|
0 | VARINT | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | I64 | fixed64, sfixed64, double |
2 | LEN | string, bytes, embedded messages, packed repeated fields |
3 | SGROUP | group start (deprecated) |
4 | EGROUP | group end (deprecated) |
5 | I32 | fixed32, sfixed32, float |
https://protobuf.dev/programming-guides/encoding/#structure
The field number for the request message in our protocol buffer is 1
. And we have defined the type as string
. From the table, we can find that wire type for string is 2
. That’s all we need to calculate the tag.
If we apply that to the formula,
(field_number << 3) | wire_type
(1 << 3) | 2
and we get this.
8 | 2
The |
in the middle is a bitwise OR operator, so we need to convert this to binary again.
8 → 0000 1000
2 → 0000 0010
bitwise OR is a simple arithmetic operation, you just need to add the numbers together, but 1+1 = 10, which 2 in decimal. And we keep 0 on that column and carry 1 to the next column. I am not going to explain it furthermore, but I am happy to write another post on Computer Arithmetic.
The operation will look something like this in python,
>>> field_number = 0b01>>> field_number = field_number << 3>>> hex(field_number | 0b10)'0xa'>>>
Once, we apply bitwise OR on the two numbers we will get 0b00001010
and this is equivalent to decimal 10
which is 0x0a
in hexadecimal. From that we know where data starts.
So far, we only looked at the request gRPC call, let’s take a look at the response from the server.
21:52:33.263753 IP6 ::1.50051 > ::1.55746: tcp 97600f 17bc 0081 0640 0000 0000 0000 0000 `......@........0000 0000 0000 0001 0000 0000 0000 0000 ................0000 0000 0000 0001 c383 d9c2 6cf1 19cf ............l...25c2 a18b 8018 0200 0089 0000 0101 080a %...............37a4 b2bc 37a4 b2bc 0000 2601 0400 0000 7...7.....&.....0188 5f8b 1d75 d062 0d26 3d4c 4d65 6461 .._..u.b.&=LMeda96c3 61be 9410 54c2 58d4 1004 da81 72e0 ..a...T.X.....r.8571 9654 c5a3 7f00 0014 0000 0000 0001 .q.T............0000 0000 0f1a 0d48 656c 6c6f 2c20 576f .......Hello,.Wo726c 6421 0000 0c01 0500 0000 0140 889a rld!.........@..cac8 b212 34da 8f81 07 ....4....
I intentionally used 3
for the field number in the response message, now should see a difference in the tag,
>>> field_number = 0b11>>> field_number = field_number << 3>>> hex(field_number | 0b10)'0x1a'>>>
based on that, if we look for the tag in our tcpdump, we will find this,
0000 0000 0f1a 0d48 656c 6c6f 2c20 576f .......Hello,.Wo726c 6421 0000 0c01 0500 0000 0140 889a rld!.........@..
Now, we can collect the necessary information by visual inspection.
Field Number | Length | Value | |
---|---|---|---|
Hex | 0x1a | 0x0d | 48656c6c6f2c20576f726c6421 |
Decimal | 26 | 13 | Hello, World! |
It is that easy to look for content on a serialize protocol buffer. Now, let’s take a look at a different scenario.
use hello::greeter_server::{Greeter, GreeterServer};use hello::{HelloReply, HelloRequest};use std::sync::{Arc, Mutex};use tonic::{transport::Server, Request, Response, Status};
pub mod hello { tonic::include_proto!("hello");}
#[derive(Debug)]pub struct MyGreeter { pub count: Arc<Mutex<i32>>, // Use Arc<Mutex> for thread-safe mutability}
#[tonic::async_trait]impl Greeter for MyGreeter { async fn say_hello( &self, request: Request<HelloRequest>, ) -> Result<Response<HelloReply>, Status> { let name = request.into_inner().name;
// Lock the mutex to access and modify the count let mut count = self.count.lock().unwrap(); *count += 1;
// Print the number of requests processed println!("Number of requests: {}", *count);
let reply = HelloReply { message: format!("Hello, {}!", name), count: *count, };
Ok(Response::new(reply)) }}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?;
// Initialize the counter inside an Arc<Mutex> let greeter = MyGreeter { count: Arc::new(Mutex::new(0)), };
println!("GreeterServer listening on {}", addr);
Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?;
Ok(())}
// update the reply messagemessage HelloReply { string message = 1; int32 count = 2;}
Now, update the server code to count the number of requests it receives and the server will include the current count in the reply message.
grpcurl -proto ./hello.proto -plaintext -d '{"name": "grpc"}' localhost:50051 hello.Greeter/SayHello
Running this command again will return a different message compared to last time, this time each request to the server updates the counter and send it in the reply, it is more like an identifier to the request.
15:29:41.180180 IP6 ::1.50051 > ::1.51344: tcp 986006 d7fb 0082 0640 0000 0000 0000 0000 `......@........0000 0000 0000 0001 0000 0000 0000 0000 ................0000 0000 0000 0001 c383 c890 0d45 f2ae .............E..4247 6ac6 8018 0200 008a 0000 0101 080a BGj.............f071 fc68 f071 fc67 0000 2601 0400 0000 .q.h.q.g..&.....0188 5f8b 1d75 d062 0d26 3d4c 4d65 6461 .._..u.b.&=LMeda96dd 6d5f 4a09 9530 9635 0401 36a0 1fb8 ..m_J..0.5..6...dbf7 1a0a 98b4 6f00 0015 0000 0000 0001 ......o.........0000 0000 100a 0c48 656c 6c6f 2c20 6772 .......Hello,.gr7063 2110 0100 000c 0105 0000 0001 4088 pc!...........@.9aca c8b2 1234 da8f 8107
Request to the server will be identical, there is no difference to what we had earlier, but this time we can see this response.
By looking at the tcpdump we can see the first values ends with !
. And the tag for the next values should be right next to that. With that understanding, we can say the tag for the counter
field is 10
. But is it, let’s check it.
>>> f = 0b10>>> f = f << 3>>> hex(f | 0b0)'0x10'>>>
Yes, it is right, we updated the .proto reply message and set the field number for counter field as 2
and set it to be an int32
. Therefore, the formula will look like something like this.
(field_number << 3) | wire_type
2<<3 | 0
and we can see that the field number is two and the value is set to 1
.
If we take a look at the second gRPC call response.
16:14:21.339219 IP6 ::1.50051 > ::1.52030: tcp 756006 29f4 006b 0640 0000 0000 0000 0000 `.)..k.@........0000 0000 0000 0001 0000 0000 0000 0000 ................0000 0000 0000 0001 c383 cb3e 24a0 9923 ...........>$..#d912 994e 8018 0200 0073 0000 0101 080a ...N.....s......f09a e1c7 f09a e1c7 0000 1a01 0400 0000 ................0388 c061 96dd 6d5f 4a09 9530 9635 0401 ...a..m_J..0.5..36a0 4171 a6ae 082a 62d1 bf00 0015 0000 6.Aq...*b.......0000 0003 0000 0000 100a 0c48 656c 6c6f ...........Hello2c20 6772 7063 2110 0200 0001 0105 0000 ,.grpc!.........0003 bf ...
The values are increment by one.
16:21:04.558423 IP6 ::1.50051 > ::1.42188: tcp 1026006 da25 0086 0640 0000 0000 0000 0000 `..%...@........0000 0000 0000 0001 0000 0000 0000 0000 ................0000 0000 0000 0001 c383 a4cc f5b2 dcc6 ................a2e5 565b 8018 0200 008e 0000 0101 080a ..V[............f0a1 08da f0a1 08da 0000 2601 0400 0000 ..........&.....0188 5f8b 1d75 d062 0d26 3d4c 4d65 6461 .._..u.b.&=LMeda96dd 6d5f 4a09 9530 9635 0401 36a0 4171 ..m_J..0.5..6.Aqb0dc 034a 62d1 bf00 0019 0000 0000 0001 ...Jb...........0000 0000 140a 0c48 656c 6c6f 2c20 6772 .......Hello,.gr7063 2110 ffff ffff 0700 000c 0105 0000 pc!.............0001 4088 9aca c8b2 1234 da8f 8107 ..@......4....
Here is another example, in this example the counter is set to the maximum number possible with a 32-bit signed integer. Counter field holds the value 2,147,483,647
or 0x7FFFFFFF
in hexadecimal. Unsigned integers can hold double this amount, but that’s something to explain in another post.
What I wanted to show using the example is unlike strings, numerical values are stored in little-endian where the least significant byte comes first, it is the reverse of how we see numbers naturally. In the above example, the counter value is stored as ffffffff07
in little endian order.
Conclusion
In this blog post, we built a simple gRPC server in Rust, captured the network traffic using tcpdump
, and analyze the packets. gRPC is a powerful tool for building efficient, type-safe APIs, and Rust’s ecosystem makes it easy to get started. By understanding the underlying network traffic, you can gain deeper insights into how gRPC works and how to troubleshoot potential issues.
Feel free to expand on this example by adding more services, implementing client-side code, or exploring advanced features like TLS and authentication. Happy coding!
Resources
Source: https://github.com/n3tw0rth/rust/tree/master/grpc