How Ping work?

A C++ raw ICMP ping utility that sends ICMP Echo Requests to a host, measures round-trip time, and prints results. Uses threads to receive replies, calculates checksums, handles timeouts, and shows statistics including min/avg/max RTT and packet loss. Supports -c for count.

Network
Programming
Code
Banner image

What is Ping?!

Ping is a diagnostic network utility that tests reachability of a host on an IP network and measures the round-trip time (RTT) for packets to travel from the source to the destination and back. it uses ICMP (internet control message protocol).

Packet Format

ICMP, is encapsulated inside an IP packet, meaning the ICMP packet itself comes after IP header.

  1. type it's 1 byte, contain the type of ICMP message, each reply, destination unreachable. each request, etc.
  2. Code 1 byte, provides more information about the type for example type 3 which mean Destination Unreachable has code like
    1. 0, Net Unreachable
    2. 1, Host Unreachable
    3. 3, Port Unreachable
  3. Checksum 2 bytes, error-checking over the ICMP message (header + data).
  4. Identifier 2 bytes,help's distinguish multiple pings .
  5. Sequence Number 2 bytes, increments with each ping.
  6. Data optional

what happens when you run ping 8.8.8.8

When you type ping 8.8.8.8 on your computer, the ping utility creates an ICMP Echo Request packet. This packet contains an ICMP header specifying the message type (8 for Echo Request), a code (usually 0), a checksum for error detection, and fields like an identifier and sequence number to match replies. The packet also includes a payload, typically 32 to 56 bytes, which is used to measure round-trip time. This ICMP message is then encapsulated inside an IP packet with your computer’s IP as the source, 8.8.8.8 as the destination, and a TTL (Time To Live) value to prevent infinite loops.

Next, the IP packet is handed to your network interface, which prepares it for transmission over your local network. If the destination is outside your LAN, your computer uses ARP (Address Resolution Protocol) to determine the MAC address of the default gateway. The packet is then wrapped in an Ethernet frame (or Wi-Fi frame) and sent to the router. The router reads the IP header, decrements the TTL, and forwards the packet towards 8.8.8.8 based on its routing table. This process continues through multiple routers across the internet until the packet reaches the destination network.

When the packet arrives at Google’s DNS server at 8.8.8.8, the server inspects the ICMP type and recognizes it as an Echo Request. It then prepares an ICMP Echo Reply, which mirrors the identifier, sequence number, and payload of the original request, and calculates a new checksum. The reply is encapsulated in a new IP packet with the server’s IP as the source and your IP as the destination. This packet traverses the internet in reverse, passing through routers that decrement TTLs and forward the packet along the correct path.

Finally, the ICMP Echo Reply reaches your local network and is delivered to your computer. The network interface strips the Ethernet frame, and the IP layer passes the ICMP payload to the ICMP handler. The ping utility matches the reply to the original request using the identifier and sequence number. It calculates the round-trip time by comparing the timestamps of sending and receiving, and displays the result in a readable format, such as:

64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=22 ms

This entire process involves multiple layers of the network stack: the application layer generates the request, the network layer handles routing and addressing, the data link layer manages local delivery, and physical layer transmits the bits. Along the way, mechanisms like TTL and fragmentation ensure packets are delivered efficiently and safely, while ICMP provides diagnostic feedback to the sender.

Code

the code is simple all what you need just basic c++ understanding and networking.

first step we need to create a ICMP structure. This structure matches the standard ICMP packet format. The payload is filled with dummy data (0xAA) and can be used to measure latency.

