Barr Group FacebookBarr Group TwitterBarr Group LinkedInBarr Group Vimeo

 Printable PDF

The address resolution protocol provides a necessary bridge between physical and logical addresses on a TCP/IP network.

Every system on a TCP/IP network has two addresses, one physical and one logical. The address resolution protocol (ARP) provides a necessary bridge between these two addresses.

TCP protocol support is not always required in embedded uses of Internet technologies. I don't have to convince you that there's no reason to go to the trouble and expense of including software in ROM that your application doesn't actually require. A UDP/IP stack was sufficient for my own use of the Internet protocols to keep a satellite gateway's firmware up to date and send logging messages to a control and monitoring station.

A network protocol stack is internally dependent upon the API of the underlying operating system and network device driver, but otherwise separate from those pieces of software. It is, in effect, middleware. The ARP protocol is just one component of a TCP/IP or UDP/IP stack.

Address lookups

There are unique hardware MAC addresses associated with each device connected to a physical network like Ethernet. These addresses must be globally unique; no two Ethernet-connected systems may have the same 6-byte hardware address. It turns out that every system on an IP network also has a second address. This logical address is called, as you might expect, the "IP address."

The best analogy I can draw to this two-address phenomenon is that of a toll-free phone number, like the 800- and 888-prefixed numbers in the U.S. Although each toll-free number is unique and can be used to contact a person at a particular physical location, the number itself does not convey any direct information about what the actual phone number is. For a while I had a toll-free number of my own; calls to that number were automatically redirected to the phone in my office. My office phone had an area code, exchange, and four more digits just like any other; my 888-number had nothing in common with it. You can't tell from a toll-free number if it will reach the phone on your desk or one on the other side of the country.

In much the same way, each node on the Internet (or an intranet) has two addresses: one physical and one logical. Neither of these addresses contains any information about the other. And yet, unlike the phone number example, you need both addresses to communicate with a given system. Usually, your application knows just one address-the IP address-of the remote system. But no network packets can be sent to the remote system without the hardware address as well.

ARP is the Internet's lookup service. Given an IP address (toll-free number), ARP can obtain the hardware address (actual area code and phone number) to which network packets should be sent on the physical network. Similarly, a related reverse-lookup service called RARP can obtain the IP address of a machine given only its hardware address. ARP is used by every machine on the Internet; the use of RARP is more limited.

ARP and away

Before we go on, I need to let you know that I'm restricting the remainder of this discussion to simple networks where all of the connected systems are on the same physical network. (In other words, I'm not going to tell you how the hardware addresses obtained with the help of ARP are sometimes white lies, used in conjunction with nodes on the network called bridges, switches, and routers to direct your packets across physical network boundaries. Those details really aren't important to you anyway-unless the embedded system you are building is itself a bridge, switch, or router-since these white lies don't affect your ARP implementation. Now just pretend like I didn't open up a whole can of worms of routing issues you hadn't thought about before and let's carry on.)

As specified in RFC 826 (way back in the early days of the Reagan administration), ARP is a general-purpose protocol that can be used to map any type of hardware address to any type of protocol address. However, for most practical purposes, all anyone really cares about using ARP for, these days, is converting the IP address of a remote machine into an Ethernet address. It's the device driver for the Ethernet controller that needs this information. Every time the IP layer passes the network driver a packet to send over the Ethernet, it needs to figure out what Ethernet address, specifically, to send the packet to. So ARP will be at the very bottom of our UDP/IP stack, residing below the IP layer but above the network driver.

ARP Request and Reply

Figure 1. How an ARP request works

Figure 1 shows how the ARP protocol works. In short, the system that needs a hardware address sends an ARP request message out onto the network. Since the sender doesn't know the hardware address of the system it's looking for, this message is broadcast to all systems on the physical network. (On Ethernet, address FF:FF:FF:FF:FF:FF is reserved for broadcast messages.) Included within the ARP request is the IP address (also known as, protocol address) of the target system and both of the sender's addresses. Each system that receives the broadcast ARP request checks to see if its local IP address matches the target protocol address in the ARP request. The one system with that IP address sends an ARP reply directly to the requester. Normal UDP/IP communication can begin only after the requester receives the ARP reply.

Preliminaries

Before I can show my ARP implementation code, we need to discuss a few preliminaries. The first of these is that I'm following the source code conventions of the µC/OS-II real-time operating system. One thing you'll notice as a direct result of this is that my code uses the set of portable, compiler-independent data types shown in Table 1. These types will be used instead of char, short, int, long, and their unsigned counterparts, whenever the size of a field is dictated by a network protocol.

