Set timeout to unresponded hosts in ARP request with libpcap

762 views Asked by At

I made a simple arp request program in C. I generate a host list based on my IP and subnet mask and send arp request for each ip. In the end I gather the results and print the responded computers MAC addresses to screen.

Preface

I use libpcap implementation in C to handle the process of sending and receiving packets.

First I create a handler.

pcap_t pcapHandler;
if (!(pcapHandler = pcap_create(interfaceName, errorBuffer))) {
    error(errorBuffer, EXIT_FAILURE);
  }
  if ((pcap_set_snaplen(pcapHandler, 64)) < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }
  if ((pcap_set_promisc(pcapHandler, 1)) < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }
  if ((pcap_set_timeout(pcapHandler, 0)) < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }
  if ((pcap_activate(pcapHandler)) < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }

and then I attach a filter to receive only arp responses to my computer

  struct bpf_program filter;
  char *filter_string;
  filter_string = makeMessage("ether dst %.2x:%.2x:%.2x:%.2x:%.2x:%.2x and "
                              "(arp or (ether[14:4]=0xaaaa0300 and "
                              "ether[20:2]=0x0806) or (ether[12:2]=0x8100 "
                              "and ether[16:2]=0x0806) or "
                              "(ether[12:2]=0x8100 and "
                              "ether[18:4]=0xaaaa0300 and "
                              "ether[24:2]=0x0806))",
                              interface_mac[0], interface_mac[1],
                              interface_mac[2], interface_mac[3],
                              interface_mac[4], interface_mac[5]);

  if ((pcap_compile(pcapHandler, &filter, filter_string, 1, subnet_mask)) < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }
  if ((pcap_setfilter(pcapHandler, &filter)) < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }

In the end I use a user defined function to prepare the arp request. I simply call

 sent = pcap_sendpacket(pcapHandler, ARPBuffer, ARPBufferLength);

  if (sent < 0) {
    error(pcap_geterr(pcapHandler), EXIT_FAILURE);
  }

afterwards to send the packet.

My problem is in the stage of receiving the arp response (if any).

I use a for loop to iterate each host address and send it a arp packet.

HE *currHost; // list of my current host informations.
  for (currHost = hostListHead; currHost != NULL; currHost = currHost->next) {
    printf("Testing: %s\n", inet_ntoa(currHost->addr));

    // function that take the handler the current host details and my wireless interface I use to generate and send arp packages
    sendARPRequest(pcapHandler, currHost, workingWI);

    // dispatcher
    pcap_dispatch(pcapHandler, -1, callback, NULL);
  }

Then in the callback function I get the response and proceed it.

Problem

My loop stacks on each host. Let's say I send an arp request in some address that doesn't contain any computer. My program don't proceed to next host but waits to get a response. The program continue with another host only when I get a response from my router (why is this possible?)

Is there any way to set a timeout? So when I don't get any response for let's say 20 ms to proceed to next one? But I don't know even if I can proceed the packet when a client respond. Is something wrong with my implementation?

UPDATE

After Gil's proposed this is the new code I use, and again I have the same issues:

pcap_t *pcapHandler;

void alarm_handler() {
  pcap_breakloop(pcapHandler);
}

void startLocalMode(WI **workingWI) {
  HE *hostListHead;
  int numberOfHosts = generateHostList(&hostListHead, (*workingWI)->address, (*workingWI)->subnet_mask);

  
  preparePCAPHandler(&pcapHandler, (*workingWI)->name);
  setPCAPHandlerFilter(&pcapHandler, (*workingWI)->interface_mac, (*workingWI)->subnet_mask);


  HE *currHost;
  int i = 0;
  for (currHost = hostListHead; i < numberOfHosts; currHost = currHost->next) {
    sendARPRequest(pcapHandler, currHost, (*workingWI));
    ++i;
  }
  alarm(2);
  signal(SIGALRM, alarm_handler);
  pcap_loop(pcapHandler, 0, handleARPresponce, (u_char*)hostListHead);
}

This is the callback function:

