Networking Fundamentals · beginner · ~10 min

MAC addresses and ARP

- Describe what a **MAC address** is, how it differs from an IP address, and why both are needed to deliver a packet. - Trace the full **ARP request/reply** exchange, step by step, from an unanswered IP to a filled-in **ARP cache** entry. - Read the fields of an **ARP frame** (opcode, sender/target hardware and protocol addresses) and tell a request from a reply. - Explain why ARP is **trust-based** and how that single design gap makes **ARP spoofing** and man-in-the-middle attacks possible. - Name the common defensive measures (dynamic ARP inspection, static entries, segmentation) and where each fits.

Overview

When two computers on the same local network talk, the data does not travel by IP address the way most people imagine. IP addresses are how the internet routes packets between networks, but the actual wire — Ethernet or Wi-Fi — only knows how to deliver a frame to a hardware address burned into a network card. That hardware address is the MAC address, and the small protocol that figures out which MAC address goes with a given IP is the Address Resolution Protocol (ARP).

In plain language: your computer knows the IP of the machine it wants to reach (say the router at 192.168.1.1), but to put a frame on the wire it must know that IP's MAC address. ARP is how it asks the local network "who owns this IP?" and remembers the answer.

This lesson builds directly on The TCP/IP and OSI models. There you learned that networking is organized in layers, with the link layer (layer 2) sitting below the internet/IP layer (layer 3). ARP is the glue between those two layers: it lets a layer-3 IP address be translated into a layer-2 MAC address so the frame can actually be sent. If the layered model told you what the layers are, this lesson shows you how two adjacent layers cooperate on a real LAN.

Once the plain idea is clear, the terminology is easy: MAC address (48-bit hardware address), ARP request (a broadcast question), ARP reply (a unicast answer), and ARP cache (the short-lived table of answers).

Why it matters

ARP runs constantly, silently, under almost every connection you make on a LAN — every web request, every file copy, every video call starts with an ARP lookup for the next hop. If ARP is broken or slow, nothing else works, so understanding it is fundamental to diagnosing "I have an IP but I can't reach anything" problems.

It matters even more for security. ARP was designed in an era of trusted networks and has no authentication whatsoever — any machine on the LAN can claim to own any IP, and everyone believes it. This is the root of ARP spoofing / ARP poisoning, the classic technique for putting an attacker between two hosts as a man-in-the-middle (MITM) who can read or alter traffic. Many real LAN attacks, and the defenses built into managed switches, exist precisely because of this gap.

ARP is also where practical packet analysis begins. Open any capture of local traffic and the first frames you see are usually ARP. Being able to recognize an ARP request versus a reply, and spot two different MACs claiming the same IP, is an everyday skill for network engineers and defenders alike.

Core concepts

1. The MAC address

Definition. A MAC (Media Access Control) address is a 48-bit (6-byte) identifier assigned to a network interface, usually written as six hex pairs like 00:1a:2b:3c:4d:5e. It operates at the link layer.

Plain explanation. Think of the IP address as a mailing address (it can change when you move networks) and the MAC address as a serial number on the mailbox hardware itself. On a single LAN, frames are delivered to MAC addresses, not IPs.

How it works internally. The first 3 bytes are the OUI (Organizationally Unique Identifier), assigned to the hardware vendor; the last 3 bytes are chosen by the vendor per device. A special value, ff:ff:ff:ff:ff:ff, is the broadcast address — a frame sent there is delivered to every interface on the LAN. That broadcast ability is exactly what ARP uses to ask its question.

When it matters / when it doesn't. MAC addresses matter only within a single LAN segment. The moment a packet crosses a router into another network, the source and destination MAC are rewritten for the next hop — the IP addresses stay the same end-to-end, but the MACs are hop-by-hop.

Common pitfall. Believing a MAC address is globally unique and permanent. In practice MACs can be spoofed in software (ip link set dev eth0 address ...), and many devices randomize their MAC for privacy. Never treat a MAC as a trustworthy identity.

48-bit MAC address: 00:1a:2b : 3c:4d:5e
                    \_______/  \_______/
                       OUI       device
                    (vendor)   (per-NIC)

Special: ff:ff:ff:ff:ff:ff = broadcast (everyone on the LAN)

Knowledge check: Two laptops are on the same Wi-Fi network. Laptop A sends a file to Laptop B. Which address does the Wi-Fi frame carrying the data use as its destination — B's IP or B's MAC? Why?

2. IP vs MAC — why both exist

Definition. IP addresses are layer-3 logical addresses used to route between networks; MAC addresses are layer-2 physical addresses used to deliver within one network.

Plain explanation. IP gets a packet to the right neighborhood (network); MAC gets it to the right house (interface) once it has arrived. You need both because the internet is a network of networks — routing across it needs a hierarchical, changeable address (IP), while the final local delivery needs a fixed hardware handle (MAC).

Aspect IP address MAC address
Layer 3 (internet) 2 (link)
Scope Global / across networks Local LAN segment
Size (IPv4) 32 bits 48 bits
Changes when you move networks Yes No (burned in, though spoofable)
Used for Routing between networks Delivery within one network
Rewritten at each router hop No (stays end-to-end) Yes (per hop)

Common pitfall. Thinking a router "forwards by MAC." Routers forward by IP and rewrite the MACs for the next hop; switches forward by MAC within a LAN. Confusing the two makes ARP's job impossible to understand.

Knowledge check (explain in your own words): Why does the destination MAC address of a frame change at every router hop, while the destination IP address stays the same the whole way?

3. The ARP request/reply exchange

Definition. ARP resolves an IPv4 address to a MAC address using a two-message exchange: a broadcast request and a unicast reply.

Plain explanation. The sender shouts to the whole LAN, "Who has 192.168.1.20? Tell me your MAC." Only the owner of that IP answers, privately: "192.168.1.20 is at 00:1a:2b:3c:4d:5e." Everyone else ignores the question.

How it works internally. The request is an Ethernet frame with destination MAC ff:ff:ff:ff:ff:ff (broadcast) carrying an ARP payload whose opcode = 1 (request). It fills in the sender's own IP+MAC and the target IP, leaving the target MAC blank. The owner responds with an ARP frame, opcode = 2 (reply), sent unicast straight back to the requester with its MAC filled in.

Host A (192.168.1.10) wants to reach 192.168.1.20

1) REQUEST  (broadcast, opcode 1)
   A --> ff:ff:ff:ff:ff:ff : "Who has 192.168.1.20? Tell 192.168.1.10"
           |
   +-------+-------+-------+   (every host on the LAN receives it)
   v               v       v
  .11 ignores    .20 MATCH  .30 ignores

2) REPLY  (unicast, opcode 2)
   .20 --> A : "192.168.1.20 is at 00:1a:2b:3c:4d:5e"

3) A stores the pair in its ARP cache and sends the real data.

