---
title: 'Address Assignment Risks'
source: 'https://academia.sh/en/courses/network-security/address-assignment-risks'
course: 'Network Security'
language: en
updated: '2026-10-21T05:02:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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**.

```js
// 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,
  };
}
```

```js
// 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.
