Lesson 05 / 20
Address Assignment Risks
Measuring address assignment in claims: 14 endpoints accepting 70 claims without verification at 5 claims per reply, the first-reply-wins rule binding 7 endpoints to a source with no proof, port trust zeroing out misconfiguration while leaving 2 endpoints unconfigured, and pool inspection combined with static assignment correctly configuring the same 12 endpoints with 3 manual items while leaving 2 endpoints misconfigured.
Published Updated
In this lesson
- Counts the claims an address assignment reply carries and measures the outcome of the first-reply-wins rule
- Separately counts the endpoints rescued by port trust, pool inspection, and static assignment
- Compares the endpoints each defense leaves unconfigured against the manual items and rejected legitimate replies
Contents
The previous two lessons shared an assumption: that the endpoint asking the cache a question already knew who to ask. The name resolver’s address, the gateway’s address, and the endpoint’s own address were all taken as given. But when an endpoint joins a network it knows none of these.
Because it does not know, it asks, and at the moment it asks it has no criterion at all: it cannot tell which reply comes from an authoritative source, because the very configuration that would let it tell arrives with that reply. The acceptance rule is therefore very plain — the first reply to arrive wins. When an unauthorized source uses this rule to hand out configuration, this is called a rogue address assignment server (rogue DHCP server). This lesson runs that rule on a data structure and counts how many claims a single reply binds at once.
One Reply, Five Claims
PS13: an address assignment reply carries five claims in a single message — address, gateway, name resolver, lease time, and domain suffix — and the endpoint accepts every one of them without testing. PS14: the endpoint’s acceptance rule is “the first reply wins”; who the source is plays no part in the rule. PS15: a defense can filter a reply only by its arrival path and its content; there is no evidence in the message showing whether the source is authoritative.
PS13 sets this lesson apart from the previous two. In address resolution, what was corrupted was a single mapping; in name resolution, it was a name and the services bound to it. Here, a single acceptance determines the endpoint’s entire behavior on the network: which address it uses, where it sends its outbound traffic, and who it asks for names. The third claim links directly back to the previous lesson: once the name resolver claim is wrongly accepted, all of that endpoint’s name records are resolved again through a source that carries no signed verification at all.
The four-step lease process, lease time and renewal thresholds, and the relay agent’s role were measured in the Network Models and Protocols course; they are not repeated here. The assignment set below is a model: the endpoints in a fictional branch segment and their reply delays are written as data — there is no real network or real device. The set contains no forged assignment step; the code that runs is only the endpoint’s acceptance rule and the segment’s defenses. Performing such a measurement outside a segment under your own administration is both illegal and unprofessional without written authorization and a defined scope.
// networks/assignment.mjs — address assignment in a branch segment of a fictional regional // measurement network (model): no real network or device; addresses from a private block. No randomness. // PS13: an address assignment reply carries five claims in a single message, and the // endpoint accepts every one of them without testing. export const CLAIMS = ["address", "gateway", "name resolver", "lease time", "domain suffix"]; // PS14: the endpoint's acceptance rule is "the first reply wins"; the source does not matter. // Columns: endpoint, delay from source a (ms), from b, from c, whether b's address is // inside the authoritative pool (y/n). "-" means no reply arrived from that source. // a = the segment's authoritative distributor (trusted port), c = backup authoritative // distributor (untrusted port), b = a source with no proof. export const ENDPOINT = ` u01 40 12 - y u02 38 55 - y u03 45 20 - n u04 30 70 - y u05 50 18 - y u06 42 90 - n u07 36 22 - n u08 48 14 - y u09 33 60 - y u10 44 25 - y u11 39 80 - n u12 41 28 - n u13 - 95 30 y u14 - 45 26 y` .trim().split(/\s+/).reduce((l, _, i, a) => (i % 5 ? l : [...l, { id: a[i], delay: { a: a[i + 1], b: a[i + 2], c: a[i + 3] }, inPool: a[i + 4] === "y", }]), []); // PS15: a defense can filter a reply only by its arrival path and its content; there is // no evidence in the message showing whether the source is authoritative. export const STATIC = ["u05", "u08"]; export const PORT = 24; // number of ports in the segment switch where a trust setting is written export const AUTH = ["a", "c"]; export function assign(defenses = []) { const result = ENDPOINT.map((u) => { if (defenses.includes("static") && STATIC.includes(u.id)) return { id: u.id, source: "static", correct: true }; const candidate = Object.entries(u.delay) .filter(([k, v]) => v !== "-" && !(defenses.includes("port") && k !== "a") && !(defenses.includes("pool") && k === "b" && !u.inPool)) .sort((x, y) => +x[1] - +y[1])[0]; return candidate ? { id: u.id, source: candidate[0], correct: AUTH.includes(candidate[0]) } : { id: u.id, source: "-", correct: false }; }); return { result, wrong: result.filter((s) => s.source === "b"), unassigned: result.filter((s) => s.source === "-"), rejectedLegit: ENDPOINT.filter((u) => u.delay.c !== "-" && defenses.includes("port")).length, }; }
// networks/assignment-audit.mjs — how many claims one assignment reply carries, how many endpoints the // first-reply-wins rule binds to which source, how many a defense rescues, and at what cost. import { ENDPOINT, CLAIMS, STATIC, PORT, assign } from "./assignment.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 = assign(), N = ENDPOINT.length, I = CLAIMS.length; console.log(`${N} endpoints, ${I} claims per reply, ${N * I} claims accepted in total ` + `without verification; endpoints choosing an unproven source ${baseline.wrong.length}, ` + `claims accepted that way ${baseline.wrong.length * I}`); const A = [-6, 8, 8, 8, 11, 10]; console.log("\n1. the first reply wins: which source won and by how many ms"); write(A, "id", "a (ms)", "b (ms)", "c (ms)", "b in pool", "winner"); for (const u of ENDPOINT) write(A, u.id, u.delay.a, u.delay.b, u.delay.c, u.inPool ? "yes" : "no", baseline.result.find((s) => s.id === u.id).source); const B = [-22, 19, 17, 17]; console.log("\n2. defenses: endpoints rescued and their cost"); write(B, "defense", "correct endpoints", "wrong endpoints", "unconfigured"); const C = [-22, 14, 16, 8]; const cfg = [[], ["port"], ["pool"], ["static"], ["pool", "static"], ["port", "pool", "static"]]; for (const s of cfg) { const r = assign(s); write(B, s.join(" + ") || "none", r.result.filter((x) => x.correct).length, r.wrong.length, r.unassigned.length); } console.log("\n3. the cost of the same defenses"); write(C, "defense", "wrong claims", "legit rejected", "manual"); for (const s of cfg) { const r = assign(s); write(C, s.join(" + ") || "none", r.wrong.length * I, r.rejectedLegit, (s.includes("port") ? PORT : 0) + (s.includes("pool") ? 1 : 0) + (s.includes("static") ? STATIC.length : 0)); } const D = [-22, -30, -22]; console.log("\n4. which endpoints remain"); write(D, "defense", "chose unproven source", "got no configuration"); for (const s of cfg.slice(1)) { const r = assign(s); write(D, s.join(" + "), r.wrong.map((x) => x.id).join(" ") || "none", r.unassigned.map((x) => x.id).join(" ") || "none"); }
14 endpoints, 5 claims per reply, 70 claims accepted in total without verification; endpoints choosing an unproven source 7, claims accepted that way 35 1. the first reply wins: which source won and by how many ms id a (ms) b (ms) c (ms) b in pool winner u01 40 12 - yes b u02 38 55 - yes a u03 45 20 - no b u04 30 70 - yes a u05 50 18 - yes b u06 42 90 - no a u07 36 22 - no b u08 48 14 - yes b u09 33 60 - yes a u10 44 25 - yes b u11 39 80 - no a u12 41 28 - no b u13 - 95 30 yes c u14 - 45 26 yes c 2. defenses: endpoints rescued and their cost defense correct endpoints wrong endpoints unconfigured none 7 7 0 port 12 0 2 pool 10 4 0 static 9 5 0 pool + static 12 2 0 port + pool + static 12 0 2 3. the cost of the same defenses defense wrong claims legit rejected manual none 35 0 0 port 0 2 24 pool 20 0 1 static 25 0 2 pool + static 10 0 3 port + pool + static 0 2 27 4. which endpoints remain defense chose unproven source got no configuration port none u13 u14 pool u01 u05 u08 u10 none static u01 u03 u07 u10 u12 none pool + static u01 u10 none port + pool + static none u13 u14
The numbers are in the measurement class; their inputs are PS13–PS15.
Seven Endpoints, Thirty-Five Claims
Fourteen endpoints accept five claims per reply: 70 claims in total, none of them backed by proof. With no defense in place, seven of these endpoints receive the reply of a source with no proof, and all five claims of that reply are accepted too — 35 claims. Half of the total.
The first table gives the winner in milliseconds and shows how narrow a margin the rule works
on. The authoritative distributor’s delay falls between 30 and 50 ms; the unproven source’s
delay spreads between 12 and 90 ms. The winning criterion is not who the source is, only which
of these two numbers is smaller. u02 accepts the authoritative source at 38 ms, u12 accepts
the unproven source at 28 ms, and the only difference between the two is ten milliseconds.
Two endpoints — u13 and u14 — receive no reply at all from the authoritative distributor
and select the backup authoritative distributor instead. These two rows are the entry that will
determine the defense’s cost in the next section: the backup distributor is legitimate, but it
sits on an untrusted port.
The table’s third column also carries the one signal the defending side can actually see. The seven endpoints that chose the unproven source have delays between 12 and 28 ms; the five that did not have delays between 55 and 90 ms. There is no overlap between the two sets, and the gap between them is 27 ms. A two-peaked, rather than single-peaked, distribution of reply delays is a countable sign that two separate sources answered on the segment. The signal does not say who the source is; it only says that replies arrived from two different places, and this is a concrete quantity that can be searched for in assignment logs.
Three Defenses, Two Different Outcomes
The second table runs the three defenses separately and together. Port trust lets through only
replies arriving from the trusted port: the count of misconfigured endpoints drops from seven
to zero. The count of correctly configured endpoints, however, rises not to fourteen but to
twelve. The two endpoints in between are u13 and u14, bound to the backup authoritative
distributor, and they receive no configuration at all.
Pool inspection looks at the reply’s content: it rejects a reply that proposes an address outside the authoritative pool. It rescues three of the seven wrong endpoints and leaves four. The four it leaves are replies that propose an address from inside the pool, and content inspection cannot tell them apart. Static assignment puts two critical endpoints on a list and rescues those two under every condition; it leaves the remaining five. Static assignment’s coverage, just like the static record in the third lesson, is exactly as long as the list: two entries written, two endpoints rescued. Extending the list to all fourteen endpoints would zero out misconfiguration, but at that point the assignment mechanism itself is no longer being used, and every new endpoint added to the network demands one manual entry.
The fifth row is this lesson’s point of comparison. Pool inspection combined with static assignment correctly configures twelve endpoints — the exact number port trust reaches on its own. The same outcome is reached by two separate paths, and what each path leaves behind is entirely different: one misconfigures two endpoints, the other leaves two endpoints with no configuration at all.
The Cost of Two Remainders
The third table puts the cost of these two paths side by side. Port trust requires 24 manual entries — one trust setting for every port on the segment switch — and rejects two legitimate replies. Pool inspection combined with static assignment requires three manual items and rejects no legitimate reply at all; in exchange, ten claims remain accepted without verification.
The gap between twenty-four entries and three entries is a gap that widens as scale grows: every new port adds one entry on the first path and adds nothing on the second. Against that, the two endpoints the second path leaves behind grow in number as scale grows, and each one carries five claims.
The nature of the two remainders differs too. An endpoint that gets no configuration does not stay silent: it never joins the network, which is visible and gets fixed. A misconfigured endpoint, on the other hand, works — its traffic flows, it carries out its function, and because none of its five claims is ever tested, it leaves no record at all. Whether a defense’s remainder is visible matters as much as how many it leaves.
The last row turns all three on at once and zeroes out misconfiguration. Its cost is 27 manual entries and two rejected legitimate replies; two endpoints still remain unconfigured. Static assignment adds nothing in this combination, because port trust has already rescued the two endpoints it would have rescued — the same layered-defense pattern from the third lesson.
The weight of the five wrongly accepted claims is not equal either. The third claim — the name resolver — invalidates the entire previous lesson’s measurement for that endpoint: all sixteen of the sixteen names now come from a source that endpoint never tested, and even the three records signed verification rescued change with the reply that source gives. The lease time claim is the lightest; it only determines how long the misconfiguration stays in place before renewal. The five claims carried in one message count as a single number, but their costs are not the same size; when prioritizing, note how many downstream measurements each claim determines.
Summary
- An address assignment reply carries 5 claims in a single message; 14 endpoints accept 70 claims in total without testing, and for one endpoint this single acceptance determines its address, its outbound path, and its name resolver all at once.
- The acceptance rule is “the first reply wins”: 7 endpoints select a source with no proof and 35 claims come from that source; the winning margin is as little as 10 ms on some endpoints.
- Port trust zeroes out misconfiguration but leaves 2 endpoints bound to a legitimate backup distributor unconfigured, and requires 24 manual items.
- Pool inspection combined with static assignment correctly configures the same 12 endpoints, requires 3 manual items, rejects no legitimate reply, and leaves 2 endpoints misconfigured.
- The nature of the two remainders differs: an unconfigured endpoint is visible because it never joins the network, while a misconfigured endpoint leaves no record because it keeps working.
Next Step
The four lessons so far measured a claim being accepted wrongly: a corrupted record, a wrong address, a wrong source. In every one of these measurements the target service stayed up and kept answering requests. But an unverified claim does not only produce a wrong reply; it can also lead to producing no reply at all. If a connection-opening request does not prove its source, the target has no way of knowing whom it is reserving its held space for, and once that space runs out, legitimate requests are left out too. The next lesson measures this with capacity arithmetic: how many requests from how many sources cross the threshold, how many seconds it takes for the state table to fill, which defense lowers which rate, and how much of the legitimate traffic that same defense also lowers.
To keep your progress and take notes, Log in
My notes
Log in to take notes.