Data Type
Description
INT8U
An unsigned 8-bit integer
INT8S
A signed 8-bit integer
INT16U
An unsigned 16-bit integer
INT16S
A signed 16-bit integer
INT32U
An unsigned 32-bit integer
INT32S
A signed 32-bit integer

Table 1. Portable integer data types

So, for example, the definition of an ARP packet looks like this in my implementation:

typedef struct 
{ 
    INT16U  hw_type; 
    INT16U  prot_type;                  
    INT8U   hw_len; 
    INT8U   prot_len; 
    INT16U  operation; 

} NetArpHdr;
                  
typedef struct 
{ 
    NetArpHdr  arpHdr; 
    INT8U      sender_hw_addr[HW_ADDR_LEN]; 
    INT32U     sender_ip_addr; 
    INT8U      target_hw_addr [HW_ADDR_LEN];
    INT32U     target_ip_addr; 

} NetArpPkt;

The advantage of this should be clear enough. Each field will have the correct size (in bytes and sign) on any platform, provided the fellow doing the port of the protocol stack remembers to redefine the six basic types in Table 1 to match the compiler and underlying hardware. So, for example, a port to an 80186 processor would include the definitions:

typedef unsigned char INT8U;
typedef unsigned short INT16U;
typedef unsigned long INT32U;

My choice of µC/OS-II as RTOS also affects my naming convention. Except for well-known names I want to mimic, all of my function and data structure names will begin with the prefix Net. That prefix will be followed by the module name--for example, Arp--and then a name descriptive of the function itself.

The second thing we need to talk about is the format of IP addresses. You might, for example, refer to your personal workstation by a dotted-decimal number such as 207.221.32.136. This is your workstation's IP address, but in a human-readable string format. For reasons you can easily understand, the protocol stack doesn't much like to deal with strings. Rather, it prefers to deal with numbers. Therefore, the protocol stack treats your IP address as a big-endian 4-byte unsigned integer. (The preceding string IP address would be treated as 0x8820DDCF.)

Just as string addresses are hard for the protocol stack to manipulate, big-endian 32-bit integers are hard for people (even programmers) to interpret. So one of the first things I did on this project was to write a pair of functions for converting string IP addresses to 32-bit integers and vice versa. Following a naming convention with which I was familiar I called these inet_addr() and inet_ntoa(), respectively. Their prototypes are as follows:

INT32U inet_addr(char const * str);
void inet_ntoa(INT32U ip_addr, char * buf);

The only thing to be aware of when using these is that the second function requires the caller to reserve 16-bytes of space for buf in advance of the call. That is the maximum length of a dotted-decimal IP address, including the null terminator for the string.

ARP implementation code

With those preliminaries out of the way, we can now begin to look at the code within the ARP module. It should be obvious from Figure 1 that there will be at least two functions: one for the sending of ARP requests and replies and another for receiving these packets and processing them. I've called these functions NetArpSnd() and NetArpRcv(), respectively.

The implementation of NetArpSnd() is shown in Listing 1. This function would typically be called by the IP module above or the network driver below, after it had been determined that the hardware address of the target system was not known. In that case, the call might look something like this:

NetArpSnd(ARP_REQUEST, broadcast, inet_addr("192.168.1.1"));       

In addition, NetArpSnd() may also be called by the NetArpRcv() function, which we'll see next, in order to send an ARP reply to a remote system that requested the hardware address of the local system. In that case, the first parameter will be ARP_REPLY and the second and third parameters will be the local_hw_addr() and the local_ip_addr(), respectively.

#define ETHERNET 1 

#define PROTO_IP  0x800 
#define PROTO_ARP 0x806 

#define HW_ADDR_LEN 6 
#define IP_ADDR_LEN 4 

#define ARP_REQUEST 1 
#define ARP_REPLY   2 

#define ARP_PACKET_LEN sizeof(NetArpPkt) 


const INT8U broadcast[HW_ADDR_LEN] = 
    { 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF }; 