When to use / when NOT. ARP is used only for IPv4 on a shared-medium LAN. It is not used across routers (you ARP for your gateway's MAC, then the gateway ARPs onward on the next network), and it does not exist in IPv6 — IPv6 uses Neighbor Discovery Protocol (NDP) instead.

Common pitfall. Assuming the reply is verified. Nothing checks that the replier actually owns the IP; the first believable answer wins. Hold that thought for concept 5.

Knowledge check (predict the output): You run a packet capture and see a frame with destination MAC ff:ff:ff:ff:ff:ff and ARP opcode 1. Is this a request or a reply, and roughly what question is it asking?

4. The ARP cache

Definition. The ARP cache (or ARP table) is a short-lived, in-memory table of recently resolved IP-to-MAC mappings, one per active neighbor.

Plain explanation. Asking the whole LAN every single time would be wasteful, so each host remembers answers for a while (typically seconds to a couple of minutes) and reuses them.

How it works internally. Entries expire on a timer so that stale mappings (a device that moved or changed its MAC) get re-resolved. Entries can be dynamic (learned via ARP) or static (pinned manually). You can view the cache with arp -a or ip neigh.

$ ip neigh
192.168.1.1   dev eth0  lladdr 00:1a:2b:3c:4d:5e  REACHABLE
192.168.1.20  dev eth0  lladdr 3c:22:fb:88:11:aa  STALE

When to use static entries. Pinning a static entry for a critical host (like the default gateway) is a lightweight hardening step: a spoofed reply can no longer overwrite it.

Common pitfall. Forgetting that dynamic entries can be overwritten by an unsolicited reply on many systems (a gratuitous ARP). That overwrite behavior is the mechanism ARP poisoning abuses.

Knowledge check (find the bug in reasoning): A student says, "My ARP cache proves who's on the network — if an IP maps to a MAC there, that's definitely that machine." What's wrong with that claim?

5. No authentication → ARP spoofing (defensive view)

Definition. ARP spoofing / poisoning is sending forged ARP replies that map a victim's IP (often the gateway) to the attacker's MAC, so the victim's traffic is delivered to the attacker instead.

Plain explanation. Because any host may answer and answers aren't verified, an attacker on the LAN can repeatedly announce "the gateway's IP is at my MAC." Victims cache that lie and send their outbound traffic to the attacker, who forwards it on — becoming an invisible man-in-the-middle.

How it works internally (conceptually, for defense). The attacker sends unsolicited (gratuitous) ARP replies to the victim and, to intercept both directions, also poisons the gateway's cache with the victim's IP. Enabling IP forwarding lets traffic still flow, so the victim notices nothing.

Before:   Victim  <----------->  Gateway  <---> Internet

Poisoned: Victim  --->  Attacker  --->  Gateway  <---> Internet
                 (victim's cache says
                  gateway-IP = attacker-MAC)

Defenses (the point of learning this):

Defense What it does Where it fits
Dynamic ARP Inspection (DAI) Managed switch validates ARP against a trusted DHCP-snooping table and drops forgeries Enterprise switched networks
Static ARP entries Pin critical mappings (e.g. gateway) so replies can't overwrite them A few high-value hosts
Network segmentation / VLANs Shrinks the broadcast domain so fewer machines can even attempt it Network design
Encryption (TLS, SSH, VPN) Even if intercepted, traffic stays confidential and tamper-evident Every application
ARP monitoring (e.g. arpwatch) Alerts when an IP's MAC suddenly changes Detection / SOC

Common pitfall. Relying on a single defense. Encryption protects the content but not the fact of interception or metadata; DAI needs a managed switch. Defense in depth is the professional answer.

Knowledge check (concept): Why does encrypting traffic with TLS reduce the impact of ARP spoofing even though it doesn't stop the spoofing itself?

Syntax notes

ARP is a protocol, not code, so the "syntax" here is the layout of an ARP-over-Ethernet frame. Recognizing these fields is what a classifier reads.

Ethernet header (14 bytes)
  [ dst MAC 6 ][ src MAC 6 ][ ethertype 2 = 0x0806 for ARP ]

ARP payload (28 bytes for IPv4-over-Ethernet)
  offset
   0  Hardware type   (2)  = 0x0001  (Ethernet)
   2  Protocol type   (2)  = 0x0800  (IPv4)
   4  HW addr len     (1)  = 6
   5  Proto addr len  (1)  = 4
   6  Opcode          (2)  = 1 request, 2 reply   <-- the key field
   8  Sender MAC      (6)
  14  Sender IP       (4)
  18  Target MAC      (6)   (all zeros in a request)
  24  Target IP       (4)

The opcode at offset 6-7 (big-endian, i.e. network byte order) is what tells a request (0x0001) from a reply (0x0002). Everything numeric on the wire is big-endian, so byte 6 is the high byte and byte 7 is the low byte: opcode = frame[6] << 8 | frame[7].

Lesson

Delivery on a LAN uses MAC, not IP

On a single LAN, hosts reach each other by MAC address (a 48-bit hardware address like 00:1a:2b:3c:4d:5e), not by IP.

The link layer does not understand IP. To send a frame, it needs the MAC address of the next hop.

ARP: turning an IP into a MAC

ARP (Address Resolution Protocol) bridges layer 3 (IP) and layer 2 (the link layer).

Suppose a host wants to send to 192.168.1.20. It broadcasts a question to the whole LAN:

"Who has 192.168.1.20? Tell me your MAC."

The owner of that IP replies with its MAC address. The sender stores the result in its ARP table (also called the ARP cache) so it does not have to ask again right away.

You can inspect that table:

$ arp -a
? (192.168.1.1) at 00:1a:2b:3c:4d:5e [ether] on eth0

Why this matters for security

ARP has no authentication. Any host can claim, "I have that IP," and the reply is believed.

This is ARP spoofing (also called ARP poisoning). In authorized testing, it is used to place an attacker in the middle of LAN traffic as a man-in-the-middle.

Common defences include:

  • Dynamic ARP inspection on switches.
  • Static ARP entries for critical hosts.
  • Network segmentation to limit who shares a LAN.

Recognising ARP frames is also a first step in reading packet captures.

Code examples

Below is a small, complete C11 program that reads the opcode out of a raw ARP frame buffer and reports whether it is a request or a reply — the same first step an ARP-spoofing detector performs before it compares sender MAC/IP pairs. It parses two hard-coded sample frames so it runs with no network access.

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

/* Offsets within an ARP-over-Ethernet frame (14-byte Ethernet header + ARP). */
#define ETH_HEADER_LEN 14
#define ETH_TYPE_OFFSET 12
#define ETHERTYPE_ARP  0x0806
#define ARP_OPCODE_OFFSET (ETH_HEADER_LEN + 6) /* opcode is 6 bytes into ARP */
#define ARP_FRAME_MIN  (ETH_HEADER_LEN + 28)   /* 14 + 28 for IPv4/Ethernet */

#define ARP_REQUEST 1
#define ARP_REPLY   2

/* Read a big-endian (network byte order) 16-bit value at frame[pos]. */
static uint16_t read_be16(const uint8_t *frame, size_t pos) {
    return (uint16_t)((frame[pos] << 8) | frame[pos + 1]);
}

/* Return the ARP opcode, or 0 if the buffer is not a valid ARP frame. */
int arp_opcode(const uint8_t *frame, size_t n) {
    if (frame == NULL || n < ARP_FRAME_MIN)      /* guard: too short to parse */
        return 0;
    if (read_be16(frame, ETH_TYPE_OFFSET) != ETHERTYPE_ARP) /* not ARP */
        return 0;
    return read_be16(frame, ARP_OPCODE_OFFSET);
}

static void classify(const uint8_t *frame, size_t n, const char *label) {
    int op = arp_opcode(frame, n);
    switch (op) {
        case ARP_REQUEST: printf("%s: ARP request (opcode 1)\n", label); break;
        case ARP_REPLY:   printf("%s: ARP reply   (opcode 2)\n", label); break;
        default:          printf("%s: not a valid ARP frame\n", label); break;
    }
}

int main(void) {
    /* A broadcast ARP request: dst = ff:ff:ff:ff:ff:ff, ethertype 0806, opcode 0001. */
    uint8_t request[ARP_FRAME_MIN] = {
        0xff,0xff,0xff,0xff,0xff,0xff,            /* dst MAC (broadcast) */
        0x00,0x1a,0x2b,0x3c,0x4d,0x5e,            /* src MAC */
        0x08,0x06,                               /* ethertype = ARP */
        0x00,0x01, 0x08,0x00, 0x06, 0x04,        /* HW/proto type + lengths */
        0x00,0x01,                               /* opcode = 1 (request) */
        /* remaining ARP address fields left as zero for this demo */
    };

    /* A unicast ARP reply: same header shape, opcode 0002. */
    uint8_t reply[ARP_FRAME_MIN] = {
        0x00,0x1a,0x2b,0x3c,0x4d,0x5e,           /* dst MAC (the requester) */
        0x3c,0x22,0xfb,0x88,0x11,0xaa,           /* src MAC (the answerer)  */
        0x08,0x06,
        0x00,0x01, 0x08,0x00, 0x06, 0x04,
        0x00,0x02,                               /* opcode = 2 (reply) */
    };

    classify(request, sizeof request, "frame #1");
    classify(reply,   sizeof reply,   "frame #2");

    uint8_t truncated[10] = {0};                 /* too short to be ARP */
    classify(truncated, sizeof truncated, "frame #3");
    return 0;
}

What it does. arp_opcode first rejects anything too short or whose ethertype isn't 0x0806, then returns the 16-bit opcode read in network byte order. main builds one request frame, one reply frame, and one deliberately truncated buffer, then classifies each.

Expected output:

frame #1: ARP request (opcode 1)
frame #2: ARP reply   (opcode 2)
frame #3: not a valid ARP frame

Key edge cases. A buffer shorter than 42 bytes can't hold a full ARP frame, so it's rejected before any out-of-bounds read. A frame whose ethertype isn't ARP returns 0 even if the bytes at the opcode offset happen to look like 1 or 2. Opcodes other than 1 and 2 exist (RARP, etc.) and here fall through to "not a valid ARP frame," which is fine for a request/reply classifier.

Line by line

We'll trace classify(request, 42, "frame #1").

  1. classify calls arp_opcode(request, 42).
  2. Guard 1: frame is non-NULL and n (42) is >= ARP_FRAME_MIN (42), so we continue. Had n been 10, we'd return 0 immediately — this is the bounds check that prevents reading past the buffer.
  3. Ethertype check: read_be16(frame, 12) reads bytes 12 and 13, which are 0x08 and 0x06. It computes (0x08 << 8) | 0x06 = 0x0806, which equals ETHERTYPE_ARP, so we don't bail out.
  4. Opcode read: ARP_OPCODE_OFFSET is 14 + 6 = 20. read_be16(frame, 20) reads bytes 20 and 21: 0x00 and 0x01, giving (0x00 << 8) | 0x01 = 1. That's returned.
  5. Back in classify, op == 1 == ARP_REQUEST, so the case ARP_REQUEST branch prints frame #1: ARP request (opcode 1).

For frame #2, step 4 reads bytes 20-21 = 0x00, 0x02, yielding 2, so the reply branch fires. For frame #3, n is 10, which is < 42, so step 2 returns 0 and the default branch prints "not a valid ARP frame."

Frame n ethertype@12 bytes@20-21 opcode branch
#1 request 42 0x0806 00 01 1 request
#2 reply 42 0x0806 00 02 2 reply
#3 short 10 (not read) (not read) 0 not valid

The key idea: validate length first, then type, then read the field — each guard protects the read that follows it.

Common mistakes

Mistake 1 — Reading the opcode without a length check.

/* WRONG: assumes the buffer is long enough */
int arp_opcode(const uint8_t *frame) {
    return (frame[20] << 8) | frame[21];   /* reads offsets 20,21 blindly */
}

Why it's wrong: if the caller passes a 10-byte buffer, frame[20] reads 10 bytes past the end — undefined behavior that may crash or leak stack memory. Fix: pass the length and check it first (as in the lesson code). Recognize it when a fuzzer or a short real-world frame causes a crash.

Mistake 2 — Ignoring byte order.

/* WRONG on a little-endian machine */
uint16_t op = *(uint16_t *)&frame[20];  /* reads bytes as host order */

On x86 this yields 0x0100 for a request instead of 0x0001. Network data is big-endian; always assemble it byte-by-byte (hi << 8 | lo) or use ntohs. Recognize it when values look byte-swapped (256 instead of 1).

Mistake 3 — Confusing IP delivery with MAC delivery. Learners often say "the packet is sent to the destination IP's MAC." On a routed path the frame's destination MAC is the next hop (your gateway), not the final host. Correct mental model: IP is end-to-end, MAC is hop-by-hop. Catch it by remembering that a far-away server's MAC never appears in your local ARP cache — only your gateway's does.

Mistake 4 — Trusting the ARP cache as identity. Assuming arp -a proves who's who. A spoofer can make any IP map to their MAC. Never authenticate a peer by its MAC or ARP entry; use cryptographic identity (TLS certs, SSH keys) instead.

Mistake 5 — Forgetting IPv6 doesn't use ARP. Debugging IPv6 connectivity by looking for ARP traffic finds nothing, because IPv6 uses NDP (ICMPv6 Neighbor Solicitation/Advertisement). Look at ip -6 neigh instead.

Debugging tips

Compiler errors (for the C example):

  • "implicit declaration of function" for printf → you forgot #include <stdio.h>. For uint8_t/uint16_t include <stdint.h>; for size_t include <stddef.h>.
  • "comparison of integer expressions of different signedness" when comparing n (a size_t) with a small int → compare against a size_t-typed constant like ARP_FRAME_MIN (fine here) or cast deliberately.

Runtime errors:

  • Segfault or garbage opcode → almost always a missing/incorrect length guard, or reading at the wrong offset. Print n and the raw bytes with %02x before parsing.
  • Every frame classified as "not valid" → check the ethertype offset (12) and value (0x0806); a common slip is forgetting the 14-byte Ethernet header when computing the opcode offset.

Logic / network errors (real ARP):

  • "I have an IP but can't reach the gateway" → inspect ip neigh; a FAILED or missing entry for the gateway means ARP isn't resolving (cabling, VLAN, or the gateway is down).
  • Intermittent connectivity where the gateway's MAC keeps changing → possible ARP poisoning or a duplicate-IP conflict; compare the MAC in your cache against the device's real MAC.

Questions to ask when it doesn't work:

  1. Am I checking length before every read?
  2. Am I reading multi-byte fields in network (big-endian) order?
  3. Did I account for the 14-byte Ethernet header in my offsets?
  4. For a real network: is this even IPv4 (ARP) or IPv6 (NDP)?
  5. Does the MAC in my cache match the device's actual MAC?

Memory safety

This is a concept lesson, but the sample parser is C, and parsing untrusted network bytes is one of the most dangerous things C code does — so the memory-safety discipline is the real lesson.

  • Bounds first. Any raw frame comes from the network and may be truncated or malicious. Check n >= ARP_FRAME_MIN (and NULL) before indexing. Every field read must be provably within n. Reading frame[20] on a 10-byte buffer is an out-of-bounds read (undefined behavior) that can crash or leak memory.
  • No type punning across alignment/endianness. Don't cast &frame[20] to uint16_t *: it assumes host byte order and can also violate alignment on some platforms. Assemble bytes explicitly with shifts, as read_be16 does.
  • Initialize buffers. The demo frames use aggregate initializers so trailing bytes are zero; an uninitialized stack buffer would feed garbage into the parser.
  • Integer width. Shifting a uint8_t promotes to int, so (frame[pos] << 8) is safe here; but be wary of shifts near 32 bits and of signed overflow when hand-rolling parsers.

Security framing (defensive, lab-only). ARP's lack of authentication is a design vulnerability, clearly labeled: any host can forge replies, enabling MITM. The defensive pairing: validate at the switch (Dynamic ARP Inspection backed by DHCP snooping), pin static entries for critical hosts, segment the network to shrink the attack surface, encrypt so intercepted traffic stays confidential, and monitor (arpwatch) for sudden MAC changes. Practice interception only in an isolated lab you own — never on networks or targets you aren't authorized to test.

Real-world uses

Where ARP shows up in real systems.

  • Every LAN connection. Your OS ARPs for the default gateway before the first packet leaves; home routers, office switches, data-center top-of-rack switches all depend on it.
  • Switches build MAC-address tables by watching source MACs, and managed switches run Dynamic ARP Inspection to block forged replies.
  • Security tooling. Network monitors (Wireshark, arpwatch, Zeek) classify ARP frames to detect poisoning; the classify-arp-frame exercise mirrors the very first step such a detector performs.
  • Provisioning & inventory. DHCP servers and network scanners map IPs to MACs (and MACs to vendors via the OUI) to identify devices.

Professional best-practice habits.

Beginner Advanced
Parsing frames Length-check, then read fixed offsets Validate every field (hw/proto type, lengths) and reject malformed frames; fuzz the parser
Naming #define offsets instead of magic numbers Model the frame as a documented struct/spec with byte-order helpers
Error handling Return 0 / a sentinel on bad input Distinguish "not ARP" from "malformed ARP" and log for monitoring
Security Encrypt traffic; don't trust MACs Deploy DAI + DHCP snooping, static gateway entries, VLAN segmentation, ARP monitoring
Cleanup/robustness Handle truncated input gracefully Handle VLAN-tagged (802.1Q) frames, IPv6/NDP equivalents, and gratuitous ARP

Good habits: never treat a MAC as identity, always account for the Ethernet header in offsets, read multi-byte fields in network byte order, and pair any "here's how the attack works" understanding with "here's the deployed defense."

Practice tasks

Beginner 1 — Classify by opcode. Write const char *arp_kind(int opcode) that returns "request" for 1, "reply" for 2, and "other" otherwise. Requirements: no I/O, pure function. Example: arp_kind(2)"reply". Hint: a switch is cleanest. Concepts: opcode field, request vs reply.

Beginner 2 — Print a MAC address. Write void print_mac(const uint8_t mac[6]) that prints six hex pairs separated by colons, e.g. 00:1a:2b:3c:4d:5e. Requirements: use %02x; lowercase hex. Input {0,0x1a,0x2b,0x3c,0x4d,0x5e} → output 00:1a:2b:3c:4d:5e. Hint: loop 0..5, print a colon before every pair except the first. Concepts: MAC layout, byte formatting.

Intermediate 1 — Extract sender IP and MAC. Extend the lesson parser with int arp_sender(const uint8_t *frame, size_t n, uint8_t out_mac[6], uint8_t out_ip[4]) that, for a valid ARP frame, copies the sender hardware address (offset 22) and sender protocol address (offset 28) into the outputs and returns 1; returns 0 for invalid/too-short frames. Requirements: bounds-check before copying. Constraint: no dynamic allocation. Hint: reuse the ethertype/length guards from arp_opcode. Concepts: ARP field offsets, bounds checking.

Intermediate 2 — Detect a suspicious mapping change. Write a function that, given a stream of (ip, mac) pairs (as arrays), reports the first time a previously-seen IP appears with a different MAC — the core signal an ARP-monitoring tool raises. Requirements: store seen pairs in a small fixed array; compare 6-byte MACs with memcmp. Example: seeing 192.168.1.1→AA.. then later 192.168.1.1→BB.. prints an alert. Hint: linear search is fine for small tables. Concepts: ARP cache semantics, spoofing detection (defensive).

Challenge — A tiny ARP frame validator. Write int arp_validate(const uint8_t *frame, size_t n) that returns 1 only if: n >= 42; ethertype at 12 is 0x0806; hardware type is 0x0001; protocol type is 0x0800; hardware length is 6; protocol length is 4; and opcode is 1 or 2. Otherwise return 0. Requirements: reject at the first failed check; no out-of-bounds reads for any n. Add a main that runs your validator against at least three crafted frames (valid, wrong-ethertype, truncated) and prints pass/fail. Hint: build a helper read_be16 and validate fields in offset order. Concepts: full-frame validation, defensive parsing, byte order, bounds safety. Do not just check the opcode — validate the whole header shape.

Summary

  • On a LAN, frames are delivered to MAC addresses (48-bit hardware handles), not IPs; IP is end-to-end and routed between networks, MAC is hop-by-hop within one network.
  • ARP bridges layer 3 and layer 2: a broadcast request (opcode 1, dst ff:ff:ff:ff:ff:ff) asks "who has this IP?", the owner sends a unicast reply (opcode 2), and the result is cached in the ARP cache for a short time.
  • The one field that distinguishes request from reply is the opcode at byte 6 of the ARP payload (offset 20 counting the 14-byte Ethernet header), read in network (big-endian) byte order.
  • ARP has no authentication, so any host can forge replies — the basis of ARP spoofing / MITM. Defenses: Dynamic ARP Inspection, static entries for critical hosts, segmentation, encryption, and ARP monitoring.
  • Common mistakes: skipping the length check (out-of-bounds read), ignoring byte order, confusing IP vs MAC delivery, and trusting a MAC as identity. Remember: validate length → validate type → read the field, and never treat a MAC or ARP entry as proof of who someone is.

Practice with these exercises