#define ETHER_HDR_SIZE 14   /* Size of Ethernet frame header in bytes */
#define ARP_PKT_SIZE 28 /* Size of ARP Packet in bytes */

void handleARPresponce(u_char *args, const struct pcap_pkthdr *header, const u_char *packet_in) {
  int n = header->caplen;
  if (n < ETHER_HDR_SIZE + ARP_PKT_SIZE) {
    //printw("%d byte packet too short to decode\n", n);
    return;
  }

  arp_ether_ipv4 arpei;
  ether_hdr frame_hdr;
  struct in_addr source_ip;
  HE *temp_cursor;
  unsigned char extra_data[MAX_FRAME];
  size_t extra_data_len;
  int vlan_id;
  int framing;

  framing = unpackageARP(packet_in, n, &frame_hdr, &arpei, extra_data, &extra_data_len, &vlan_id);
  source_ip.s_addr = arpei.spa;

  temp_cursor = findHost((HE *)args, &source_ip);
  if (temp_cursor) {
    displayARPresponce(temp_cursor, &arpei, &frame_hdr);
  }
  return;
}

The callback function handles responses properlyc (as I said I get requests sent to my computer from my router). Now what's the deal with my code?

1

There are 1 answers

6
Gil Hamilton On BEST ANSWER

Your current implementation blocks forever because you're invoking pcap_dispatch with a cnt argument of -1 which essentially says loop until a "buffer full" of packets is received. I believe that will wait until at least one packet is received, so if you get no ARP response, it will wait forever.

You are already using pcap_set_timeout. You are currently setting the timeout to 0. My suspicion is that a value of 0 makes it an infinite wait. (The pcap(3) man page says to avoid a timeout value of 0. There are other caveats there about use of the timeout as well.)

If you wish to wait 20ms, then, you should provide a value of 20 as the second argument to pcap_set_timeout. You will probably then want to "manually" receive the next packet with pcap_next_ex (rather than pcap_dispatch which invokes a loop inside the pcap library). The return value from pcap_next_ex allows you to distinguish whether a packet was actually read or the timeout expired.

All that being said, I find the architecture brittle. I would restructure the code as follows:

  1. Make a list of all the hosts to which you wish to send an ARP. (You're already doing this so nothing additional really.)

  2. Send an ARP request to each host in the list.

  3. Set an external timeout (with alarm(2) say).

  4. Invoke pcap_loop to process incoming packets.

  5. For each ARP response received (in the callback function), find the appropriate host by scanning the list, and record the MAC address you got from the response.

  6. When the timeout expires, call pcap_breakloop (from the signal handler, say) to exit pcap_loop. (You might still need to call pcap_set_timeout in order to ensure that pcap_breakloop takes effect -- see the pcap_breakloop(3) manual page for discussion).

In effect, you're processing all of the hosts in parallel this way. At the end of this, you have a list of hosts, each with an associated MAC address if the ARP succeeded.

In fact, if this is to be robust, I would go one step further and run this for multiple iterations if necessary -- that is, re-send ARP requests to machines from which you didn't get a response -- in case a request or response is dropped somewhere in your network.

Psuedo-code:

alarm_handler()
{
  pcap_breakloop(pcap_handler)
}

callback(u_char *user, const struct pcap_pkthdr *hdr, const u_char *data)
{
  for (host in host_list) {
    if (host->IP == IP_from_ARP_packet(data)) {
      host->MAC = MAC_from_ARP_packet(data);
      host->MAC_is_known = true;
      break;
    }
  }
}

run_sweep(host_list)
{
  for (host in host_list) {
    if (!host->MAC_is_known)
      sendARPRequest(pcapHandler, host...);
  }
  alarm(SWEEP_TIMEOUT);
  signal(SIGALRM, alarm_handler);
  pcap_loop(pcapHandler, 0, callback, NULL);
}

/* Main code */
for (host in host_list)
  host->MAC_is_known = false;

sweeps = 0;
while (sweeps++ < MAX_SWEEPS && !all_hosts_MAC_known(host_list))
  run_sweep(host_list);