int 
NetArpSnd(INT16U operation, INT8U * target_hw_addr, 
    INT32U target_ip_addr) 
{ 
    NetArpPkt * pArpPkt; 


    /*
     * Allocate a network buffer. 
     */ 
    pArpPkt = 
        (NetArpPkt *) NETMemGet(NET_ARP, ARP_PACKET_LEN); 

    /*
     * Fill in the ARP header fields.
     */ 
    pArpPkt->arpHdr.operation = htons(operation); 
    pArpPkt->arpHdr.hw_type   = htons(ETHERNET); 
    pArpPkt->arpHdr.prot_type = htons(PROTO_IP); 
    pArpPkt->arpHdr.hw_len    = HW_ADDR_LEN; 
    pArpPkt->arpHdr.prot_len  = IP_ADDR_LEN; 

    /*
     * Fill in the address fields.
     */ 
    memcpy(pArpPkt->target_hw_addr, target_hw_addr, 
        HW_ADDR_LEN); 
    pArpPkt->target_ip_addr = target_ip_addr; 
    memcpy(pArpPkt->sender_hw_addr, local_hw_addr(), 
        HW_ADDR_LEN); 
    pArpPkt->sender_ip_addr = local_ip_addr(); 
    
    /*
     * Broadcast the request over the network.
     */ 
    return (NetPhySnd(broadcast, htons(PROTO_ARP),
	(unsigned char *) pArpPkt, ARP_PACKET_LEN)); 

}   /* NetArpSnd() */

Listing 1. A function to send ARP request and reply packets

The implementation of NetArpRcv() is shown in Listing 2. This implementation conforms to the recommendation, in RFC 826, that any valid ARP packet directed to a local system, whether it be an ARP request or reply, be examined for useful hardware addresses. If the packet is an ARP request, this function is responsible for invoking NetArpSnd() to send the reply.

int 
NetArpRcv(int n, NetArpPkt * pArpPkt) 
{ 
    int retval = NET_ERROR; 


    /*
     * If ARP packet is addressed to this system, process it.
     */ 
    if ((pArpPkt->arpHdr.hw_type == htons(ETHERNET)) && 
        (pArpPkt->arpHdr.prot_type == htons(PROTO_IP)) && 
        (pArpPkt->target_ip_addr == local_ip_addr())) 
    { 
        /*
         * Add or update the sender's ARP cache entry.
         */ 
        NetArpAddEntry(pArpPkt->sender_hw_addr, 
            pArpPkt->sender_ip_addr); 

        /*
         * Process the ARP message.
         */ 
        switch (ntohs(pArpPkt->arpHdr.operation)) 
        { 
          case ARP_REQUEST: 
            /*
             * Reply to the request. 
             */
            NetArpSnd(ARP_REPLY, pArpPkt->sender_hw_addr, 
                pArpPkt->sender _ip_addr); 
            retval = NET_SUCCESS; 
            break; 

          case ARP_REPLY: 
            /*
             * We've already updated ARP cache as necessary. 
             */ 
            retval = NET_SUCCESS; 
            break; 

          default: 
            /*
             * Unsupported operation (RARP?).
             */ 
            retval = NET_ERROR; 
            break; 
         } 
    } 

    return (retval); 

}   /* NetArpRcv() */ 

Listing 2. A function to handle incoming ARP packets

ARP cache

If you looked closely at the code in Listing 2, you probably noted a call to a mysterious function named NetArpAddEntry(). Obviously, we don't want to broadcast an ARP request onto the network and wait for a reply before sending each and every IP packet. This could seriously offset the performance of communications across the entire network. So we need a way to keep track of the hardware addresses we've already learned about. To do this, the ARP module typically includes a cache of the most recently used hardware addresses.

An ARP cache can be implemented in a variety of ways and there are no strict rules about doing it. In fact, nothing in the standard specifically precludes a system from asking for the hardware address of a system each time a packet is to be sent to it; so an ARP cache is not strictly necessary.

Because I want my stack to be small and simple and because I only aim to support IP-Ethernet address pairs, I've employed a pretty basic strategy for hardware address tracking. The first element of this strategy is the ARP cache itself, which is defined as follows:

typedef struct 
{ 
    INT32U  ip_addr; 
    INT8U   hw_addr[HW_ADDR_LEN];

} NetArpTblEntry; 

NetArpTblEntry gArpCache[NET_ARP_CACHE_SIZE];

where the size of the cache, NET_ARP_CACHE_SIZE, is a configuration option. Each entry in the cache takes up 10 bytes of RAM, so you want to limit this as much as possible. The correct number of entries depends heavily on your application. If you'll only be communicating with one other system, a cache containing just one entry would be sufficient.

Before using the ARP cache, it should be flushed or initialized with meaningless records. This can be done with the help of the function shown in Listing 3, NetArpFlush(). The idea here is simply to ensure that none of the data in the cache be interpreted as a valid address pair.

