---
title: 'Address Resolution Attacks'
source: 'https://academia.sh/en/courses/network-security/address-resolution-attacks'
course: 'Network Security'
language: en
updated: '2026-10-21T05:02:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Models the address resolution cache as a data structure and measures the number of corrupted records'
    - 'Counts separately the records rescued by a pinned record, a consistency check, and request matching'
    - 'Compares the cost each defense pays in rejected legitimate replies, manual items, and extra round trips'
---

# Address Resolution Attacks

Measuring how the address resolution cache is corrupted by an unverified reply: 6 of 18 records corrupted in a set of 26 replies, a pinned record rescuing 4 and leaving 2, a consistency check rescuing 5 and leaving the single most valuable record, accepting only the requested reply rescuing all six while cutting 3 legitimate announcements, and the three defenses together rejecting 4 legitimate replies and never forming 2 records at all.

The previous lesson assumed a service was reachable: every row in the inventory was written
by accepting that a packet can reach a host from a zone. But even two machines on the same
segment reaching each other rests on a claim. An endpoint knows its target's network address
but not which hardware address to send the frame to. It asks, and writes the reply it
receives into its cache.

The write rule is this lesson's starting point. The cache does not test whether the incoming
reply is correct; it does not even ask whether the reply was requested. When this acceptance
produces a false mapping it is called **address resolution poisoning (ARP poisoning)**; when
the sender presents itself as the owner of another address it is called **address resolution
spoofing (ARP spoofing)**. Three of the eighteen claims in the first lesson were at the link
layer, and all three rest on this single rule. This lesson runs that rule on a data structure
and counts how many records an unverified reply corrupts.

## What the Cache Can Test

**PS7:** the address resolution cache's acceptance rule is "the last reply wins"; whether the
reply was requested and whether it is correct play no part in the rule. **PS8:** a record is
corrupted when a network address becomes bound to a hardware address it does not actually
belong to; every corrupted record misdirects all frames headed to that address. **PS9:** a
defense can test a reply only against the cache's own data; it cannot know whether the reply
is correct.

PS9 is this lesson's boundary, and it determines all three of the three defenses. The cache
has no source of "the correct mapping"; the only thing that can be tested is whether an
incoming reply is **consistent** with the cache's existing data. Protocol mechanics — how
requests and replies are carried, cache lifetime, proxying, and unsolicited usage patterns —
were measured in the **Network Models and Protocols** course; they are not repeated here.

The reply set below is a **model**: it writes as data the replies that reach the cache on one
segment of a fictional regional measurement network; there is no real network or real hardware
address. The set contains no step that generates a forged reply; only the cache's acceptance
rule and its checks run. Sending such a reply set to a real network **is both illegal and
unprofessional without written authorization and a defined scope**.

```js
// networks/resolution.mjs — the address resolution cache of one segment in a fictional
// regional measurement network (model): there is no real network and no real hardware
// address. Hardware addresses are labeled d01..d17. No randomness.

// PS7: the address resolution cache's acceptance rule is "the last reply wins"; whether
// the reply was requested and whether it is correct play no part in the rule.
// PS8: a record is corrupted when a network address becomes bound to a hardware address it
// does not actually belong to; every corrupted record misdirects all frames headed there.
// Columns: network address, claiming hardware address, is it a reply to a pending request
// (y/n), is the claim correct (y/n). The last column is for scoring only; no defense sees it.
export const REPLY = `
  10.20.0.1   d01  y y     10.20.0.5   d02  y y     10.20.0.6   d03  y y
  10.20.0.7   d04  y y     10.20.0.8   d05  y y     10.20.0.9   d06  y y
  10.20.0.10  d07  y y     10.20.1.11  d08  y y     10.20.1.12  d09  y y
  10.20.1.13  d10  y y     10.20.2.21  d11  y y     10.20.2.22  d12  y y
  10.20.0.11  d13  y y     10.20.0.12  d14  y y     10.20.0.30  d17  y y
  10.20.0.31  d17  y y     10.20.0.2   d01  n y     10.20.0.32  d17  y y
  10.20.0.7   d15  n y     10.20.2.22  d16  n y     10.20.0.1   d09  n n
  10.20.0.5   d09  n n     10.20.0.6   d09  n n     10.20.0.8   d09  n n
  10.20.0.10  d09  n n     10.20.0.9   d09  n n`
  .trim().split(/\s+/).reduce((l, _, i, a) => (i % 4 ? l : [...l,
    { net: a[i], hw: a[i + 1], req: a[i + 2] === "y", ok: a[i + 3] === "y" }]), []);

// PS9: a defense can test a reply only against the cache's own data; it cannot know
// whether the reply is correct. All three defenses work within this limit.
export const PINNED = ["10.20.0.1", "10.20.0.5", "10.20.0.6", "10.20.0.7", "10.20.0.10"];
export const THRESHOLD = 2;

// The cache processes replies in order; the selected defenses test each reply before accepting it.
export function cache(defense = []) {
  const table = new Map(), rejected = [];
  for (const rep of REPLY) {
    if (defense.includes("pinned") && PINNED.includes(rep.net) && table.has(rep.net)) {
      rejected.push(rep); continue;
    }
    if (defense.includes("requested") && !rep.req) { rejected.push(rep); continue; }
    if (defense.includes("consistency")) {
      const after = [...table].filter(([a, d]) => d === rep.hw && a !== rep.net).length + 1;
      if (after > THRESHOLD) { rejected.push(rep); continue; }
    }
    table.set(rep.net, rep.hw);
  }
  const corrupted = [...table].filter(([a, d]) =>
    !REPLY.some((rep) => rep.net === a && rep.hw === d && rep.ok));
  return { table, corrupted, rejectedLegit: rejected.filter((r) => r.ok), rejectedFalse: rejected.filter((r) => !r.ok) };
}
```

```js
// networks/cache-audit.mjs — how many records an unverified reply corrupts, which defense
// rescues how many, how many stay corrupted, and what the cost is.
import { REPLY, PINNED, THRESHOLD, cache } from "./resolution.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const baseline = cache();

console.log(`${REPLY.length} replies, ${baseline.table.size} records; ` +
  `corrupted records ${baseline.corrupted.length}, incorrect replies ` +
  `${REPLY.filter((r) => !r.ok).length}, unrequested replies ` +
  `${REPLY.filter((r) => !r.req).length}`);

const A = [-14, 10, 13, 9];
console.log("\n1. reply set: being requested and being correct are two separate properties");
write(A, "reply", "correct", "not correct", "total");
for (const [name, f] of [["requested", (r) => r.req], ["unrequested", (r) => !r.req]]) {
  const l = REPLY.filter(f);
  write(A, name, l.filter((r) => r.ok).length, l.filter((r) => !r.ok).length, l.length);
}

const B = [-10, 9, 15, 16];
console.log("\n2. signal: how many network addresses did a hardware address claim");
write(B, "hardware", "claimed", "accepted", "over threshold");
const hw = [...new Set(REPLY.map((r) => r.hw))];
for (const h of hw.filter((h) => REPLY.filter((r) => r.hw === h).length > 1)) {
  const l = [...new Set(REPLY.filter((r) => r.hw === h).map((r) => r.net))];
  const k = [...baseline.table].filter(([, v]) => v === h).length;
  write(B, h, l.length, k, l.length > THRESHOLD ? "yes" : "no");
}

const C = [-34, 9, 11, 12, 16, 8, 14];
console.log("\n3. defenses: rescued records and their cost");
write(C, "defense", "records", "corrupted", "rescued", "legit rejected", "manual", "extra rounds");
for (const s of [[], ["pinned"], ["consistency"], ["requested"], ["pinned", "consistency"],
  ["pinned", "consistency", "requested"]]) {
  const r = cache(s);
  write(C, s.join(" + ") || "none", r.table.size, r.corrupted.length,
    baseline.corrupted.length - r.corrupted.length, r.rejectedLegit.length,
    s.includes("pinned") ? PINNED.length : 0, s.includes("requested") ? r.table.size : 0);
}

const D = [-28, -34, -44];
console.log("\n4. which record stays corrupted, which legitimate reply is rejected");
write(D, "defense", "record still corrupted", "legitimate reply rejected");
for (const s of [["pinned"], ["consistency"], ["requested"], ["pinned", "consistency"]]) {
  const r = cache(s);
  write(D, s.join(" + "), r.corrupted.map(([a]) => a).join(" ") || "none",
    r.rejectedLegit.map((rep) => `${rep.net}=${rep.hw}`).join(" ") || "none");
}
```

