Demystifying Network Sockets (Part 2): Deconstructing Ping with C and Node.js
This tutorial was originally posted by the author on his github page. This version has been edited for clarity and may appear different from the original post.
I’m David Gatti and my goal in this post is to, again, demystify the word protocols. I'll be a bit more specific this time and cover structs in C. This is another post in a series of articles where I try to learn something new myself and try to prove that there is nothing too hard to learn. All we need to do is to pass through the unknown cloudy zone. On the other side, the weather is clear and sunny.
Before you start, I recommend that you read the previous article, where I explain sockets in detail using Particle and Node.js. We're going to use sockets in this one, too, but this time, we're going to focus on how to work with a binary protocol. We're also going to craft our own ICMP header and read the response from the remote host.
I know that the words protocol, binary, and crafting might sound difficult and complex; however, I can guarantee that that is not the case.
How to understand a header specification
If you visit the Wikipedia page on ICMP protocol, you’ll find this table. It describes the header that needs to be sent over the wire to actually make a Ping request.
Let's start by talking about a binary protocol. In binary protocol, we are going to send bytes, which are numbers. These numbers are as is, they are not, for example, an ASCII representation of the keyboard character set.
One byte is 8 bits, which means that an integer of value 8 is 00001000. This is not an ASCII 8. This means that we can’t simply type the number 8 on our keyboards.
To make life easier, we're going to write our data in Hex (short for Hexadecimal). This format is way more manageable — instead of writing numbers as integers, we're going to write an integer in a set of 2 characters. 38450 becomes
0x9632, which will be displayed as 96 32. A space will be inserted between every 2 numbers because 2 numbers in Hex are considered one byte. This makes it way easier to debug in the console.
Let's break down the table above
It goes from 00 to 31, which means each row consists of 32 bits, which, if we divide by 8 bits, gives us 4 bytes. The table has 3 rows, so in total, we are going to send 12 bytes.
The first row consists of 3 data sets: 1 byte for the type (uint8_t); 1 byte for the code (uint8_t); and 2 bytes for the check sum (uint16_t). This could look like:
08 00 FF F7.
The second row has 2 bytes (uint16_t) for the identifier, and 2 for the sequence number. Here's an example:
09 A0 00 01.
The third row is the payload, which is optional. You can send some random data to the router, which would return that data back to you. It's useful if you want to make sure that data is not lost along the way. We will not be using this in our example.
Why we're using Node.js for this project
This project uses Node.js to show the difference between a low level language and a high level one. Node.js can handle sockets very well. However, there are different types of sockets that live in different parts of the OSI model. As you can see from the Wikipedia table, TCP and UDP live on the fourth layer of the model, which Node.js can handle. However, from the Examples column, you can see that ICMP lives on the third layer — Node.js can not reach this layer. With that said, we will still be able to ping from Node.js; I’ll explain how to do that later.
The file structure
As you can see, there are two folders: C and Node.js. Each folder has three files that are named using the same format to help you match each example from one language to the other:
- pingWithBareMinimum: This file will create a PING with the bare minimum code needed to do so. This way, we can focus on understanding how to build our own header and get a result from the remote machine.
- pingWithStructResponse: This file is where we are going to apply our knowledge from before. However, this time, we're going to store the results in a C struct and a buffer in the case of Node.js
- pingWithChecksum: This is where we implement everything so we can actually send a proper ping with changing data.
Let's start with C
Section 3 in the
pingWithBareMinimum.c file is the part that interests us the most. Here is where we actually create the ICMP header. First of all, we must describe a struct, which is our header - the same header that I described above in the image. To make sure we are on the same page:
- uint8_t: means 8 bits, which is 1 byte
- uint16_t: means 16 bits, which is 2 bytes
- uint32_t: means 32 bits, which is 4 bytes
Now that we are finished with our struct, we have basically created our own data type. That's why we're typing
icmp_hdr_t pckt;, where
icmp_hdr_t is the type that we created. We'd then create a variable of this type.
The following code just uses the newly created struct and adds the required data. One important thing to notice is the data field. We don’t need to write 4 zeros to fill the 32-bit space. This is because when we create the variable with our struct, the memory is already assigned. By explicitly saying
uint32_t data;, the compiler knows that we are using 4 bytes of memory.
The next part is the IP header, which, as you can see, also uses a struct. But this time, we are not making it because someone else already did it in one of the libraries that we imported. We could make our own — we would just need to follow the IP protocol. But we're lazy, so let's use someone else's creation!
Once we have all of this, we can use the socket that we've created at the beginning of the file and send our header. If we did everything correctly, in Section 4, we should get a nice replay from the remote host.
Read the replay
Up until now, we've created the most basic ping possible. In this section, we're going to focus on the response. More specifically, we're going to look at how to get the response and map it to a struct so we can have easy access to the data within our code. Let's open the
pingWithStructResponse.c file and get down to it.
Scroll down to Section Four and you’ll see that there is a new struct in place. Here, we are, again, telling the compiler how much space are we going to need and how it should be divided.
On line 110, you can see that we are creating a variable using our struct, and in the line below that, we are casting the buffer to our variable, starting at the 20th byte. Since everything before the 20th is data related to the IP and TCP protocol, which we don’t care about, we just want to see what message we have from the remote host.
After mapping the data to a structure, you can see in the next line that we are able to log each piece of information very easily.
We can make a request and we can easily read the reply. But we can’t use the whole protocol yet — it lacks the checksum function that should calculate the check sum. Right now, we would have to keep sending a fixed checksum because we don't pass the identifier or the sequence_number.
Now is the right time to improve our code.
In regards to the new variables from file
- identifier: A number that, if sent, will be sent back to us. The idea is that we can identify whether the ping came from our app or someone else's. We can filter the incoming ping responses to only show ours in replay.
- sequence_number: A number that will also be sent back to us. It is useful to check whether we've lost some packets — the idea is to increment this number whenever we make a ping request.
Adding these values will make each request unique; therefore, we must calculate the checksum. Otherwise, the ping will be stopped within our network by our home router.
The checksum works in the following way: it grabs two bytes at a time and sum them together with the previous value. If there are left overs, it means the headers are not dividable by two — the if statement after the loop will add the last value. Since we are dealing with a 12 byte header, we’ll never use that code.
The difference between Node.js and C
Until now, we've covered only the C code in this project, but why even put Node.js here? I like to see what the limits of this environment are and then use it as an example to show the difference between a low-level language like C and a higher-level language like Node.js.
Since the file names are exactly the same - as far as the format - you can easily open the matching files and see the difference when you perform certain tasks. For example, it is much easier to create a binary header in C, thanks to structs, and map a buffer to each struct to have easy access to the data.
This also means that this module might work differently under different systems, since it uses the sockets that the systems is providing. The author of this module, of course, tried to make sure it would work in as many places as possible, but different systems - like Windows and MacOS - don’t adhere to the POSIX standard. I highly recommend reading the project README.md file to learn more.
Was it that bad?
And you made it! I hope this article was clear and that protocols don't scare you anymore! By now, this word should make you excited at the prospect of learning how a piece of software is able to communicate with another remote device, whether it's a computer, IoT, or even a radio.
If you've enjoyed this article/project, please consider giving it a 🌟. Also check out my GitHub account, where I have other articles and apps that you might find interesting.
Where to follow
You can follow me on social media 🐙😇, at the following locations:
More about me
I don’t only live on GitHub, I try to do many things not to get bored 🙃. To learn more about me, you can visit the following links: