commit 4772a7dd2a15c69cd41644c5d105a4fb82ebeaeb Author: rmanach Date: Fri Aug 29 12:07:27 2025 +0200 init repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01df6dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode + +sping \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7311e4c --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +HOST := google.com + +format: + @astyle --style=java --recursive *.c,*.h -n || true + +build: format + @gcc -o sping sping.c -Wall -lm + +run: build + @sudo ./sping $(HOST) -c 6 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ff8ccd --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# sping + +A simple **ping** implementation to briefly details how it works under the hood. + +## Overview + +The **Internet Control Message Protocol (ICMP)** is a network layer protocol in the IP suite used for diagnostics, error reporting, and network management. It supports tools like `ping` by sending messages to test connectivity and measure round-trip time. + +How ICMP Works ? +* **Purpose**: Facilitates error reporting (e.g., "destination unreachable") and diagnostic queries (e.g., Echo Request/Reply for ping). +* **Operation**: ICMP packets are encapsulated in IP packets (IPv4/IPv6) and sent between hosts or routers. No TCP/UDP is used. +* **Key Messages**: + * Echo Request (Type 8): Sent by ping to test if a host is reachable. + * Echo Reply (Type 0): Response from the target confirming reachability. + * Other types: Destination Unreachable (Type 3), Time Exceeded (Type 11). + +Below, the ICMP packet structure: +```ascii ++-------------------------------+ +| Ethernet Frame (optional) | +|-------------------------------| +| Destination MAC (6 bytes) | +| Source MAC (6 bytes) | +| EtherType (2 bytes) = 0x0800 | ++-------------------------------+ +| IPv4 Header (20 bytes) | +|-------------------------------| +| Version (4b) = 4 | +| Header Len (4b) = 5 | +| Type of Service (8b) = 0 | +| Total Length (16b) = 84 | +| Identification (16b) | +| Flags/Offset (16b) = 0 | +| TTL (8b) = e.g., 64 | +| Protocol (8b) = 1 (ICMP) | +| Checksum (16b) | +| Source IP (32b) | +| Destination IP (32b) | ++-------------------------------+ +| ICMP Packet (64 bytes def) | +|-------------------------------| +| Type (8b) = 8 / 0 | +| Code (8b) = 0 | +| Checksum (16b) | +| Identifier (16b) | +| Sequence Number (16b) | +| Payload (56 bytes, e.g., 0s) | ++-------------------------------+ +| Ethernet FCS (4 bytes) | ++-------------------------------+ +``` + + +## Build +Of course a C compiler must be installed. +```bash +make build +``` + +## Run +Run the build and launch the program, you must have the superuser rights to run it (raw socket access). +```bash +make run +``` +By default, packets are sent to [google.com](google.com), if you want to change the destination update **HOST** variable: +```bash +make run HOST=anotherhost.com +``` \ No newline at end of file diff --git a/sping.c b/sping.c new file mode 100644 index 0000000..ea0c188 --- /dev/null +++ b/sping.c @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Parse a string into an integer. +// Returns 0 if it fails. +int parse_int(char *s, int *result) { + if (s == NULL || result == NULL) + return 0; + return sscanf(s, "%d", result) == 1; +} + +// Compute ICMP checksum. +// It uses a standard 16-bit one's complement sum algorithm. +unsigned short checksum(void *b, int len) { + // points to the first 2-byte length of b + unsigned short *buf = b; + + // set sum to zero + unsigned int sum = 0; + + // split the packet into 2-byte chunks and sum it + for (sum = 0; len > 1; len -= 2) + sum += *buf++; + + // if length is odd, last byte is treated as a 16-bit word by padding it with a zero + if (len > 0) + sum += *(unsigned char *)buf; + + // any bits beyond 16 bits are shift and added to the lower 16 bits + sum = (sum >> 16) + (sum & 0xFFFF); + sum += (sum >> 16); + + return (unsigned short) ~sum; +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + printf("usage: %s \n", argv[0]); + return 1; + } + + // parse packets number optional arg + char **args = argv; + int count = 4; // default packet number to send + for(int argpos = 1; argpos < argc; argpos++) { + if (!strcmp(*args++, "-c")) { + if (!parse_int(*args, &count)) { + printf("unable to parse ping count number\n"); + printf("usage: -c \n"); + return 1; + } + continue; + } + args++; + } + + // DNS resolution + struct addrinfo hints, *res; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_RAW; + hints.ai_protocol = IPPROTO_ICMP; + + int status = getaddrinfo(argv[1], NULL, &hints, &res); + if (status != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status)); + return 1; + } + + // extract IP address + char ip_str[INET_ADDRSTRLEN]; + struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr; + inet_ntop(AF_INET, &(addr->sin_addr), ip_str, INET_ADDRSTRLEN); + printf("PING %s (%s): 56 data bytes\n", argv[1], ip_str); + + // create raw socket for ICMP + int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + if (sock < 0) { + perror("socket"); + freeaddrinfo(res); + return 1; + } + + // set destination address + struct sockaddr_in dest = *addr; + + // ICMP packet (header + 56-byte payload) + char packet[64]; + struct icmp *icmp = (struct icmp *)packet; + icmp->icmp_type = ICMP_ECHO; // type 8 (Echo Request) + icmp->icmp_code = 0; + icmp->icmp_id = getpid() & 0xFFFF; // use process ID as identifier fitting into 16 bits + icmp->icmp_seq = 0; + icmp->icmp_cksum = 0; + memset(packet + 8, 0, 56); // zero payload + + struct timeval timeout; + timeout.tv_sec = 2; + timeout.tv_usec = 0; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + int sent = 0, received = 0; + double min_rtt = 1e9, max_rtt = 0, sum_rtt = 0, sum_sq_rtt = 0; + + for (int i = 0; i < count; i++) { + icmp->icmp_seq = i + 1; + icmp->icmp_cksum = 0; + icmp->icmp_cksum = checksum(packet, 64); + + // record send time + struct timeval send_time, recv_time; + gettimeofday(&send_time, NULL); + sent++; + + // send ICMP packet + if (sendto(sock, packet, 64, 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) { + perror("sendto"); + continue; + } + + // receive reply + char recv_buf[128]; + struct sockaddr_in from; + socklen_t from_len = sizeof(from); + int n = recvfrom(sock, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&from, &from_len); + gettimeofday(&recv_time, NULL); + + if (n < 0) { + printf("Request timed out\n"); + continue; + } + + // extract IP and ICMP headers + struct iphdr *ip = (struct iphdr *)recv_buf; + struct icmp *recv_icmp = (struct icmp *)(recv_buf + (ip->ihl << 2)); + + // verify reply + if (n >= (int)(ip->ihl << 2) + 8 && recv_icmp->icmp_type == ICMP_ECHOREPLY && + recv_icmp->icmp_id == icmp->icmp_id && recv_icmp->icmp_seq == icmp->icmp_seq) { + + double rtt = (recv_time.tv_sec - send_time.tv_sec) * 1000.0 + + (recv_time.tv_usec - send_time.tv_usec) / 1000.0; + received++; + + sum_rtt += rtt; + sum_sq_rtt += rtt * rtt; + + if (rtt < min_rtt) min_rtt = rtt; + if (rtt > max_rtt) max_rtt = rtt; + + printf("%d bytes from %s: icmp_seq=%d ttl=%d time=%.1f ms\n", + n - (ip->ihl << 2), ip_str, recv_icmp->icmp_seq, ip->ttl, rtt); + } else { + printf("Invalid reply\n"); + } + + sleep(1); + } + + printf("\n--- %s ping statistics ---\n", argv[1]); + printf("%d packets transmitted, %d received, %.0f%% packet loss\n", + sent, received, (sent - received) * 100.0 / sent); + + // see for stddev computation: https://en.wikipedia.org/wiki/Standard_deviation + if (received > 0) { + double avg_rtt = sum_rtt / received; + double stddev = received > 1 ? sqrt((sum_sq_rtt - sum_rtt * sum_rtt / received) / (received - 1)) : 0; + + printf("rtt min/avg/max/stddev = %.3f/%.3f/%.3f/%.3f ms\n", + min_rtt, avg_rtt, max_rtt, stddev); + } + + close(sock); + freeaddrinfo(res); + return 0; +} \ No newline at end of file