ping.cpp
1struct ICMPPacket { 2 uint8_t type; // Message type: 8 = Echo Request 3 uint8_t code; // Code, usually 0 for Echo 4 uint16_t checksum; // Checksum over the ICMP message 5 uint16_t id; // Identifier (usually process ID) 6 uint16_t sequence; // Sequence number 7 uint8_t payload[56]; // Payload data 8}; 9

then we created a ICMP class warps the packet structure and provide helper methods. first we have the constructor initializes an echo request with a process-based identifier, sequence number, and payload. then we have calculateChecksum() to compute the ICMP checksum over the header + payload. This ensures packet integrity. and we have some helpers, like setInfo etc, last one is match used to compare a received ICMP packet to a send one using the type, id, and sequence.

ping.cpp
1 2class ICMP { 3private: 4 ICMPPacket packet_; 5 static string payloadHex(const uint8_t* data, size_t len) { 6 stringstream ss; 7 ss << hex << uppercase << setfill('0'); 8 for (size_t i = 0; i < len; ++i) 9 ss << setw(2) << (int)data[i] << (i < len-1 ? " " : ""); 10 return ss.str(); 11 } 12 static string payloadAscii(const uint8_t* data, size_t len) { 13 string s; 14 for (size_t i = 0; i < len; ++i) 15 s += isprint(data[i]) ? (char)data[i] : '.'; 16 return s; 17 } 18 static uint16_t calculateChecksum(const void *data, size_t len) { 19 const uint16_t *words = static_cast<const uint16_t *>(data); 20 uint32_t sum = 0; 21 size_t count = len / 2; 22 for (size_t i = 0; i < count; ++i) { 23 sum += ntohs(words[i]); 24 } 25 if (len & 1) { 26 sum += *(reinterpret_cast<const uint8_t *>(data) + len - 1); 27 } 28 sum = (sum >> 16) + (sum & 0xFFFF); 29 sum += (sum >> 16); 30 return static_cast<uint16_t>(~sum); 31 } 32public: 33 ICMP(uint16_t id = 0, uint16_t seq = 0) { 34 packet_.type = 8; 35 packet_.code = 0; 36 packet_.id = htons(id ? id : (getpid() & 0xFFFF)); 37 packet_.sequence = htons(seq); 38 memset(packet_.payload, 0xAA, sizeof(packet_.payload)); 39 updateChecksum(); 40 } 41 void setSequence(uint16_t seq) { packet_.sequence = htons(seq); updateChecksum(); } 42 void updateChecksum() { 43 packet_.checksum = 0; 44 packet_.checksum = htons(calculateChecksum(&packet_, sizeof(packet_))); 45 } 46 uint16_t getId() const { return ntohs(packet_.id); } 47 uint16_t getSeq() const { return ntohs(packet_.sequence); } 48 uint16_t getCksum() const { return ntohs(packet_.checksum); } 49 size_t size() const { return sizeof(packet_); } 50 const ICMPPacket* raw() const { return &packet_; } 51 bool matches(const ICMPPacket* r) const { 52 return r->type == 0 && r->code == 0 && 53 r->id == packet_.id && r->sequence == packet_.sequence; 54 } 55 string sentInfo(const string& ip) const { 56 stringstream ss; 57 ss << "PING " << ip << " (" << getId() << ") " 58 << sizeof(packet_.payload) << " data bytes: " 59 << "cksum=0x" << hex << uppercase << setw(4) << setfill('0') 60 << getCksum() << dec; 61 return ss.str(); 62 } 63 string replyInfo(const in_addr& from, double rtt) const { 64 stringstream ss; 65 ss << (8 + sizeof(packet_.payload)) << " bytes from " << inet_ntoa(from) 66 << ": icmp_seq=" << getSeq() << " id=" << getId() 67 << " ttl=128 time=" << fixed << setprecision(3) << rtt << " ms" 68 << " cksum=0x" << hex << uppercase << setw(4) << setfill('0') 69 << getCksum() << dec; 70 return ss.str(); 71 } 72};

function resolveHost() users getaddrinfo() to covert a hostname to an IPv4 address. the result address is stored in sockaddr_storage for later use when we send icmp.

ping.cpp
1bool resolveHost(const string& host, sockaddr_storage& out, socklen_t& len) { 2 addrinfo hints{}, *res; 3 hints.ai_family = AF_INET; 4 hints.ai_socktype = SOCK_RAW; 5 if (getaddrinfo(host.c_str(), nullptr, &hints, &res)) return false; 6 memcpy(&out, res->ai_addr, res->ai_addrlen); 7 len = res->ai_addrlen; 8 freeaddrinfo(res); 9 return true; 10} 11

now the main() function we start be creating the a SOCK_RAW which allow us to access lower-level protocols which ICMP in this case unlike TCP or UDP. it bypass transport layer and let's us craft's packets. note this requires root privileges.

We set a receive timeout on the socket so the program won’t block indefinitely waiting for a reply. Then we enter a loop that runs for the number of pings specified by the user with the -c option. On each iteration the program constructs and sends an ICMP Echo Request, waits up to the socket timeout for a reply, records the result (or reports a timeout), and moves on to the next sequence number.

ping.cpp
1 2 3int main(int argc, char* argv[]) { 4 if (argc < 3) { 5 cerr << "Usage: " << argv[0] << " [-c count] <host>\n"; 6 return 1; 7 } 8 9 int count = 4; 10 string host; 11 for (int i = 1; i < argc; ++i) { 12 if (string(argv[i]) == "-c" && i+1 < argc) count = atoi(argv[++i]); 13 else host = argv[i]; 14 } 15 if (host.empty()) { cerr << "Host required\n"; return 1; } 16 17 sockaddr_storage dst{}; 18 socklen_t dlen = 0; 19 if (!resolveHost(host, dst, dlen)) return 1; 20 21 char ipStr[INET_ADDRSTRLEN]; 22 inet_ntop(AF_INET, &((sockaddr_in*)&dst)->sin_addr, ipStr, sizeof(ipStr)); 23 24 int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); 25 if (sock < 0) { cerr << "socket: " << strerror(errno) << "\n"; return 1; } 26 27 timeval tv{1, 0}; 28 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); 29 30 uint16_t pid = getpid() & 0xFFFF; 31 int sent = 0, recv = 0; 32 double minRtt = 1e9, maxRtt = 0, sumRtt = 0; 33 34 for (int seq = 1; seq <= count; ++seq) { 35 ICMP pkt(pid, seq); 36 cout << pkt.sentInfo(ipStr) << "\n"; 37 38 auto st = high_resolution_clock::now(); 39 if (sendto(sock, pkt.raw(), pkt.size(), 0, (sockaddr*)&dst, dlen) < 0) { 40 cerr << "sendto error: " << strerror(errno) << "\n"; 41 continue; 42 } 43 ++sent; 44 45 char buf[512]; 46 sockaddr_in from{}; 47 socklen_t flen = sizeof(from); 48 49 ssize_t n = recvfrom(sock, buf, sizeof(buf), 0, (sockaddr*)&from, &flen); 50 auto end = high_resolution_clock::now(); 51 double rtt = duration_cast<microseconds>(end - st).count() / 1000.0; 52 this_thread::sleep_for(milliseconds(400)); 53 if (n < 0) { 54 if (errno == EAGAIN || errno == EWOULDBLOCK) { 55 cout << "Request timeout for icmp_seq=" << seq << "\n"; 56 } else { 57 cerr << "recvfrom error: " << strerror(errno) << "\n"; 58 } 59 continue; 60 } 61 62 int ipLen = (buf[0] & 0x0F) * 4; 63 if (n < ipLen + (int)sizeof(ICMPPacket)) continue; 64 65 const ICMPPacket* rep = (const ICMPPacket*)(buf + ipLen); 66 if (!pkt.matches(rep)) continue; // not our packet 67 68 ++recv; 69 minRtt = min(minRtt, rtt); 70 maxRtt = max(maxRtt, rtt); 71 sumRtt += rtt; 72 73 cout << pkt.replyInfo(from.sin_addr, rtt) << "\n"; 74 } 75 76 if (sent) { 77 cout << "\n--- " << host << " ping statistics ---\n" 78 << sent << " packets transmitted, " << recv << " received, " 79 << (sent - recv) * 100 / sent << "% packet loss\n"; 80 if (recv) { 81 double avg = sumRtt / recv; 82 cout << "rtt min/avg/max = " << fixed << setprecision(3) 83 << minRtt << "/" << avg << "/" << maxRtt << " ms\n"; 84 } 85 } 86 close(sock); 87 return 0; 88}

let's run the code

the left terminal, as we can see the code resolved google to 124.250.193.238 then send an ICMP echo request, and we received replies each response line indicates ICMP echo request type (8,0) with 56 bytes of data.

TO-DO

As we can see, the code is working perfectly and produces accurate ICMP responses. However, there’s still room for improvement to make it more versatile and feature-rich. For instance, we can extend it to support IPv6, allowing the program to handle both IPv4 and IPv6 targets seamlessly depending on user input. Another useful enhancement would be implementing a traceroute feature where instead of just sending echo requests, the program would gradually increase the TTL (Time-To-Live) value to trace each hop along the network path to the destination. These upgrades will make the tool more powerful and will be the focus of the next blog.

Published 26 days ago