A Digital Garden - frequent growth but a bit weedy in the back corner
63 | Numbers here are in the Ethernet standard, which is high byte 64 | first. This is the opposite of the byte addressing of machines 65 | such as PDP-11s and VAXes. Therefore, special care must be taken 66 | with the opcode field (
ar$op) described below.
This is even more of an issue today than the RFC implies because little-endian won. x86 and nearly all ARM systems in use run little-endian, so today’s typical host has exactly the PDP-11/VAX problem the RFC describes. Network byte order is still big-endian, so every multi-byte ARP field needs converting. In C that’s htons(), which is why Linux writes the opcode as htons(ARPOP_REQUEST).
The concrete failure the RFC mentions is that ar$op is 1 for a request and 2 for a reply. A little-endian host that stores 1 in memory and sends those bytes as-is puts 01 00 on the wire, which a receiver reads as 256, an unknown opcode, and the packet gets ignored.
It’s a real, recurring bug. Anyone building ARP packets by hand (raw sockets, embedded firmware, network test tools, packet-crafting code) can still get this wrong. Libraries such as Scapy hide it, but hand-rolled code doesn’t.
It isn’t only the opcode. ar$hrd and ar$pro are 16-bit too and need the same treatment. The RFC doesn’t say why it singles out the opcode. A plausible reading is that the hardware and protocol type values tend to be copied from tables or the Ethernet header already in network order, while the opcode is a small constant a programmer is likely to type directly as 1 or 2, and therefore get wrong.
80 | The world is a jungle in general, and the networking game 81 | contributes many animals. At nearly every layer of a network
They don’t write RFCs like they used to
161 | As a packet is sent down through the network layers, routing 162 | determines the protocol address of the next hop for the packet 163 | and on which piece of hardware it expects to find the station 164 | with the immediate target protocol address. In the case of the
On line 163, “piece of hardware” means a network interface on the sending machine: a particular Ethernet card, Wi-Fi adapter, or other port that connects it to a link.
When a packet goes down the stack, routing answers two questions:
eth0 versus eth1.Knowing the first doesn’t automatically tell the low layers the second, especially on a multi-homed host or router.
Picture a router with two Ethernet cards:
eth0 connected to 192.168.1.0/24eth1 connected to 10.0.0.0/24A packet arrives for 8.8.8.8. The routing table says the next hop is 10.0.0.1 via eth1. A routing table entry typically holds both fields for exactly this reason.
ARP is a broadcast on one specific link. To resolve 10.0.0.1 into a MAC address, the machine must broadcast “who has 10.0.0.1?” on the network where 10.0.0.1 actually is. Broadcasting on eth0 would reach the wrong network, and nobody would answer. The interface also fixes the hardware type, since RFC 826 was written to work over different kinds of networks (Ethernet, and in principle others) with different address formats. The ARP cache is therefore effectively keyed by interface and protocol address together.
So the sentence is setting up ARP’s inputs: routing hands down “resolve this IP address, on this interface,” and ARP turns that into “send to this hardware address, out of this interface.”
164 | with the immediate target protocol address. In the case of the 165 | 10Mbit Ethernet, address resolution is needed and some lower 166 | layer (probably the hardware driver) must consult the Address 167 | Resolution module (perhaps implemented in the Ethernet support 168 | module) to convert the <protocol type, target protocol address> 169 | pair to a 48.bit Ethernet address. The Address Resolution module 170 | tries to find this pair in a table. If it finds the pair, it
The table mentioned in line 169 is the ARPCache
170 | tries to find this pair in a table. If it finds the pair, it 171 | gives the corresponding 48.bit Ethernet address back to the 172 | caller (hardware driver) which then transmits the packet. If it
No ARP packets are sent in this instance
197 | When an address resolution packet is received, the receiving
This means all ARP packets not only requests
203 | ?Do I have the hardware type in
ar$hrd?
Almost always Ethernet these days but you might see InfiniBand and FireWire. See also lines 236 to 242.
204 | Yes: (almost definitely)
“Almost definitely” reflects the reasoning that a packet arriving on, say, an Ethernet interface was almost certainly sent by another Ethernet host, so the check is mostly a formality. In practice, the main ways it can fail are these:
In all these cases the RFC’s procedure is the same: the answer is “no,” and the packet is discarded. That outcome is harmless because ARP is self-correcting. A dropped request just leads to a retry, and an unmatched entry simply never enters the table.
205 | [optionally check the hardware length
ar$hln] 206 | ?Do I speak the protocol inar$pro? 207 | Yes: 208 | [optionally check the protocol lengthar$pln]
These checks are optional because once the hardware type check has passed, the length is normally already known. Each hardware type implies an address length (Ethernet addresses are always 6 bytes), so for a host that recognizes ar$hrd, the ar$hln field is redundant: it tells the host something it could have deduced.
The length fields still earn their place in the packet for other reasons:
ar$hln and ar$pln. With the lengths present, any program can walk the packet’s structure even for a hardware type it has never heard of. That matters for network monitors and debugging tools, which see traffic of every kind and needn’t understand all of it.ar$hln catches that.That last point is the case for doing the “optional” check anyway. If a host skips it and the length is wrong, it will read the remaining fields from the wrong offsets and could treat garbage as addresses. For a well-formed packet, though, the type check already guarantees the right length, which is why the RFC considered the extra check a choice rather than a requirement. Many modern stacks do it regardless: Linux, for instance, drops an ARP packet if ar$hln doesn’t match the receiving interface’s address length or ar$pln isn’t 4 for IPv4. The same reasoning applies to the parallel “[optionally check the protocol length ar$pln]” step.
210 | If the pair <protocol type, sender protocol address> is 211 | already in my translation table, update the sender
‘translation table’ means ARPCache
212 | hardware address field of the entry with the new 213 | information in the packet and set Merge_flag to true.
This is the receiver (of any ARP message) updating its ARPCache with the sender’s info. It might as well see if it’s changed while it’s looking at it. See lines 227-234 for more info.
216 | If Merge_flag is false, add the triplet <protocol type, 217 | sender protocol address, sender hardware address> to 218 | the translation table.
This is a new table entry (since the merge didn’t happen because there was nothing to merge with in the ARPCache) as opposed to the update of an exiting one described at lines 210 to 213
221 | Swap hardware and protocol fields, putting the local 222 | hardware and protocol addresses in the sender fields.
This is the data requested in the ARP request being inserted into the ARP response Target MAC and Target IP are set to the ARP Request’s Sender MAC and Sender IP to setup the return trip Sender MAC and Sender IP are set to the MAC and IP of the local machine setting up the requested data
225 | the same hardware on which the request was received.
‘Same hardware’ is referring to the NIC on which the ARP request arrived
248 | Periodic broadcasting is definitely not desired. Imagine 100
Today, mainstream host stacks (Linux, Windows, macOS, BSD) still don’t broadcast their addresses on a timer. What’s common today is broadcasting at specific events, which can look similar in a packet capture:
Gratuitous ARP and announcements. When an interface comes up or gets a new address, hosts broadcast ARP packets about themselves. RFC 5227, which formalized address conflict detection, has a host send a couple of announcements after claiming an address, and then stop. That’s a burst at an event, not a periodic broadcast.
Failover. When a virtual IP moves between machines (VRRP, HSRP, clustered firewalls, VM migration), the new owner broadcasts gratuitous ARPs so everyone updates their tables immediately. This is the RFC’s “Related issue” problem, a host’s address mapping changing, solved by pushing the change instead of waiting for entries to expire.
Cache refresh is mostly unicast. When an entry goes stale, Linux first sends unicast ARP requests straight to the known hardware address, and falls back to broadcast only if those fail. Refreshing is directed at the one host you care about, very much in Plummer’s spirit.
Where you do see genuinely periodic broadcasts:
So the pattern since 1982 has been mostly to keep his design (resolve on demand, learn from traffic) and add event-driven announcements for the cases on-demand resolution handles badly. Periodic broadcasting survives in niches, not as the default.
272 | The value of the hardware field (
ar$hrd) is taken from a list for 273 | this purpose. Currently the only defined value is for the 10Mbit 274 | Ethernet (ares_hrd$Ethernet = 1). There has been talk of using 275 | this protocol for Packet Radio Networks as well, and this will 276 | require another value as will other future hardware mediums that 277 | wish to use this protocol.
The authoritative list is IANA’s Address Resolution Protocol (ARP) Parameters registry, in its Hardware Types sub-registry: https://www.iana.org/assignments/arp-parameters. There, ares_hrd$Ethernet corresponds to value 1, listed as “Ethernet (10Mb).”
How it got there:
ares_hrd$Ethernet = 1) and, lacking an official authority, asked people to send requests for new values to Plummer by post or by mail to DCP@MIT-MC.The ares_hrd$Ethernet name is only RFC 826’s notation; nothing defines that identifier in real code. In implementations it appears as ARPHRD_ETHER, defined as 1 in Linux’s include/uapi/linux/if_arp.h and in net/if_arp.h on the BSDs.
279 | For the 10Mbit Ethernet, the value in the protocol field (
ar$pro) 280 | is taken from the setether_type$. This is a natural reuse of
Where EtherTypes are defined:
In practice, that convention has also stuck beyond Ethernet: ARP over other hardware types still uses EtherType values in ar$pro, so 0x0800 means IPv4 whatever the medium.
In code, it’s ETH_P_IP (0x0800) in Linux’s include/uapi/linux/if_ether.h, and ETHERTYPE_IP in net/ethernet.h on the BSDs.
294 | The opcode is to determine if this is a request (which may cause 295 | a reply) or a reply to a previous request. 16 bits for this is 296 | overkill, but a flag (field) is needed.
Although it’s conceptually a flag, it gets its own field in the packet.
As the RFC puts it in lines 281-286, combining this with the protocol field (ar$pro) would effectively halve the number of protocols that can be resolved under this protocol and would make a monitor/debugger more complex. Stealing a bit from ar$pro would have cut the usable EtherType space from 65,536 values to 32,768 and would have meant every tool decoding the field had to mask the bit off. So the flag couldn’t hide in an existing field; it needed one of its own.
The spare room did get used. RARP (RFC 903) added opcodes 3 and 4, Inverse ARP (RFC 2390) added 8 and 9, ATMARP added a NAK, and IANA’s ARP Parameters registry now lists a couple of dozen opcodes. A 1-bit flag would have needed a new packet format for each of those, while the 16-bit field let them reuse ARP’s format and processing unchanged.
304 | enter the sender information in a table or to send a reply. It 305 | is not necessarily needed in the reply form if one assumes a 306 | reply is only provoked by a request. It is included for 307 | completeness, network monitoring, and to simplify the suggested 308 | processing algorithm described above (which does not look at the 309 | opcode until AFTER putting the sender information in a table).
It’s not needed in a reply because the target is the host that sent the original request, and that host already knows everything the field would tell it.
Walk through an exchange. Host A asks “who has 10.0.0.5?” Host B answers, swapping the fields so that B is now the sender and A is the target. When the reply reaches A:
So the target protocol address in a reply just restates A’s own address back to it. That’s the RFC’s point: it’s not necessarily needed in the reply form if one assumes a reply is only provoked by a request.
The RFC keeps it anyway, giving three reasons: completeness, network monitoring, and to simplify the suggested processing algorithm described above (which does not look at the opcode until AFTER putting the sender information in a table). The last one ties back to the Packet Reception pseudo-code: it asks “Am I the target protocol address?” before it ever checks the opcode. That only works if every packet, request or reply, has a meaningful target protocol address. Without it, the algorithm would have to branch on the opcode first and handle replies separately.
The “if one assumes” condition also matters in practice. Unsolicited replies, such as gratuitous ARP replies sent after a failover, aren’t provoked by any request, so the assumption doesn’t hold there.
315 | In some implementations (which do not get to look at the 14.byte 316 | ethernet header, for example) this may save some register 317 | shuffling or stack space by sending this field to the hardware 318 | driver as the hardware destination address of the packet.
This is in relation to how a host sends a reply, which matters if the ARP code on the reply sending host can’t see the Ethernet frame header.
Many implementations strip it off and hands only the payload, here the ARP packet, up to the protocol code. So the ARP module never learns the frame’s source address directly.
To reply, the host must tell the driver where to send the frame: the requester’s hardware address. Without the Ethernet header, the only place that address exists is inside the ARP packet itself, in the sender hardware address field of the request.
Why the target hardware address field solves it neatly. The RFC’s reply procedure reuses the request’s packet buffer, swapping fields in place:
Step 2 overwrites the requester’s hardware address in the sender field. If that address had nowhere else to go, the code would have to save it first, in a register or on the stack, and keep it around until handing the frame to the driver. But step 1 already copied it into the target hardware address field, at a fixed spot in the buffer. So the code can simply give the driver a pointer to that field as “send to this address.” The address stays in the packet, so nothing extra has to be held.
On 1982 hardware, with few registers, tight memory and code often written in assembly, avoiding that bookkeeping was worth mentioning. It’s also part of why the RFC justifies a field that has no meaning in the request form: in the reply, it does double duty as the frame’s destination address.
Today the saving is trivial, and drivers usually take the destination as a separate argument regardless, but the reasoning explains the field’s placement and why the reply mirrors the request.
329 | The above Address Resolution protocol allows a machine to gain 330 | knowledge about the higher level protocol activity (e.g., CHAOS, 331 | Internet, PUP, DECnet) on an Ethernet cable. It can determine 332 | which Ethernet protocol type fields are in use (by value) and the 333 | protocol addresses within each protocol type. In fact, it is not 334 | necessary for the monitor to speak any of the higher level 335 | protocols involved. It goes something like this:
Here’s a small 1982-style network with five machines on one shared cable. The monitor, M, speaks only IP, but two protocols are in use:
| Machine | Protocol | Protocol address | Hardware address |
|---|---|---|---|
| A | IP | 10.0.0.1 | 08:00:2b:00:00:0a |
| B | IP | 10.0.0.2 | 08:00:2b:00:00:0b |
| C | CHAOS | 0x0A21 | 08:00:2b:00:00:0c |
| D | CHAOS | 0x0A22 | 08:00:2b:00:00:0d |
| M (monitor) | IP | 10.0.0.9 | 08:00:2b:00:00:99 |
IP’s EtherType is 0x0800; CHAOS’s is 0x0804. IP addresses are 4 bytes, CHAOS addresses 2 bytes.
Event 1: A broadcasts “who has 10.0.0.2?”
M receives it, like every station. It reads ar$pro = 0x0800, ar$hln = 6, ar$pln = 4, finds the sender fields at the offsets those lengths imply, and records:
| Protocol type | Protocol address | Hardware address |
|---|---|---|
| 0x0800 | 10.0.0.1 | 08:00:2b:00:00:0a |
The target protocol address is 10.0.0.2, not M’s, so M does nothing else.
Event 2: B replies to A. The reply goes directly to A’s hardware address, so M never sees it. M still doesn’t know B’s mapping, even though B just announced it to A.
Event 3: C broadcasts a CHAOS request for D. This is where the format pays off. M doesn’t speak CHAOS, but the packet describes itself:
00 01 hrd = 1 (Ethernet)
08 04 pro = 0x0804 (CHAOS)
06 hln = 6
02 pln = 2
00 01 op = 1 (request)
08 00 2b 00 00 0c sha (6 bytes, per hln)
0a 21 spa (2 bytes, per pln)
00 00 00 00 00 00 tha (unknown, ignored)
0a 22 tpa (2 bytes, per pln)Using only the two length bytes, M slices out C’s addresses and adds a row:
| Protocol type | Protocol address | Hardware address |
|---|---|---|
| 0x0800 | 10.0.0.1 | 08:00:2b:00:00:0a |
| 0x0804 | 0x0A21 | 08:00:2b:00:00:0c |
M now knows CHAOS is in use on the cable, without knowing what a CHAOS address means. It also doesn’t need to know whether CHAOS uses its own request or reply codes: the opcode is in a separate field with the same meaning for every protocol.
Event 4: D replies to C, directly, so M doesn’t see it. D remains invisible to M.
Event 5: B broadcasts “who has 10.0.0.9?” That’s M’s own address. M records B, then behaves as an ordinary host: swaps fields, fills in its own addresses, and sends a reply to B.
| Protocol type | Protocol address | Hardware address |
|---|---|---|
| 0x0800 | 10.0.0.1 | 08:00:2b:00:00:0a |
| 0x0804 | 0x0A21 | 08:00:2b:00:00:0c |
| 0x0800 | 10.0.0.2 | 08:00:2b:00:00:0b |
What M ends up knowing. Two protocols are active (EtherTypes 0x0800 and 0x0804), and it has mappings for A, B and C. It knows only about requesters, never repliers: B surfaced only because B later sent a request of its own, and D will stay unknown until D broadcasts one. That’s the limitation the RFC describes at lines 344-346. M could fill the gap by sending its own request for D, but D’s address is CHAOS and M doesn’t speak CHAOS. And if another monitor were listening and reacted to M’s request the same way, the RFC’s request loop (mentioned at lines 346-348) would begin.
The idea survives in tools like ARPWatch, ARPScan, Wireshark, and intrusion detection systems that watch for ARP spoofing. The unicast-reply limitation still holds, and is stronger on switched networks: a passive listener sees broadcast requests but generally not the replies, unless it uses port mirroring.