void 
NetArpFlush(void) 
{ 
    int i; 


    /*
     * Fill the ARP cache with null records. 
     */ 
    for (i = 0; i < NET_ARP_CACHE_SIZE; i++) 
    { 
        gArpCache[i].ip_addr = 0; 
        memcpy(gArpCache[i].hw_addr, broadcast, HW_ADDR_LEN); 
    } 

}   /* NetArpFlush() */ 

Listing 3. A function to flush the ARP cache

One important thing to note about flushing the cache is that address pairings can become "stale" over time. For example, imagine that a system you're communicating with has a hardware failure. A new system is substituted for it and given the same IP address as the old one. But this new system will have a different hardware address. The result? Your IP packets will be sent to a non-existent hardware address and will be received by no one. You won't be able to communicate with the replacement system unless and until it happens to send you an ARP request. (NetArpRcv() automatically updates the ARP cache when either an ARP request or an ARP reply is received.)

One way to prevent such problems from occuring is to periodically flush the cache. The network device driver might, for example, flush the cache every 20 minutes. The next IP packet sent from the local application code to each IP address will trigger an ARP request and a fresh ARP reply. Since embedded systems tend to run for long periods of time without a reset, you should plan for the worst case and flush the cache from time to time.

A cache of hardware addresses isn't very useful without a way to do lookups. That's the purpose of the NetArpLookup() function in Listing 4. The guts of this routine should make perfect sense. The ARP cache is searched, linearly, until an IP address matching the one provided as a parameter is found. If a match is found, a pointer to the associated hardware address is returned. Otherwise, a pointer to the broadcast address is returned. The caller must check this return value to see if a call to NetArpSnd() is necessary.

INT8U * 
NetArpLookup(INT32U ip_addr) 
{ 
    int i; 


    /*
     * Search the ARP cache for a matching record.
     */
    for (i = 0; i < NET_ARP_CACHE_SIZE; i++) 
    { 
        if (gArpCache[i].ip_addr == ip_addr) 
        { 
            /*
             * Found a match, return the hardware address.
             */
            return (gArpCache[i].hw_addr); 
        } 
    } 

    /*
     * Not found, return a pointer to the broadcast address. 
     */ 
    return (broadcast); 

} /* NetArpLookup() */ 

Listing 4. A function to search the ARP cache for a MAC address

Listing 5 shows the implemenation of the final function in the ARP module. NetArpAddEntry() simply adds a new IP-Ethernet address pair to the ARP cache. If the IP address is already in the cache, that record is updated. If it is not already in the cache, the first available slot is filled in. If, for some reason, the entire ARP cache is filled, the cache is flushed and the new address pair is inserted in the very first location. The position of the entry in the cache is returned, though the caller should have no particular use for it.

int 
NetArpAddEntry(INT8U * hw_addr, INT32U ip_addr) 
{ 
    int i; 


    /*
     * Look for a place to insert entry into ARP cache.
     */ 
    for (i = 0; i < NET_ARP_CACHE_SIZE; i++) 
    { 
        if ((gArpCache[i].ip_addr == ip_addr) 
            || (gArpCache[i].ip_addr == 0)) 
        { 
            /*
             * Found existing or new slot in cache.
             */
            gArpCache[i].ip_addr = ip_addr; 
            memcpy(gArpCache[i].hw_addr, hw_addr, 
                HW_ADDR_LEN); 
            return (i); 
        } 
    } 

    /*
     * The ARP cache is full! 
     * Clear the cache and use first slot.
     */ 
    NetArpFlush(); 
    gArpCache[0].ip_addr = ip_addr; 
    memcpy(gArpCache[0].hw_addr, hw_addr, HW_ADDR_LEN); 
    return (0); 

}   /* NetArpAddEntry() */ 

Listing 5. A function to add an entry to the ARP cache


This article began as a column in the July 2000 issue of Embedded Systems Programming. If you wish to cite the article in your own work, you may find the following MLA-style information helpful:

Barr, Michael. "Mid Year's Resolutions," Embedded Systems Programming, July 2000 , pp. 43-54.

Related Barr Group Courses: 

Device Driver Development for Embedded Linux

For a full list of Barr Group courses, go to our Course Catalog. See our Training Calendar for our latest public training calendar.

What’s happening and how it’s done. Get in the know.

Sign up for our newsletter today!

Receive free how-to articles, industry news, and the latest info on Barr Group webinars and training courses via email. 

To prevent automated spam submissions leave this field empty.