```
26 replies, 18 records; corrupted records 6, incorrect replies 6, unrequested replies 9

1. reply set: being requested and being correct are two separate properties
reply            correct  not correct    total
requested             17            0       17
unrequested            3            6        9

2. signal: how many network addresses did a hardware address claim
hardware    claimed       accepted  over threshold
d01               2              1              no
d09               7              7             yes
d17               3              3             yes

3. defenses: rescued records and their cost
defense                             records  corrupted     rescued  legit rejected  manual  extra rounds
none                                     18          6           0               0       0             0
pinned                                   18          2           4               1       5             0
consistency                              17          1           5               1       0             0
requested                                17          0           6               3       0            17
pinned + consistency                     17          1           5               2       5             0
pinned + consistency + requested         16          0           6               4       5            16

4. which record stays corrupted, which legitimate reply is rejected
defense                     record still corrupted            legitimate reply rejected                   
pinned                      10.20.0.8 10.20.0.9               10.20.0.7=d15                               
consistency                 10.20.0.1                         10.20.0.32=d17                              
requested                   none                              10.20.0.2=d01 10.20.0.7=d15 10.20.2.22=d16  
pinned + consistency        10.20.0.8                         10.20.0.32=d17 10.20.0.7=d15                
```
The numbers belong to the **measurement** class; their inputs are PS7–PS9.

## Six Records

Twenty-six replies produce eighteen records, and with no defense in place **six** of those
records are corrupted. The corruption rate is one in three, but what actually matters is
which records are corrupted: the gateway, the collector, the verifier, the name server, the
directory, and the measurement store. All six are addresses that carry traffic leaving the
segment or heading to central services; the field terminals' and the printer's records are
not corrupted.

The number of corrupted records and the traffic affected are not the same number. In the
previous lesson's inventory, the four services on the collector received 141,652 requests in
thirty days, and the two services on the name server received 163,000. For an endpoint on
this segment, all six of the six corrupted records deliver frames headed to their destination
to the wrong hardware address; the impact per record equals the request volume that address
carries. **The number of corrupted records measures the surface, while the request volume
carried measures the impact, and the two do not give the same ranking.**

The first table separates two properties. All seventeen replies that are a response to a
pending request are correct. Nine replies arrived unrequested; of these, **three are correct
and six are not**. Being unrequested does not mean being false: the announcement of the
virtual gateway address, billing address failover, and an interface change are all announced
unrequested, and all three are legitimate. A defense that rejects unrequested replies
wholesale cuts these three along with the six false ones.

The second table gives the **signal** a check can see. Three hardware addresses claim more
than one network address: one claims two, one three, one **seven**, and all seven of the
seven are accepted. The row of seven collects replies that come from one endpoint on the same
segment and claim six addresses beyond its own. The rows claiming two and three addresses are
legitimate: one is the gateway's virtual address, the other is a host running three guest
systems. **A signal is a number, not a decision**; wherever the threshold is set, it cuts
correct and incorrect together.

## Three Defenses, Three Separate Gaps

The third table runs the three defenses separately and together. A pinned record puts five
addresses on a list and ignores any subsequent reply for those on the list: it rescues four
corrupted records and **leaves two**. The two left behind are the name server and the
directory, which are not on the list. A pinned record's protection extends exactly as far as
the list's length, and it rescues no record that is not on the list.

A consistency check allows a hardware address to claim at most two network addresses, and it
rescues **five** of the six corrupted records. The one record it cannot rescue is the
gateway. The reason is order: the hardware address claiming seven addresses claims the
gateway **first**, in addition to its own address, and at that point the threshold has not
yet been crossed. The threshold is only crossed on the second claim, and everything after
that point is rejected. **A threshold-based check accepts claims up to the threshold; the
claim it misses is always the first claim, and the first claim is usually the most valuable
target.**

