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...

# ./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.

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.

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.

The Full Code

The full code is at ping.c.


site built using mf technology