Write Your Own Ping
published: [nandalism home] (dark light)
What is ping
?
ping
is a unix command which is used as a basic network connectivity check, when troubleshooting network problems.
You give it an ip address and it sends a message there and waits for a reply. If a reply comes, your network connection is working.
In practice, actual ping tools take a lot of options and are quite complicated internally.
$ ping example.com PING example.com (93.184.216.34): 56 data bytes 64 bytes from 93.184.216.34: seq=0 ttl=42 time=12.915 ms ...
Implementing Our Own Version Of ping
Here I will show how to write a simple ping
tool which performs the basic functionality. The code shown is in C
and compiles on alpine linux (which uses muslibc not gnu glibc). The restrictions are that this ping...
- will only work for IPv4
- requires the ip address as a numeric dotted-quad e.g.
93.184.216.34
- will only send one ping packet, it may be dropped or lost on the way, and you will not receive a pingback (real ping tools loop, trying again periodically)
- must be run as root (real ping tools use suid sticky bit or capabilities to avoid this)
- will show the incoming reply size, from where the reply came and how long the round trip took
# ./ping 93.184.216.34 48 bytes from from (93.184.216.34) elapsed time 36.920us
Creating A RAW Socket
ping
needs a socket, a raw
socket, to send ICMP control packet. These can only be created by root
(or using capabilities)
and this is why we must run this ping as root.
static int create_icmp_sock(void){ enum{icmp_protocol=1}; int sock = socket(AF_INET, SOCK_RAW, icmp_protocol); if(sock<1) diesys("socket"); int sockopt=1; if(setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &sockopt, sizeof(sockopt))) die("setsockopt SO_BROADCAST"); return sock; }
This code creates a AF_INET (ipv4) socket but the unusual part is that we pass SOCK_RAW
. We need this so that we can send ICMP level packets.
We also need, minimally, that the socket is a broadcast socket.
- create a RAW socket (protocol ICMP - see /etc/protocols)
- set the broadcast option on it
Send The Ping Packet
static void send_ping(int sock){ enum{noflags=0}; struct icmp icmp; memset(&icmp, 0xab, sizeof(icmp)); icmp.icmp_type = ICMP_ECHO; icmp.icmp_code = 0; icmp.icmp_cksum = 0; // must=zero for checksum calc icmp.icmp_seq = htons(ping.seqno); icmp.icmp_id = ping.myid = getpid(); ++ping.seqno; uint64_t tus = nowus(); memcpy(icmp.icmp_data, &tus, sizeof(tus)); // the icmp_dun{} data union is big enough for the timestamp size_t const size_pkt = sizeof(struct icmp); icmp.icmp_cksum = inet_cksum(&icmp, size_pkt); ssize_t sz = sendto(sock, &icmp, size_pkt, noflags, (struct sockaddr*)&ping.pingaddr, sizeof(ping.pingaddr)); if(size_pkt != sz) diesys("sendto"); }
We create an ICMP packet with a uint64_t extra data slot. This contains a timestamp and we will use this when we receive the echo, to calculate the round trip elapsed time. Note: we could keep the time stamp in a global since this ping only sends a single packet, however real ping tools send multiple packets, and since packets may be lost it's easier to include the time and have it echoed back to us.
Similarly the seqno is superfluous, we could just set zero, since we only send one packet, but this is more easily extensible.
- create an ICMP struct and fill it with the ICMP_ECHO type, a unique id (our pid) and a checksum
- we also add the current time in microseconds (the icmp_data field is at the very end of the icmp structure). The timestamp will be echoed back to us and we can calculate a round trip time
True to its name the echo packet will be returned to us with all the data it contained (i.e. the timestamp we sent).
Await the Response
static void await_pingback(int sock){ enum{noflags=0}; struct sockaddr_in from; socklen_t fromlen = (socklen_t)sizeof(from); byte pkt[sizeof(struct iphdr) + sizeof(struct icmp)]; for(;;){ int const sz = recvfrom(sock, pkt, sizeof(pkt), noflags, (struct sockaddr *)&from, &fromlen); if(sz<0){ if(EINTR==errno) continue; diesys("recvfrom"); } if(sz < (int)(sizeof(struct icmp))){ warn("received packet is too small size:%d\n", sz); continue; } struct iphdr *iphdr = (struct iphdr *)pkt; struct icmp *icmp = (struct icmp *)(pkt + (iphdr->ihl * 4)); if(icmp->icmp_id != ping.myid){ warn("not our ping id:%d\n", icmp->icmp_id); continue; } switch(icmp->icmp_type){ case ICMP_ECHOREPLY: show(&from, sz, icmp); return; break; case ICMP_ECHO: /*ignore, echo from us*/; break; default: warn("received not ICMP_ECHOREPLY/ICMP_ECHO type:icmp->icmp_type:%d\n", icmp->icmp_type); break; } } }
First we create space to contain the incoming packet and the address from which we received it. Then we loop calling recvfrom
.
We keep looping until we receive our expected ICMP_ECHOREPLY. It may never come if the network is unreachable or if our single packet got lost.
The real ping sets timeouts and retries. We don't.
If we receive an ICMP_ECHOREPLY packet, we print the address it came from and time it took (full code linked below).
If you ping 127.0.0.1
(localhost) you will receive the broadcast of your own ICMP_ECHO packet. We ignore that but warn
about receiving any other packets which match the id of the one we sent but are neither ICMP_ECHOREPLY nor ICMP_ECHO.
- create space for the packet and the remote ip address
- loop calling
recvfrom
to wait for incoming packets on our raw socket - check the incoming packet size and, if large enough, extract the
id
field; it must match the id we sent or we skip it - if the id matches ours, then we check if its type is ICMP_ECHOREPLY, now we can extract the timestamp, compute the roundtrip time, display the information and exit
The Full Code
The full code is at ping.c.