Accepting only the requested reply rescues all six of the six corrupted records and drops the
corrupted record count to zero. This is the only one of the three defenses that is complete,
because all six of the six false replies arrived unrequested. This completeness is a property
of the reply set, not of the defense's strength: a reply arriving as a response to a pending
request would have passed the same check.

The third table's fifth row shows that a combination of defenses does not always add up. With
a pinned record and a consistency check both on, the corrupted record count stays at **one**
— the same number the consistency check alone gives. The rejected legitimate reply count,
though, rises from one to **two**, and five manual items are added on top. The fourth table
shows that the record left corrupted **changes**: with consistency alone, the gateway record
stayed corrupted; in the combination, the name server record stays corrupted instead. The
pinned list cuts the first three claims, so the first claim to cross the threshold is now the
name server, fourth in line, and it is accepted. **The record a threshold-based check misses
shifts when the defense ahead of it changes; the count of records missed stays the same.**

## Three Shapes of Cost

The fourth table names the legitimate reply each defense cuts, and the shape of the three
costs is entirely different from one another.

The pinned record's cost is **manual management**: five entries are kept on a list, each is
updated by hand on every address change, and when an address on the list legitimately
changes hands the change is rejected. The table shows exactly this: billing address failover
gets cut. Five entries is a small number, but every new critical address on the network grows
that number, and a forgotten entry stays silently unprotected.

The consistency check's cost is **a false catch**: the third address of a host running three
guest systems is rejected. Raising the threshold to three rescues this legitimate record, but
the same move also accepts the second claim of the row claiming seven addresses. The
threshold shifts both the correct and the incorrect in the same direction.

The cost of accepting only the requested reply is **round-trip count and cut announcements**:
each of the seventeen records requires one request round trip, and all three of the three
legitimate unrequested announcements are rejected. Cutting a failover announcement means the
cache keeps the old record until its lifetime expires; at the moment of failover, part of the
traffic keeps going to the old hardware address.

With all three on together, corrupted records are zero, but the rejected legitimate reply
count rises to four and the record count falls from eighteen to **sixteen**. The two missing
records are two mappings that never form at all: the virtual gateway address and the third
guest system's address. Traffic headed to those two addresses does not go anywhere until a
record forms. **The cost of zeroing corrupted records is not only the rejected reply but the
record that never forms.** A rejected reply leaves a record in the log and can be searched
for; a mapping that never forms leaves no trace at all and is only noticed once that address
turns out to be unreachable.

## Summary

- The address resolution cache's acceptance rule is "the last reply wins"; **6** of 18
  records are corrupted in a set of 26 replies, and all six are addresses carrying traffic
  leaving the segment.
- Being requested and being correct are separate properties: all 17 of 17 requested replies
  are correct, while 3 of 9 unrequested replies are correct and 6 are not.
- A pinned record rescues 4 records for the 5 addresses on its list and leaves the 2 that are
  not; a consistency check rescues 5 records and misses the gateway record, which comes
  first in order.
- Accepting only the requested reply rescues all 6 of 6 records, but it cuts 3 legitimate
  unrequested announcements and adds an extra request round trip for 17 records.
- The three defenses together zero out corrupted records; the cost is 4 rejected legitimate
  replies, 5 manual items, 16 extra round trips, and 2 records that never form at all.

## Next Step

This lesson stayed within one segment: the question asked was "which hardware address does
this network address belong to," and the answer came from the same segment. Every request
that leaves the segment first asks a different question — which address a name corresponds
to. The answer to that question is also written into a cache, but this time the cache is not
local to one segment; it sits at every layer of the network, and a single false record
misdirects not one machine but every client that reads that record. The next lesson builds
and counts the name resolution cache the same way: how many records and how many dependent
services an unverified reply corrupts, how many signed verification rescues, and how many
entries an unsigned zone leaves out in the open.
