The project is a custom implementation of the ping utility, written in C. It is well-structured into three files:
ft_ping.h: The header file. It defines the necessary data structures, constants, and declares all the function prototypes. It acts as the central "contract" for the other files.
main.c: The core of the program. It handles initialization, the main execution loop, user interaction (via signals), and final cleanup.
helper.c: Contains utility and processing functions. This keeps the main logic in main.c clean and focuses on specific tasks like calculating checksums, processing replies, and printing statistics.
The program aims to send ICMP "Echo Request" packets to a specified host and listen for ICMP "Echo Reply" packets, calculating the round-trip time (RTT) and other statistics. File 1: ft_ping.h (The Blueprint)
This file sets up the entire structure of the program. Includes
It includes a comprehensive list of standard C and POSIX headers required for networking, signal handling, and I/O.
netinet/ip_icmp.h: Crucial for defining struct icmphdr.
sys/socket.h, netdb.h, arpa/inet.h: The core trio for all socket-based networking (creating sockets, DNS lookups, IP address manipulation).
signal.h: For handling signals like SIGINT (Ctrl+C).
sys/time.h: For gettimeofday to perform high-precision timing for RTT.
math.h: For sqrt to calculate the mean deviation of RTT.
Constants
PAYLOAD_SIZE 56: This is the standard payload size for ping. The total ICMP packet size will be this plus the size of the ICMP header (8 bytes).
PACKET_SIZE: Calculated as sizeof(struct icmphdr) + PAYLOAD_SIZE. This results in 64 bytes for the ICMP packet. When this is sent, the kernel will prepend an IP header (typically 20 bytes), which is why the initial output line in main mentions PACKET_SIZE + 20.
struct s_ping_stats (The Scorecard)
This structure is designed to hold all the statistical data gathered during the ping session.
packets_sent, packets_received: Simple counters.
rtt_min, rtt_max: To track the minimum and maximum round-trip times.
rtt_total: A running sum of all RTTs, used to calculate the average.
rtt_total_sq: A running sum of the square of each RTT. This is a clever way to calculate the standard deviation (or mean deviation, mdev) at the end without having to store every single RTT value. The formula for variance is E[X^2] - (E[X])^2.
start_time, end_time: To calculate the total duration of the ping session.
struct s_ping_data (The Main Data Holder)
This is the central data structure passed around the program. It aggregates all necessary state.
target_host: The original hostname or IP string provided by the user (e.g., "google.com").
addr_info: A pointer to a struct addrinfo returned by getaddrinfo. This contains all the necessary information for sendto to connect to the target.
resolved_ip: A character array to store the human-readable IP address (e.g., "142.250.184.78") after DNS lookup.
stats: An embedded instance of the t_ping_stats struct. This is a good design choice, keeping all statistics related to a ping session together with the target information.
Function Prototypes
This section declares all functions used in the project, ensuring type safety and allowing the compiler to know about functions defined in other files. File 2: main.c (The Engine)
This file contains the primary program flow, from setup to execution to teardown. Global Variable loop
volatile __sig_atomic_t loop = 1;
This is a global flag that controls the main ping_loop.
volatile: This keyword tells the compiler that the value of loop can be changed by something outside the normal flow of the program (in this case, a signal handler). This prevents the compiler from making optimizations that might assume the variable's value never changes inside the loop.
__sig_atomic_t: This is a special integer type that guarantees that reads and writes to it are "atomic." This means an operation on it will complete without being interrupted, which is critical when a variable is shared between a main program and a signal handler to prevent race conditions.
interrupt_signal(int sig)
This is the signal handler for SIGINT (generated by Ctrl+C).
Its only job is to set loop to 0, which will cause the while (loop) in ping_loop to terminate gracefully after its current iteration.
printf("\n"); is a nice touch to ensure the final statistics appear on a new line after the ^C is shown in the terminal.
DNS_LookUp(t_ping_data *pdata)
Purpose: To convert the user-provided hostname (e.g., "localhost") into a network-usable IP address.
getaddrinfo(): This is the modern, preferred way to do DNS lookups.
hints: The hints struct is used to filter the results. Here, it specifies that we only want an AF_INET (IPv4) address for a SOCK_RAW socket using the IPPROTO_ICMP protocol.
Error Handling: If getaddrinfo fails, it returns a non-zero status. gai_strerror(status) is used to get a human-readable error message.
Storing Results: If successful, the results are stored in pdata->addr_info.
inet_ntop(): This function converts the binary IP address from the result (addr_in->sin_addr) into a human-readable string (e.g., "127.0.0.1") and saves it in pdata->resolved_ip for display purposes.
initialize_socket(void)
Purpose: To create the raw socket needed for sending and receiving ICMP packets.
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP): This is the key call.
AF_INET: Specifies the IPv4 protocol family.
SOCK_RAW: A raw socket. This is necessary because we are building the ICMP packet ourselves, rather than letting the kernel do it. Using SOCK_RAW typically requires root privileges.
IPPROTO_ICMP: Specifies that we are interested in the ICMP protocol.
setsockopt(): This sets a timeout for receiving data (SO_RCVTIMEO). If a packet is sent and no reply comes back within 1 second, the recvfrom call will time out instead of blocking indefinitely. This is crucial for handling packet loss.
create_packet(char *packet, int seq)
Purpose: To construct a valid ICMP Echo Request packet in the packet buffer.
It overlays an icmphdr struct onto the start of the buffer.
ihdr->type = ICMP_ECHO;: Sets the type to Echo Request (ping).
ihdr->un.echo.id = htons(getpid());: The ID is set to the process ID. This helps distinguish replies intended for this program from other ping processes running on the same machine.
ihdr->un.echo.sequence = htons(seq);: The sequence number is incremented for each packet sent, allowing us to match replies with requests.
htons(): This function ("host to network short") is critical. It converts the ID and sequence numbers from the machine's byte order (host order) to network byte order (big-endian), which is required by network protocols.
Payload: The payload of the packet is filled with a struct timeval containing the current time from gettimeofday(). When the remote host replies, it will echo this data back. By comparing the echoed timestamp with the current time, we can calculate the RTT.
Checksum: The checksum field is first zeroed out, then the checksum() helper function is called on the entire packet, and the result is stored back in the header.
ping_loop(int sockfd, t_ping_data *pdata)
This is the main workhorse of the program.
It records the session's start_time.
The while(loop) structure continues as long as the signal handler hasn't set loop to 0.
Inside the loop:
create_packet(): A new packet is created with an incrementing sequence number.
sendto(): The packet is sent to the destination.
recvfrom(): The program waits for a reply. It will wait up to the 1-second timeout set in initialize_socket. This call also cleverly captures the source address of the reply into from_addr, which is important as the reply might not come from the exact same IP we sent to (e.g., in a network with anycast or load balancers).
process_reply(): If data is received, it's passed to this helper function for parsing and printing.
sleep(1): Pauses for 1 second before sending the next ping.
After the loop terminates, it records the end_time and calls print_summary.
main(int ac, char *av[])
The entry point of the program.
Performs basic argument checking.
Initializes the main t_ping_data struct, zeroing it out and setting rtt_min to a very large number (__DBL_MAX__) so that the first measured RTT will always be smaller.
Calls the initialization functions in order: DNS_LookUp and initialize_socket.
Sets up the signal handler with signal(SIGINT, interrupt_signal).
Prints the initial PING header, mimicking the standard ping utility.
Calls ping_loop() to start the main process.
Performs cleanup: close(sockfd) and freeaddrinfo(pdata.addr_info) to release the allocated network resources. This is very important to prevent resource leaks.
File 3: helper.c (The Toolbox)
This file contains functions that perform specific, isolated tasks, keeping main.c cleaner. checksum(void *b, int len)
This implements the standard "Internet Checksum" algorithm.
It works by treating the packet data as a sequence of 16-bit integers and summing them up using one's complement arithmetic.
The steps are:
Sum all 16-bit words.
If there's an odd byte left over, add it to the sum.
"Fold" the 32-bit sum into 16 bits by adding the upper 16 bits to the lower 16 bits repeatedly.
Take the one's complement of the final sum (~sum).
This is a classic, low-level networking routine.
print_summary(char *host, t_ping_stats *stats)
Purpose: To print the final statistics when the program exits.
It calculates packet loss percentage.
It calculates the total session time in milliseconds.
If any packets were received, it calculates the average RTT (rtt_avg) and the round-trip time mean deviation (rtt_mdev) using the pre-calculated rtt_total and rtt_total_sq.
The output is formatted to look just like the summary from the standard ping command.
perform_reverse_dns(struct sockaddr_storage *from_addr, ...)
Purpose: To get the hostname associated with the IP address of the machine that sent the reply.
It uses getnameinfo(), which is the counterpart to getaddrinfo(). It takes a socket address and resolves it to a hostname.
Fallback: If the reverse DNS lookup fails (which is common), it gracefully falls back to just printing the numeric IP address using inet_ntop. This makes the output robust.
process_reply(char *buffer, ssize_t len, ...)
Purpose: To parse a received network packet, validate it, extract the data, and print the per-packet output line.
The buffer from recvfrom contains a full IP packet, which encapsulates the ICMP packet.
IP Header: It first casts the buffer to a struct ip* to access the IP header.
ip_hdr->ip_hl * 4: It calculates the IP header's length. ip_hl is in 4-byte words, so we multiply by 4 to get the length in bytes. This is necessary to find where the ICMP data begins.
ICMP Header: The ICMP header is located immediately after the IP header.
Validation: It performs two crucial checks:
icmp_hdr->type == ICMP_ECHOREPLY: Is this an Echo Reply?
ntohs(icmp_hdr->un.echo.id) == getpid(): Is this reply for our ping process? ntohs ("network to host short") is used here to convert the ID back from network byte order.
RTT Calculation: If the packet is valid, it gets the current time (time_received) and reads the original send time from the ICMP payload. The difference is the RTT in milliseconds.
Statistics Update: It increments packets_received and updates all the rtt statistics.
Printing: It calls perform_reverse_dns to get the hostname of the replier, then prints the familiar line: 64 bytes from hostname (ip_address): icmp_seq=X ttl=Y time=Z ms.
Overall Flow Summary
main: Parse arguments.
DNS_LookUp: Resolve hostname to an IP address.
initialize_socket: Create a raw ICMP socket with a receive timeout.
signal: Set up a Ctrl+C handler.
ping_loop: Start the main loop.
create_packet: Build an ICMP Echo Request with a timestamp in the payload.
sendto: Send it.
recvfrom: Wait for a reply (or timeout).
process_reply: If a reply is received, parse it, calculate RTT, update stats, and print the result line.
sleep: Wait 1 second.
Repeat until Ctrl+C is pressed.
print_summary: After the loop breaks, calculate and print the final statistics.
main: Close the socket and free allocated memory. Exit.