Learning Smart Contract Security @42born2code Security Track | Full-Stack Dev Building in Web3 🚀
Share Dialog
Share Dialog
Learning Smart Contract Security @42born2code Security Track | Full-Stack Dev Building in Web3 🚀

Subscribe to Gapple

Subscribe to Gapple
A deep dive into network programming, ICMP packets, and the challenges of recreating a fundamental Unix utility from scratch.
When I first saw the ft_ping project specification, I thought: "How hard could it be? It's just sending packets and measuring response time."
Spoiler alert: I was very wrong.
Recreating the ping command meant diving deep into:
Raw socket programming
ICMP protocol implementation
Network packet parsing
Signal handling and timeouts
Cross-platform networking quirks
Before jumping into code, I had to understand what ping actually does under the hood:
Creates a raw ICMP socket (requires root privileges)
Builds ICMP Echo Request packets with sequence numbers and timestamps
Sends packets to the target host at regular intervals
Listens for responses (Echo Reply, Destination Unreachable, etc.)
Calculates round-trip time and maintains statistics
Handles edge cases like timeouts, localhost, and packet loss
Sounds simple? Let me show you why it's not.
// Creating a raw ICMP socket (Linux/Unix only)
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0) {
perror("socket"); // Needs root privileges!
exit(1);
}
Raw sockets require root access because they bypass the OS network stack. This means I had to manually construct every part of the ICMP packet:
struct icmphdr *icmp = (struct icmphdr *)buffer;
icmp->type = ICMP_ECHO; // Echo Request
icmp->code = 0; // Always 0 for ping
icmp->un.echo.id = getpid(); // Process ID
icmp->un.echo.sequence = seq++; // Sequence number
icmp->checksum = calculate_checksum(icmp, packet_size);
ICMP packets require a valid checksum or they get dropped. The algorithm is deceptively simple but tricky to implement correctly:
uint16_t checksum(void *data, int len) {
uint32_t sum = 0;
uint16_t *ptr = data;
// Sum all 16-bit words
while (len > 1) {
sum += *ptr++;
len -= 2;
}
// Handle odd byte
if (len == 1)
sum += *(uint8_t *)ptr;
// Fold 32-bit sum to 16 bits
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
return ~sum; // One's complement
}
One wrong bit and your packets disappear into the void.
When you receive data from a raw socket, you get the entire IP packet, not just the ICMP payload:
// Raw packet = IP Header + ICMP Header + Data
struct ip *ip_header = (struct ip *)buffer;
int ip_len = ip_header->ip_hl * 4; // Header length in bytes
// Skip to ICMP part
struct icmphdr *icmp = (struct icmphdr *)(buffer + ip_len);
The IP header length is variable (20-60 bytes), so you have to parse it correctly to find your ICMP data.
Here's something that stumped me for hours: when pinging localhost, you receive your own Echo Request packets in addition to the Echo Replies.
Why? Because localhost routes through the loopback interface, and raw sockets see everything.
The solution:
// Filter out our own Echo Requests
if (icmp->type == ICMP_ECHO && ntohs(icmp->un.echo.id) == our_pid) {
continue; // Ignore and wait for the next packet
}
Implementing proper timeouts was trickier than expected. The naive approach using setsockopt(SO_RCVTIMEO) works, but error handling becomes messy:
// Set socket timeout
struct timeval timeout = {timeout_sec, 0};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
ssize_t bytes = recvmsg(sockfd, &msg, 0);
if (bytes < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Timeout - this is expected, not an error
return -1;
}
perror("recvmsg"); // Real error
return -1;
}
Getting the statistics right required careful RTT (Round-Trip Time) tracking:
// Calculate RTT in milliseconds
double rtt = (recv_time.tv_sec - send_time.tv_sec) * 1000.0 +
(recv_time.tv_usec - send_time.tv_usec) / 1000.0;
// Track min/max/average
if (rtt_min < 0 || rtt < rtt_min) rtt_min = rtt;
if (rtt > rtt_max) rtt_max = rtt;
rtt_sum += rtt;
// Standard deviation calculation
rtt_sum_squares += rtt * rtt;
double variance = (rtt_sum_squares / count) - (average * average);
double mdev = sqrt(variance);
Network programming is humbling - So many edge cases you never think about
Raw sockets are powerful but dangerous - One wrong packet can crash things
Testing is everything - Localhost, remote hosts, timeouts, errors - each behaves differently
The devil is in the details - Byte order, padding, alignment all matter
Root privileges exist for a reason - Raw socket access is serious business
After debugging timeout issues, fixing localhost packet filtering, and ensuring byte-perfect compatibility with the original ping, my ft_ping now:
✅ Handles IPv4 addresses and hostnames
✅ Implements all major ping options (-c, -i, -W, -s, -v, -n)
✅ Provides accurate RTT measurements (±30ms tolerance)
✅ Matches original ping output format
✅ Gracefully handles errors and edge cases
✅ Works with localhost, remote hosts, and unreachable destinations
Start simple - Get basic packet sending/receiving working first
Test extensively - Network code has infinite edge cases
Read the RFCs - ICMP specification is your friend
Use packet capture tools - Wireshark saved me countless times
Handle privileges carefully - Raw sockets are not a toy
The complete implementation is available on my GitHub, featuring:
Clean, modular C code
Comprehensive error handling
Detailed comments explaining the tricky parts
Full test suite covering edge cases
Building a fundamental network tool from scratch taught me more about networking in a few weeks than years of high-level development. Sometimes the best way to understand something is to rebuild it yourself.
Have you ever rebuilt a classic Unix tool? What did you learn? Share your experiences in the comments!
Tags: #NetworkProgramming #C #Unix #SystemsProgramming #ICMP #Sockets
A deep dive into network programming, ICMP packets, and the challenges of recreating a fundamental Unix utility from scratch.
When I first saw the ft_ping project specification, I thought: "How hard could it be? It's just sending packets and measuring response time."
Spoiler alert: I was very wrong.
Recreating the ping command meant diving deep into:
Raw socket programming
ICMP protocol implementation
Network packet parsing
Signal handling and timeouts
Cross-platform networking quirks
Before jumping into code, I had to understand what ping actually does under the hood:
Creates a raw ICMP socket (requires root privileges)
Builds ICMP Echo Request packets with sequence numbers and timestamps
Sends packets to the target host at regular intervals
Listens for responses (Echo Reply, Destination Unreachable, etc.)
Calculates round-trip time and maintains statistics
Handles edge cases like timeouts, localhost, and packet loss
Sounds simple? Let me show you why it's not.
// Creating a raw ICMP socket (Linux/Unix only)
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0) {
perror("socket"); // Needs root privileges!
exit(1);
}
Raw sockets require root access because they bypass the OS network stack. This means I had to manually construct every part of the ICMP packet:
struct icmphdr *icmp = (struct icmphdr *)buffer;
icmp->type = ICMP_ECHO; // Echo Request
icmp->code = 0; // Always 0 for ping
icmp->un.echo.id = getpid(); // Process ID
icmp->un.echo.sequence = seq++; // Sequence number
icmp->checksum = calculate_checksum(icmp, packet_size);
ICMP packets require a valid checksum or they get dropped. The algorithm is deceptively simple but tricky to implement correctly:
uint16_t checksum(void *data, int len) {
uint32_t sum = 0;
uint16_t *ptr = data;
// Sum all 16-bit words
while (len > 1) {
sum += *ptr++;
len -= 2;
}
// Handle odd byte
if (len == 1)
sum += *(uint8_t *)ptr;
// Fold 32-bit sum to 16 bits
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
return ~sum; // One's complement
}
One wrong bit and your packets disappear into the void.
When you receive data from a raw socket, you get the entire IP packet, not just the ICMP payload:
// Raw packet = IP Header + ICMP Header + Data
struct ip *ip_header = (struct ip *)buffer;
int ip_len = ip_header->ip_hl * 4; // Header length in bytes
// Skip to ICMP part
struct icmphdr *icmp = (struct icmphdr *)(buffer + ip_len);
The IP header length is variable (20-60 bytes), so you have to parse it correctly to find your ICMP data.
Here's something that stumped me for hours: when pinging localhost, you receive your own Echo Request packets in addition to the Echo Replies.
Why? Because localhost routes through the loopback interface, and raw sockets see everything.
The solution:
// Filter out our own Echo Requests
if (icmp->type == ICMP_ECHO && ntohs(icmp->un.echo.id) == our_pid) {
continue; // Ignore and wait for the next packet
}
Implementing proper timeouts was trickier than expected. The naive approach using setsockopt(SO_RCVTIMEO) works, but error handling becomes messy:
// Set socket timeout
struct timeval timeout = {timeout_sec, 0};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
ssize_t bytes = recvmsg(sockfd, &msg, 0);
if (bytes < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Timeout - this is expected, not an error
return -1;
}
perror("recvmsg"); // Real error
return -1;
}
Getting the statistics right required careful RTT (Round-Trip Time) tracking:
// Calculate RTT in milliseconds
double rtt = (recv_time.tv_sec - send_time.tv_sec) * 1000.0 +
(recv_time.tv_usec - send_time.tv_usec) / 1000.0;
// Track min/max/average
if (rtt_min < 0 || rtt < rtt_min) rtt_min = rtt;
if (rtt > rtt_max) rtt_max = rtt;
rtt_sum += rtt;
// Standard deviation calculation
rtt_sum_squares += rtt * rtt;
double variance = (rtt_sum_squares / count) - (average * average);
double mdev = sqrt(variance);
Network programming is humbling - So many edge cases you never think about
Raw sockets are powerful but dangerous - One wrong packet can crash things
Testing is everything - Localhost, remote hosts, timeouts, errors - each behaves differently
The devil is in the details - Byte order, padding, alignment all matter
Root privileges exist for a reason - Raw socket access is serious business
After debugging timeout issues, fixing localhost packet filtering, and ensuring byte-perfect compatibility with the original ping, my ft_ping now:
✅ Handles IPv4 addresses and hostnames
✅ Implements all major ping options (-c, -i, -W, -s, -v, -n)
✅ Provides accurate RTT measurements (±30ms tolerance)
✅ Matches original ping output format
✅ Gracefully handles errors and edge cases
✅ Works with localhost, remote hosts, and unreachable destinations
Start simple - Get basic packet sending/receiving working first
Test extensively - Network code has infinite edge cases
Read the RFCs - ICMP specification is your friend
Use packet capture tools - Wireshark saved me countless times
Handle privileges carefully - Raw sockets are not a toy
The complete implementation is available on my GitHub, featuring:
Clean, modular C code
Comprehensive error handling
Detailed comments explaining the tricky parts
Full test suite covering edge cases
Building a fundamental network tool from scratch taught me more about networking in a few weeks than years of high-level development. Sometimes the best way to understand something is to rebuild it yourself.
Have you ever rebuilt a classic Unix tool? What did you learn? Share your experiences in the comments!
Tags: #NetworkProgramming #C #Unix #SystemsProgramming #ICMP #Sockets
<100 subscribers
<100 subscribers
No activity yet