Ethernet · Module 12
Source-Address Learning
A switch is never told where anything is. Every entry is inferred from a source address nothing authenticates, and the whole design is about how long that inference is trusted and what bounds the damage when it is wrong.
Chapter 12.1 treated the forwarding table as given. lookup_hit and lookup_port arrived from somewhere; learn_request went somewhere. This chapter is that somewhere.
Nobody configures it. A switch out of its box, with no management interface touched, forwards correctly within milliseconds of the first frame. There is no discovery protocol, no registration, no address server, and no exchange of any kind between a switch and the stations attached to it.
The entire mechanism is one inference, applied to every frame that arrives:
A frame with source address
Sarrived on portP. ThereforeSis reachable throughP.
That is the whole of MAC learning. Everything else in this chapter — ageing, move detection, rate limiting, capacity — exists because that inference is not a fact. It is a conclusion drawn from evidence that was true when it was observed, about a network that is free to change afterwards, from a field that no part of Ethernet checks.
1. Scope — What This Chapter Owns
This chapter owns where the forwarding table's contents come from: the inference, the eligibility rules that decide which frames are evidence, the ageing that expires an entry, the move that relocates one, and the rate limit that bounds what a dishonest source can do.
It does not own the lookup. Chapter 12.1 §7 established the three outcomes and Chapter 12.3 owns the destination lookup and the ingress-port filter in full.
It does not own flooding — Chapter 12.4 owns what a miss costs and why flooding is the only safe answer.
And it does not own the table's hardware. Chapter 12.5 owns CAM structures, hashing, associativity and what happens on a hash collision. The store in Section 6 is deliberately behavioural: enough to hold entries and age them, not enough to be a real MAC table.
Addressing is assumed. Chapter 5.3 established the 48-bit format and the I/G bit; Chapter 5.4 established individual, group and broadcast. Both are load-bearing here — the I/G bit is the first eligibility rule, and it is the rule most often omitted.
2. The Inference, and Why It Is Sound Enough to Build On
Start with why the inference works at all, because it is not obvious that it should.
A frame carries two addresses and the switch reads both, for entirely different purposes. The destination address is a question — where should this go? The source address is an answer to a question nobody asked — where did this come from?
And the answer is usable only because of a property of Ethernet that nothing in the frame states: a frame is never emitted back toward its own source. Chapter 12.1's P9 asserted it, a station does not receive its own transmissions, and no correct switch echoes a frame to its ingress port. So a frame arriving at port P bearing source S cannot have been sent by a station on any other port of this switch — if it had been, it would have arrived on that port's link instead.
So the inference is sound given two assumptions, and it is worth naming them because both are load-bearing and neither is enforced.
Assumption one: the topology has no loop. In a loop the same frame arrives at two ports, and the inference produces two contradictory entries in rapid alternation.
Assumption two: the source address field is truthful. Nothing in Ethernet makes it so. A station writes 48 bits of its own choosing into that field and no receiver, switch or standard checks them against anything.
The first assumption is why a switched network needs a loop-prevention mechanism at all. The second is Section 16's rejected property.
3. Why This Needs No Configuration, and What That Cost
Compare against the alternative that was not chosen.
| learning from source addresses | a registration protocol | |
|---|---|---|
| configuration required | none | every station configured, or a server run |
| new station works | on its first transmitted frame | after a registration exchange |
| a station that never transmits | never learned — floods forever | registered like any other |
| a station that leaves | detected by timeout only | deregistration message |
| a station claiming another's address | accepted | rejectable |
| cost per frame | one table write | none, plus protocol machinery |
| stations that must implement it | zero | all of them |
The bottom row is why it won. A learning switch works with every station ever built, including stations designed decades before switching existed, because it requires nothing of them. The station transmits an ordinary frame; the switch learns from a field the station was already obliged to fill in.
And the price is the rest of this chapter. No registration means no deregistration, so entries must expire on a timer. No exchange means no confirmation, so a station's silence is indistinguishable from its absence. No authentication means no defence, so the only available protection is to bound the rate at which the mechanism can be exploited.
4. RTL 1 — Deciding Which Frames Are Evidence
Not every arriving frame is evidence, and the rules that exclude the rest are short, absolute, and the first thing a learning implementation gets wrong.
// -----------------------------------------------------------------------
// maclearn_pkg -- shared types for source-address learning.
// -----------------------------------------------------------------------
package maclearn_pkg;
// Why a frame was refused as evidence. Every refusal is one of these,
// and each has a different operational meaning.
typedef enum logic [2:0] {
LE_OK = 3'd0, // eligible -- learn from it
LE_BAD_FCS = 3'd1, // frame failed Chapter 7.3's check
LE_RUNT = 3'd2, // shorter than 64 octets
LE_GROUP_SRC = 3'd3, // I/G set in the SOURCE address
LE_NULL_SRC = 3'd4, // all-zero source
LE_RESERVED_SRC = 3'd5, // a reserved group range, defensively
LE_PORT_DOWN = 3'd6 // ingress port not in a learning state
} learn_elig_e;
// What the store did with a learn request. The distinction between
// INSERT, REFRESH and MOVE is the whole of this chapter's telemetry.
typedef enum logic [2:0] {
LR_INSERT = 3'd0, // new address, empty slot found
LR_REFRESH = 3'd1, // known address, same port -- age reset only
LR_MOVE = 3'd2, // known address, DIFFERENT port
LR_EVICT = 3'd3, // new address, slot taken -- something displaced
LR_REJECT = 3'd4, // refused -- rate limit or table full
LR_NONE = 3'd5
} learn_result_e;
localparam int ADDR_W = 48;
endpackage// -----------------------------------------------------------------------
// source_address_learner -- applies the eligibility rules to one frame and
// emits at most one learn request.
//
// The rules are short and every one of them has been omitted by a real
// implementation at some point. Each omission puts a specific class of
// garbage into the forwarding table.
// -----------------------------------------------------------------------
module source_address_learner
import maclearn_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int PORT_ID = 0
)(
input logic clk,
input logic rst_n,
// One pulse per received frame, with the frame's outcome already known
// because this design is store-and-forward -- Chapter 12.1 Section 10.
input logic frame_valid,
input logic [ADDR_W-1:0] frame_src,
input logic frame_bad_fcs,
input logic frame_runt,
input logic port_learning_enabled,
output logic learn_valid,
output logic [ADDR_W-1:0] learn_addr,
output logic [PORT_BITS-1:0] learn_port,
output learn_elig_e refusal
);
// RULE 1. The I/G bit of the SOURCE address. A group address can never
// legitimately be a source -- nothing transmits as a group. Learning it
// puts a multicast address in the table, where a later DESTINATION
// lookup for that same group address will HIT and forward a multicast
// frame out one port instead of flooding it.
logic src_is_group;
assign src_is_group = frame_src[40];
// RULE 2. The all-zero source. Not a legal address, and a common
// signature of a malformed or truncated frame.
logic src_is_null;
assign src_is_null = (frame_src == '0);
// RULE 3. Reserved group range 01-80-C2-00-00-00 .. 0F. Control frames
// use these as DESTINATIONS; one appearing as a source is malformed.
// Rule 1 already covers it -- this is defence in depth, because the
// consequence of learning a control address is severe.
logic src_is_reserved;
assign src_is_reserved =
(frame_src[47:8] == 40'h01_80_C2_00_00) && (frame_src[7:4] == 4'h0);
always_comb begin
learn_valid = 1'b0;
refusal = LE_OK;
if (frame_valid) begin
// RULE 4. A frame that failed its check sequence is not evidence of
// anything -- the source address field itself may be the corrupted
// part. Learning from it inserts a fictitious address that will
// displace a real one.
if (frame_bad_fcs) refusal = LE_BAD_FCS;
else if (frame_runt) refusal = LE_RUNT;
else if (src_is_group) refusal = LE_GROUP_SRC;
else if (src_is_null) refusal = LE_NULL_SRC;
else if (src_is_reserved) refusal = LE_RESERVED_SRC;
else if (!port_learning_enabled) refusal = LE_PORT_DOWN;
else begin
learn_valid = 1'b1;
end
end
end
assign learn_addr = frame_src;
assign learn_port = PORT_BITS'(PORT_ID);
endmoduleClassification: synthesizable.
What it teaches: that eligibility is decided before the frame's forwarding outcome is known and independently of it. A frame that will be filtered, flooded or dropped for congestion is still evidence. The only frames excluded are the ones whose source address field cannot be trusted to mean anything — and note that "cannot be trusted" here means malformed, not dishonest. A perfectly well-formed frame carrying a stolen source address passes all six rules, which is the point Section 16 turns on.
Deliberately simplified: the frame arrives as a single valid pulse with its checks already resolved, which is what Chapter 12.1 §10's store-and-forward discipline makes possible. A cut-through switch cannot do this — it must decide whether to learn before it has seen the FCS, so it either learns from frames that turn out to be corrupt or defers learning until the frame has fully arrived, giving up part of the latency advantage it exists for.
Production implication: refusal is a counter set, not a status bit, and the four values it can take are diagnostically distinct. A rising LE_GROUP_SRC count means a station is emitting frames with a multicast source address, which is malformed and worth finding. A rising LE_BAD_FCS count on one port localises a cabling fault more precisely than the port's own error counter, because it counts only frames that made it far enough to have a readable source. And LE_PORT_DOWN rising on a port that is up means a port state machine has not released learning — a real class of bug in which a port forwards but never learns, so everything it sends floods forever.
5. What Each Omitted Rule Actually Does
All six rules look like defensive boilerplate. Each one, omitted, produces a distinct and hard-to-diagnose failure.
| Omitted rule | What enters the table | The symptom, and why it is hard |
|---|---|---|
| I/G bit on the source | a multicast address, bound to one port | multicast frames to that group are forwarded to one port instead of flooded — most receivers silently stop getting them, and the switch reports no error |
| bad FCS | a fictitious address, from corrupted bits | a real entry is displaced by a ghost; traffic to the real station floods until it transmits again |
| runt | whatever the fragment's first octets were | same as above, at whatever rate the faulty link produces fragments |
| all-zero source | address 00:00:00:00:00:00 | harmless until something sends to it, then a flood becomes a forward |
| reserved range | a control-protocol address | control frames become forwarded rather than flooded, breaking protocols that assume every port sees them |
| port learning state | correct entries, at the wrong time | a port that is still transitioning learns addresses that will move again immediately — a self-inflicted move storm at bring-up |
6. RTL 2 — The Store, and What an Entry Has to Carry
An entry needs more than an address and a port, and the extra fields are where every operational question in this chapter is answered.
// -----------------------------------------------------------------------
// learning_store -- a behavioural forwarding table: insert, refresh, move,
// lookup and age.
//
// DELIBERATELY NOT A REAL MAC TABLE. A production table is a CAM or a
// hashed set-associative structure with collision handling -- Chapter 12.5
// owns that entirely. What this module is for is the ENTRY: what a row has
// to carry, and why each field exists.
// -----------------------------------------------------------------------
module learning_store
import maclearn_pkg::*;
#(
parameter int N_ENTRIES = 8192,
parameter int PORT_BITS = 5,
parameter int AGE_W = 9, // 300 s at a 1 s tick needs 9 bits
parameter int IDX_W = 13 // $clog2(8192)
)(
input logic clk,
input logic rst_n,
// Learn side.
input logic learn_valid,
input logic [ADDR_W-1:0] learn_addr,
input logic [PORT_BITS-1:0] learn_port,
output learn_result_e learn_result,
output logic [PORT_BITS-1:0] displaced_port, // valid on LR_MOVE
// Lookup side -- Chapter 12.3 consumes this.
input logic lookup_valid,
input logic [ADDR_W-1:0] lookup_addr,
output logic lookup_hit,
output logic [PORT_BITS-1:0] lookup_port,
// Ageing tick, one pulse per second.
input logic age_tick,
input logic [AGE_W-1:0] age_limit, // 300 by default
output logic [IDX_W:0] occupancy,
output logic table_full
);
// An entry. Five fields, and only the first two are about forwarding.
typedef struct packed {
logic valid;
logic [ADDR_W-1:0] addr;
logic [PORT_BITS-1:0] port;
logic [AGE_W-1:0] age; // seconds since last SOURCE sighting
logic stat; // statically configured -- never ages
} entry_t;
entry_t tbl [N_ENTRIES];
// Behavioural associative search. A real table hashes; Chapter 12.5
// explains why, and what a hash collision costs.
function automatic logic [IDX_W:0] find(input logic [ADDR_W-1:0] a);
find = {1'b1, {IDX_W{1'b0}}}; // MSB set == not found
for (int i = 0; i < N_ENTRIES; i++)
if (tbl[i].valid && (tbl[i].addr == a)) find = IDX_W'(i);
endfunction
function automatic logic [IDX_W:0] free_slot();
free_slot = {1'b1, {IDX_W{1'b0}}};
for (int i = N_ENTRIES-1; i >= 0; i--)
if (!tbl[i].valid) free_slot = IDX_W'(i);
endfunction
logic [IDX_W:0] hit_idx, new_idx;
logic [IDX_W:0] used;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < N_ENTRIES; i++) tbl[i] <= '0;
learn_result <= LR_NONE;
displaced_port <= '0;
end else begin
learn_result <= LR_NONE;
// ---- Learn -------------------------------------------------------
if (learn_valid) begin
hit_idx = find(learn_addr);
if (!hit_idx[IDX_W]) begin
// The address is known. Two very different cases.
if (tbl[hit_idx[IDX_W-1:0]].port == learn_port) begin
// REFRESH. The station is where we thought. Reset the age --
// and note that this is the ONLY thing that resets it.
tbl[hit_idx[IDX_W-1:0]].age <= '0;
learn_result <= LR_REFRESH;
end else begin
// MOVE. The station is somewhere else now, OR something is
// claiming to be it, OR there is a loop. The table cannot
// tell these apart -- Section 10.
displaced_port <= tbl[hit_idx[IDX_W-1:0]].port;
tbl[hit_idx[IDX_W-1:0]].port <= learn_port;
tbl[hit_idx[IDX_W-1:0]].age <= '0;
learn_result <= LR_MOVE;
end
end else begin
// Unknown address. Insert if there is room.
new_idx = free_slot();
if (!new_idx[IDX_W]) begin
tbl[new_idx[IDX_W-1:0]].valid <= 1'b1;
tbl[new_idx[IDX_W-1:0]].addr <= learn_addr;
tbl[new_idx[IDX_W-1:0]].port <= learn_port;
tbl[new_idx[IDX_W-1:0]].age <= '0;
tbl[new_idx[IDX_W-1:0]].stat <= 1'b0;
learn_result <= LR_INSERT;
end else begin
// Full. This design REFUSES rather than evicting a live
// entry, so a flood of new addresses cannot displace the
// stations that are actually communicating.
learn_result <= LR_REJECT;
end
end
end
// ---- Age ---------------------------------------------------------
// One second per tick, every entry. A statically configured entry
// is exempt: it was asserted by an operator, not inferred.
if (age_tick) begin
for (int i = 0; i < N_ENTRIES; i++) begin
if (tbl[i].valid && !tbl[i].stat) begin
if (tbl[i].age >= age_limit) tbl[i].valid <= 1'b0;
else tbl[i].age <= tbl[i].age + 1'b1;
end
end
end
end
end
// ---- Lookup ---------------------------------------------------------
// NOTE what is NOT here: a lookup does not touch the age field. A hit
// is not evidence about the destination -- Section 8.
always_comb begin
logic [IDX_W:0] q;
lookup_hit = 1'b0;
lookup_port = '0;
if (lookup_valid) begin
q = find(lookup_addr);
if (!q[IDX_W]) begin
lookup_hit = 1'b1;
lookup_port = tbl[q[IDX_W-1:0]].port;
end
end
end
always_comb begin
used = '0;
for (int i = 0; i < N_ENTRIES; i++) if (tbl[i].valid) used = used + 1'b1;
end
assign occupancy = used;
assign table_full = (used >= (IDX_W+1)'(N_ENTRIES));
endmoduleClassification: behavioural — the linear search and the full-table age sweep are illustrative, not synthesizable at 8192 entries.
What it teaches: that an entry carries five fields and only two of them answer the forwarding question. addr and port are the answer; age is how long the answer has been trusted, valid is whether it is still trusted at all, and stat marks the one kind of entry that was not inferred — an operator asserted it, so no timer may retract it.
And it teaches the three-way split on a learn. LR_INSERT, LR_REFRESH and LR_MOVE all end with an entry naming the right port, and they mean completely different things about the network. Inserts are new stations. Refreshes are normal traffic. Moves are either a station that relocated, a duplicate address, or a loop — and a healthy network produces almost none of them.
Deliberately simplified: the linear find is O(N_ENTRIES); a real table hashes the address into a small set and searches only that set, which is why Chapter 12.5 spends a chapter on collisions. The age sweep here touches every entry on every tick — a real design walks the table incrementally. At 8192 entries and one entry per cycle at 500 MHz a full sweep takes 8192 ÷ 500 M = 16.4 µs, so a once-per-second sweep occupies 0.0016% of the table's bandwidth and can be interleaved with lookups without a dedicated port.
Production implication: the refusal-on-full behaviour is a policy decision with a security consequence, and the alternative is worse. A table that evicts to make room lets an attacker displace real entries by inventing addresses; a table that refuses lets the attacker prevent new legitimate entries but cannot remove existing ones. Section 12 shows why refusing is the right side of that trade, and why neither is sufficient without Section 11's rate limiter.
7. RTL 3 — Ageing, and the Timer That Is Not Garbage Collection
Ageing is usually explained as cleaning up stale entries. That framing gets the mechanism backwards and hides why the timer's value matters.
// -----------------------------------------------------------------------
// entry_age_manager -- generates the ageing tick and reports what ageing is
// actually doing, which is the number nobody looks at.
//
// The default is 300 s, from 802.1D. This module exists to make visible
// that the interval is a TRADE, not a constant, and to report which side
// of the trade the current setting is landing on.
// -----------------------------------------------------------------------
module entry_age_manager #(
parameter int CLK_HZ = 125_000_000,
parameter int AGE_W = 9,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic [AGE_W-1:0] age_limit, // seconds, 300 by default
output logic age_tick, // one pulse per second
// Observability -- what ageing costs and what it is preventing.
input logic entry_expired, // pulse from the store
input logic lookup_miss, // pulse from the lookup
input logic lookup_hit,
output logic [CNT_W-1:0] c_expired,
output logic [CNT_W-1:0] c_miss,
output logic [CNT_W-1:0] c_hit,
output logic miss_rate_high, // ageing may be too aggressive
output logic ageing_idle // ageing may be too slow
);
localparam int TICK_MAX = CLK_HZ - 1;
logic [$clog2(CLK_HZ)-1:0] div_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
div_q <= '0;
age_tick <= 1'b0;
end else begin
age_tick <= 1'b0;
if (div_q == $clog2(CLK_HZ)'(TICK_MAX)) begin
div_q <= '0;
age_tick <= 1'b1;
end else begin
div_q <= div_q + 1'b1;
end
end
end
logic [CNT_W-1:0] win_hit, win_miss, win_exp;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_expired <= '0; c_miss <= '0; c_hit <= '0;
win_hit <= '0; win_miss <= '0; win_exp <= '0;
miss_rate_high <= 1'b0;
ageing_idle <= 1'b0;
end else begin
if (entry_expired) begin
if (!(&c_expired)) c_expired <= c_expired + 1'b1;
win_exp <= win_exp + 1'b1;
end
if (lookup_miss) begin
if (!(&c_miss)) c_miss <= c_miss + 1'b1;
win_miss <= win_miss + 1'b1;
end
if (lookup_hit) begin
if (!(&c_hit)) c_hit <= c_hit + 1'b1;
win_hit <= win_hit + 1'b1;
end
// Evaluate once a second, on the same tick that ages the table.
if (age_tick) begin
// MORE THAN A TENTH OF LOOKUPS MISSING is not normal steady-state
// behaviour. In a stable network almost every destination is
// known, and a high miss rate with a non-full table means entries
// are expiring faster than their stations transmit.
miss_rate_high <= (win_miss > (win_hit >> 3)) &&
(win_miss > CNT_W'(100));
// Expiries at zero over a long period with a filling table means
// the interval is longer than anything ever goes quiet for --
// stale entries will accumulate until the table is full.
ageing_idle <= (win_exp == '0) && (win_hit > CNT_W'(10_000));
win_hit <= '0; win_miss <= '0; win_exp <= '0;
end
end
end
// Unused in this reduced module, kept to document the interface.
wire _unused_age_limit = |age_limit;
endmoduleClassification: synthesizable.
What it teaches: that the ageing interval is a two-sided trade and both sides are measurable. Too short and entries for stations that transmit infrequently expire between their transmissions, so traffic to them floods — visible as miss_rate_high on a table that is nowhere near full. Too long and entries for departed stations persist, consuming capacity and, worse, continuing to answer lookups with a port the station has left — visible as ageing_idle.
Deliberately simplified: one global interval. Real switches allow per-VLAN and per-port intervals, and reduce the interval dramatically on a topology change, because after a change every entry's port information is suspect at once.
Production implication: miss_rate_high catches a configuration failure that looks exactly like a capacity failure. An operator who shortens the ageing time to "keep the table fresh" converts a working network into one where a large fraction of unicast traffic floods — every port carries traffic meant for one port, Chapter 12.1's 48× advantage erodes toward a hub's, and the switch reports a valid link and zero errors on every port throughout.
8. Why the Age Resets on Transmit and Not on Lookup
Look again at the store in Section 6: lookup does not touch age. Only a learn does. That asymmetry is deliberate and it is the single most-missed detail in MAC learning.
The tempting alternative is to treat a lookup hit as activity — the station is being talked to, so it must still be there, so refresh its entry. It is what a cache does, and it is wrong here.
Because a lookup hit is not evidence about the destination. It is evidence about the source of the frame doing the looking up.
Work the failure through. Station A has been unplugged. Station B keeps sending to A — a stale ARP entry, a retrying TCP connection, a monitoring poll. Every one of B's frames produces a lookup for A, and a hit.
| ageing refreshes on lookup | ageing refreshes on transmit | |
|---|---|---|
| A's entry after A is unplugged | refreshed by every frame B sends | ages normally |
| A's entry after 300 s | still present, still naming the dead port | expired |
| B's frames to A | forwarded to the port A left | flooded — reaching A wherever it is now |
| if A returns on a different port | entry says the old port until A transmits | already correct, or corrected on flood reply |
| the entry's justification | B's belief that A exists | A's own transmission |
The bottom row is the principle. An entry is a claim about S, and only S can produce evidence for it. Refreshing on lookup lets one station's stale assumption keep another station's entry alive indefinitely — and the entry it keeps alive is precisely the one that is wrong.
9. RTL 4 — Detecting That a Station Moved
A move is the store's third outcome, and it is the one that carries information the network operator needs. It is also the one an attacker produces.
// -----------------------------------------------------------------------
// station_move_detector -- watches LR_MOVE events and separates the three
// causes that produce them, which the store itself cannot distinguish.
//
// A legitimate move happens ONCE. A duplicate address alternates between
// two ports at the rate the two stations transmit. A loop alternates at
// close to line rate.
// -----------------------------------------------------------------------
module station_move_detector
import maclearn_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int CNT_W = 32,
parameter int FLAP_LIMIT = 10 // moves per second before it is a flap
)(
input logic clk,
input logic rst_n,
input logic learn_done,
input learn_result_e learn_result,
input logic [ADDR_W-1:0] learn_addr,
input logic [PORT_BITS-1:0] learn_port,
input logic [PORT_BITS-1:0] displaced_port,
input logic second_tick,
output logic [CNT_W-1:0] c_moves,
output logic [ADDR_W-1:0] last_moved_addr,
output logic [PORT_BITS-1:0] last_moved_from,
output logic [PORT_BITS-1:0] last_moved_to,
output logic flapping, // >FLAP_LIMIT moves in 1 s
output logic [ADDR_W-1:0] flapping_addr,
output logic [1:0] likely_cause // 0 none 1 move 2 dup 3 loop
);
logic [CNT_W-1:0] moves_this_second;
logic [ADDR_W-1:0] watch_addr;
logic [CNT_W-1:0] watch_moves;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_moves <= '0;
moves_this_second <= '0;
watch_addr <= '0;
watch_moves <= '0;
last_moved_addr <= '0;
last_moved_from <= '0;
last_moved_to <= '0;
flapping <= 1'b0;
flapping_addr <= '0;
likely_cause <= 2'd0;
end else begin
if (learn_done && (learn_result == LR_MOVE)) begin
if (!(&c_moves)) c_moves <= c_moves + 1'b1;
last_moved_addr <= learn_addr;
last_moved_from <= displaced_port;
last_moved_to <= learn_port;
moves_this_second <= moves_this_second + 1'b1;
// Track ONE address at a time. Which address is flapping matters
// far more than how many are, because one flapping address is a
// duplicate and many at once is a loop.
if (learn_addr == watch_addr) watch_moves <= watch_moves + 1'b1;
else begin
watch_addr <= learn_addr;
watch_moves <= CNT_W'(1);
end
end
if (second_tick) begin
flapping <= (moves_this_second > CNT_W'(FLAP_LIMIT));
if (moves_this_second > CNT_W'(FLAP_LIMIT)) flapping_addr <= watch_addr;
// The rate discriminates the cause. Below the flap limit, a move
// is a move. A handful per second on ONE address is two stations
// sharing it. Thousands per second, or many addresses at once,
// is a topology loop -- the same frames arriving from two
// directions.
if (moves_this_second == '0)
likely_cause <= 2'd0;
else if (moves_this_second <= CNT_W'(FLAP_LIMIT))
likely_cause <= 2'd1; // a real move
else if (watch_moves > (moves_this_second >> 1))
likely_cause <= 2'd2; // one address, duplicate
else
likely_cause <= 2'd3; // many addresses, loop
moves_this_second <= '0;
watch_moves <= '0;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the three causes of a move are separated by rate and by concentration, not by anything in the frames. A laptop unplugged from one port and plugged into another produces one move — a single LR_MOVE, and then refreshes on the new port forever. Two stations configured with the same address produce a move every time either transmits, alternating between two ports at the rate of their traffic, all on one address. A topology loop produces moves at close to line rate — 1.4881 M per second per gigabit port — across many addresses at once, because every frame in the loop arrives twice.
Deliberately simplified: one watched address. A production detector maintains a small set of recently-moved addresses so that several simultaneous duplicates can be identified rather than aliasing into the loop verdict.
Production implication: last_moved_from and last_moved_to name the two ports involved, and that pair is the diagnosis. For a duplicate address, the two ports are where the two conflicting stations are — an operator can go and unplug one. For a loop, the pair identifies the two ports the loop passes through. Without recording the displaced port, a move counter tells an operator that something is wrong and nothing about where, which is the same distinction Chapter 12.1 §11 drew between c_drops and drops_by_ingress.
10. A Move, a Duplicate and a Loop Are Identical for One Frame
The store cannot distinguish them and neither can any single frame. Only the pattern over time separates them, which is why this belongs in hardware telemetry rather than in the forwarding path.
| legitimate move | duplicate address | topology loop | |
|---|---|---|---|
| what happened | one station physically relocated | two stations, same 48 bits | one frame arriving twice |
| moves per second | one, then none | a few — the two stations' traffic rate | thousands — close to 1.4881 M |
| addresses involved | one | one | many at once |
| ports involved | old and new | the two stations' ports | the two loop ports, for every address |
| what the table does | correct after the first frame | alternates forever | alternates for everything |
| user-visible symptom | brief flooding, then normal | intermittent unreachability for both | everything slow, broadcast storm |
| what the switch reports | nothing | nothing | nothing — links valid, errors zero |
The last row is why likely_cause exists. All three produce a functioning switch by every measure a link-level statistic can offer.
11. RTL 5 — Bounding What an Unverified Premise Can Do
Section 2's second assumption — that the source address is truthful — cannot be enforced. What can be enforced is a bound on how fast the table can be filled with addresses that are not.
// -----------------------------------------------------------------------
// learning_rate_limiter -- caps NEW address insertions per port per second.
//
// This does not verify anything. Nothing can. What it does is make the
// arithmetic in Section 12 come out the other way: with a low enough cap,
// ageing reclaims entries faster than a single port can create them, so
// the table cannot be filled from one port no matter how long the attempt
// runs.
// -----------------------------------------------------------------------
module learning_rate_limiter
import maclearn_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int N_PORTS = 24,
parameter int NEW_PER_SEC = 25, // derived in Section 12
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic learn_req,
input logic [PORT_BITS-1:0] learn_port,
input logic addr_is_known, // a refresh or a move
input logic second_tick,
output logic learn_permit,
output logic [CNT_W-1:0] c_limited,
output logic [PORT_BITS-1:0] worst_port,
output logic limit_engaged
);
// Budget is per PORT. A shared budget would let one abusive port starve
// every other port's legitimate learning, which is exactly the
// head-of-line pathology Chapter 12.1 Section 6 rejected for congestion.
logic [CNT_W-1:0] new_this_second [N_PORTS];
logic [CNT_W-1:0] limited_by_port [N_PORTS];
// A KNOWN address is always permitted: refreshes and moves do not
// consume table capacity, so limiting them would break real traffic
// while doing nothing about the attack.
always_comb begin
learn_permit = 1'b0;
if (learn_req) begin
if (addr_is_known)
learn_permit = 1'b1;
else
learn_permit = (new_this_second[learn_port] < CNT_W'(NEW_PER_SEC));
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int p = 0; p < N_PORTS; p++) begin
new_this_second[p] <= '0;
limited_by_port[p] <= '0;
end
c_limited <= '0;
worst_port <= '0;
limit_engaged <= 1'b0;
end else begin
if (learn_req && !addr_is_known) begin
if (learn_permit) begin
new_this_second[learn_port] <= new_this_second[learn_port] + 1'b1;
end else begin
if (!(&c_limited)) c_limited <= c_limited + 1'b1;
limited_by_port[learn_port] <= limited_by_port[learn_port] + 1'b1;
end
end
if (second_tick) begin
automatic logic [CNT_W-1:0] hi = '0;
automatic logic [PORT_BITS-1:0] hp = '0;
for (int p = 0; p < N_PORTS; p++) begin
if (limited_by_port[p] > hi) begin
hi = limited_by_port[p];
hp = PORT_BITS'(p);
end
new_this_second[p] <= '0;
end
worst_port <= hp;
limit_engaged <= (hi != '0);
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the defence against an unverifiable premise is not verification — it is rate. The limiter cannot tell a forged source address from a real one, and it does not try. It observes that a legitimate port sees a handful of new addresses per second at most, and that anything faster is either a genuine mass event or an attack, and treats both the same way.
And it teaches why the budget is per port and why known addresses are exempt. A shared budget would let one abusive port exhaust the allowance that every other port's legitimate stations need — the same head-of-line pathology Chapter 12.1 §6 rejected for congestion. Exempting refreshes and moves matters because they do not consume capacity: limiting them would break ordinary traffic from stations already in the table while doing nothing about a flood of new addresses.
Deliberately simplified: a hard per-second cap with a sharp edge. Production limiters use a token bucket, which permits a short burst — a rack of servers powering on together legitimately produces dozens of new addresses in one second — while holding the same long-term average.
Production implication: worst_port names the port to investigate, and limit_engaged is the signal that the limiter has actually done something. A limiter that never engages is invisible and unproven — it might be misconfigured, disabled, or wired to the wrong signal, and nothing would say so. Section 17's scenario 30 exists to prove the limiter engages under the condition it was built for, because an untriggered protection mechanism is indistinguishable from an absent one.
12. The Arithmetic That Makes the Rate Limit a Real Defence
Compute what an unlimited port can do, and then what a limited one can.
A single gigabit port can offer 1.4881 M minimum-length frames per second — Chapter 12.1 §3's number. If each carries a different invented source address, each one is an insertion.
| Table size | Time to fill from one port, unlimited |
|---|---|
| 1024 entries | 0.69 ms |
| 4096 entries | 2.75 ms |
| 8192 entries | 5.51 ms |
| 16 384 entries | 11.0 ms |
| 131 072 entries | 88.1 ms |
Every commercial switch's table, filled in under a tenth of a second, from one port, using perfectly well-formed frames that break no rule in Section 4.
And the consequence is not that learning stops. It is that the switch degrades into a hub. Once the table is full, no new address is learnable — every destination the switch has not already learned misses, and a miss floods. An attacker on one port causes every other port's unicast traffic to be copied to every port, including the attacker's, which is why this is a traffic-interception technique and not merely a denial of service.
Now apply the limiter. The table can be filled only if new entries are created faster than ageing reclaims them, so the condition for safety is:
NEW_PER_SEC < N_ENTRIES ÷ AGE_LIMIT
With 8192 entries and 802.1D's 300 s default that threshold is 8192 ÷ 300 = 27.3 new addresses per second.
| Cap per port | Steady-state bogus entries | Table filled? |
|---|---|---|
| unlimited | 8192 in 5.51 ms | yes, immediately |
| 1000 /s | 1000 × 300 = 300 000 | yes, in 8.2 s |
| 100 /s | 100 × 300 = 30 000 | yes, in 82 s |
| 25 /s | 25 × 300 = 7500 | no — 7500 < 8192 |
| 10 /s | 3000 | no, with wide margin |
At 25 per second the attack does not merely become slow. It becomes impossible from one port, because entries expire faster than one port can create them and the occupancy converges to a value below the table's capacity. That is the parameter's derivation, and it is why NEW_PER_SEC in Section 11 is 25 rather than a round number.
What the table costs in silicon
An entry is narrower than most people expect, and the whole table is smaller than one port's packet buffer.
| Field | Width | Why |
|---|---|---|
addr | 48 bits | the address itself |
port | 5 bits | 24 ports needs ⌈log₂ 24⌉ = 5 |
age | 9 bits | 300 s at a 1 s tick needs ⌈log₂ 301⌉ = 9 |
valid | 1 bit | |
stat | 1 bit | exempt from ageing |
| total | 64 bits | 63 rounded to a natural memory width |
So 8192 entries is 8192 × 64 = 524 288 bits = 64 KiB.
Compare that against Chapter 12.1 §6's buffering arithmetic: absorbing 1 ms of 2:1 oversubscription on a single gigabit port costs 122 KiB. The entire forwarding table for a 24-port switch is half the memory of one port's millisecond of burst tolerance.
Which is why table size is almost never the constraint an RTL engineer meets first. The constraint is lookup latency — Chapter 12.1 §12 derived a 28 ns shared budget, 14 cycles at 500 MHz, to answer which port leads to this 48-bit address? A linear search of 8192 entries at one entry per cycle takes 16.4 µs, which is 585 times the budget. The table is cheap; searching it is not, and that is the problem Chapter 12.5 exists to solve.
13. RTL 6 — Recording Why an Entry Is Believed
An entry says S is on port P. The question an operator asks at three in the morning is not what the table says — it is why, and how confident should I be.
// -----------------------------------------------------------------------
// learn_evidence_recorder -- per-entry provenance.
//
// The forwarding path does not need any of this. It exists so that when an
// entry turns out to be wrong, there is a record of what produced it. An
// entry refreshed ten thousand times from one port over an hour and an
// entry inserted once, forty seconds ago, from a port that has moved it
// three times are both just "S is on P" to the lookup.
// -----------------------------------------------------------------------
module learn_evidence_recorder
import maclearn_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int IDX_W = 13,
parameter int CNT_W = 24,
parameter int T_W = 32
)(
input logic clk,
input logic rst_n,
input logic second_tick,
input logic learn_done,
input learn_result_e learn_result,
input logic [IDX_W-1:0] learn_index,
input logic [PORT_BITS-1:0] learn_port,
input logic entry_expired,
input logic [IDX_W-1:0] expired_index,
// Query one entry's provenance.
input logic query_valid,
input logic [IDX_W-1:0] query_index,
output logic [T_W-1:0] q_first_seen_s, // when first inserted
output logic [T_W-1:0] q_last_seen_s, // when last refreshed
output logic [CNT_W-1:0] q_refreshes, // how much evidence
output logic [CNT_W-1:0] q_moves, // how unstable
output logic [1:0] q_confidence // 0 none 1 weak 2 ok 3 strong
);
localparam int N = 1 << IDX_W;
logic [T_W-1:0] uptime_s;
logic [T_W-1:0] first_seen [N];
logic [T_W-1:0] last_seen [N];
logic [CNT_W-1:0] refreshes [N];
logic [CNT_W-1:0] moves [N];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uptime_s <= '0;
for (int i = 0; i < N; i++) begin
first_seen[i] <= '0; last_seen[i] <= '0;
refreshes[i] <= '0; moves[i] <= '0;
end
end else begin
if (second_tick) uptime_s <= uptime_s + 1'b1;
if (learn_done) begin
unique case (learn_result)
LR_INSERT: begin
// A fresh entry starts with NO history. Its provenance is
// reset, not inherited from whatever occupied the slot.
first_seen[learn_index] <= uptime_s;
last_seen [learn_index] <= uptime_s;
refreshes [learn_index] <= '0;
moves [learn_index] <= '0;
end
LR_REFRESH: begin
last_seen[learn_index] <= uptime_s;
if (!(&refreshes[learn_index]))
refreshes[learn_index] <= refreshes[learn_index] + 1'b1;
end
LR_MOVE: begin
last_seen[learn_index] <= uptime_s;
if (!(&moves[learn_index]))
moves[learn_index] <= moves[learn_index] + 1'b1;
end
default: ;
endcase
end
// An expiry CLEARS the provenance. If the address is learned again
// it is a new claim, not the continuation of an old one -- the same
// epoch discipline Chapter 11.3 established for bring-up
// measurements.
if (entry_expired) begin
first_seen[expired_index] <= '0;
last_seen [expired_index] <= '0;
refreshes [expired_index] <= '0;
moves [expired_index] <= '0;
end
end
end
always_comb begin
q_first_seen_s = '0; q_last_seen_s = '0;
q_refreshes = '0; q_moves = '0;
q_confidence = 2'd0;
if (query_valid) begin
q_first_seen_s = first_seen[query_index];
q_last_seen_s = last_seen [query_index];
q_refreshes = refreshes [query_index];
q_moves = moves [query_index];
// Confidence is a function of how much evidence there is and how
// consistent it has been. It is advisory -- the forwarding path
// ignores it entirely -- but it is what turns a table dump into a
// diagnosis.
if (q_moves > CNT_W'(3)) q_confidence = 2'd1; // weak
else if (q_refreshes > CNT_W'(1000)) q_confidence = 2'd3; // strong
else if (q_refreshes > CNT_W'(10)) q_confidence = 2'd2; // ok
else q_confidence = 2'd1;
end
end
wire _unused_port = |learn_port;
endmoduleClassification: behavioural — a per-entry array of this width is a memory in a real design, and the query is a register-file read rather than combinational.
What it teaches: that the table's contents and the reason for them are separate, and only the first is needed to forward. The lookup needs addr and port. Everything here is for the moment the entry turns out to be wrong, and by then the evidence that produced it has been gone for hours.
And it teaches the epoch discipline once more. When an entry expires, its provenance is cleared, not preserved — because a re-learned address is a new claim. Carrying the old refresh count forward would let a stale claim inherit the confidence of a claim that has already been retracted, which is the same trap Chapter 11.3 identified when a bring-up measurement retained its value across a link drop.
Deliberately simplified: four fields and a three-level confidence. Production designs record the VLAN, the security domain, and whether the entry was learned before or after the last topology change — the last being decisive, because a topology change invalidates the port half of every inferred entry at once while leaving the address half correct.
Production implication: q_confidence distinguishes two entries that are byte-identical to the forwarding path. An entry with ten thousand refreshes and no moves is a workstation that has sat on that port all day. An entry with two refreshes and four moves is a duplicate address, a flapping link, or a loop — and q_moves on a single entry is the fastest path from "traffic to this one host is unreliable" to Section 10's table of causes.
14. RTL 7 — Conformance: Checking the Rule, Not the Conclusion
The monitor checks that the inference was applied correctly. It cannot check that the inference was right, and Section 16 is about why that distinction is the whole chapter.
// -----------------------------------------------------------------------
// learning_conformance_monitor -- checks the learning RULE against the
// frames that produced it.
//
// What it CAN check: that every eligible frame produced exactly one learn
// of its own source at its own ingress port; that no ineligible frame
// produced any learn; that a lookup never refreshed an age; that expiry
// happened at the configured interval and not before.
//
// What it CANNOT check, ever: whether the station whose address was
// learned is actually there. No signal in this design carries that fact,
// because no frame carries it.
// -----------------------------------------------------------------------
module learning_conformance_monitor
import maclearn_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int AGE_W = 9,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_valid,
input logic [ADDR_W-1:0] frame_src,
input logic [PORT_BITS-1:0] frame_ingress,
input logic frame_eligible,
input logic learn_valid,
input logic [ADDR_W-1:0] learn_addr,
input logic [PORT_BITS-1:0] learn_port,
input logic lookup_valid,
input logic age_written, // any age field changed
input logic learn_in_progress,
input logic entry_expired,
input logic [AGE_W-1:0] expired_age,
input logic [AGE_W-1:0] age_limit,
output logic [CNT_W-1:0] v_missing_learn, // eligible, none emitted
output logic [CNT_W-1:0] v_spurious_learn, // ineligible, one emitted
output logic [CNT_W-1:0] v_wrong_addr,
output logic [CNT_W-1:0] v_wrong_port,
output logic [CNT_W-1:0] v_lookup_refreshed, // the Section 8 bug
output logic [CNT_W-1:0] v_early_expiry,
output logic conformant
);
logic pend_q;
logic [ADDR_W-1:0] pend_src;
logic [PORT_BITS-1:0] pend_port;
logic pend_elig;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pend_q <= 1'b0; pend_src <= '0; pend_port <= '0; pend_elig <= 1'b0;
v_missing_learn <= '0;
v_spurious_learn <= '0;
v_wrong_addr <= '0;
v_wrong_port <= '0;
v_lookup_refreshed <= '0;
v_early_expiry <= '0;
end else begin
// Capture the frame; the learn should follow within a few cycles.
if (frame_valid) begin
pend_q <= 1'b1;
pend_src <= frame_src;
pend_port <= frame_ingress;
pend_elig <= frame_eligible;
end
if (pend_q && learn_valid) begin
pend_q <= 1'b0;
if (!pend_elig) begin
// A frame that failed an eligibility rule produced a learn.
if (!(&v_spurious_learn)) v_spurious_learn <= v_spurious_learn + 1'b1;
end else begin
if (learn_addr != pend_src)
if (!(&v_wrong_addr)) v_wrong_addr <= v_wrong_addr + 1'b1;
if (learn_port != pend_port)
if (!(&v_wrong_port)) v_wrong_port <= v_wrong_port + 1'b1;
end
end else if (pend_q && frame_valid) begin
// A second frame arrived before the first produced its learn.
pend_q <= 1'b1;
if (pend_elig)
if (!(&v_missing_learn)) v_missing_learn <= v_missing_learn + 1'b1;
end
// THE SECTION 8 CHECK. An age field changed while a lookup was in
// flight and no learn was in progress -- the design is refreshing
// on destination activity, which keeps dead entries alive forever.
if (age_written && lookup_valid && !learn_in_progress)
if (!(&v_lookup_refreshed))
v_lookup_refreshed <= v_lookup_refreshed + 1'b1;
// An entry expired before reaching the configured interval, which
// makes traffic to a live station flood for no reason.
if (entry_expired && (expired_age < age_limit))
if (!(&v_early_expiry)) v_early_expiry <= v_early_expiry + 1'b1;
end
end
assign conformant = (v_missing_learn == '0) &&
(v_spurious_learn == '0) &&
(v_wrong_addr == '0) &&
(v_wrong_port == '0) &&
(v_lookup_refreshed == '0) &&
(v_early_expiry == '0);
endmoduleClassification: synthesizable, and intended to stay in silicon.
What it teaches: the boundary this chapter exists to draw. The monitor checks that the rule was applied — right address, right port, only eligible frames, no lookup refresh, no early expiry. It says nothing whatever about whether the resulting table is true, because the truth of an entry is a fact about the physical world and no wire in the design carries it.
v_lookup_refreshed is the one worth building even if nothing else here is. Section 8's bug is invisible in every ordinary test — a switch that refreshes on lookup forwards correctly for as long as the stations stay put, and fails only when a station leaves, which no functional test does.
Deliberately simplified: one pending frame. A pipelined design has several frames in flight and needs a small scoreboard keyed by an identifier that follows the frame through the pipeline.
Production implication: conformant is a single bit that means the learning rule is being applied as specified, and it is the right thing to expose to system software. What it must never be labelled is "the forwarding table is correct." A conformant learner with a perfectly applied rule will still hold an entry naming the wrong port for every station that has moved and not yet transmitted — and that is not a defect, it is the mechanism's defined behaviour, which is exactly what Section 16's rejected property fails to grasp.
15. RTL 8 — The One Departure That Is Not Silent
This chapter has said repeatedly that a station leaving produces no signal, which is why entries expire on a timer. There is exactly one exception, and a switch that ignores it wastes up to five minutes of correct forwarding.
When a port goes down, every entry naming that port is known — not inferred, known — to be wrong. The stations behind it are unreachable through it, immediately and with certainty. Link loss is the one departure event the switch actually receives, and Chapter 11.1's link status is where it arrives.
// -----------------------------------------------------------------------
// port_down_flusher -- invalidates every entry naming a port whose link has
// dropped, and accelerates ageing after a port comes back.
//
// Two distinct responses to two distinct events:
// link DOWN -- entries naming that port are CERTAINLY wrong. Flush them.
// link UP -- the topology may have changed underneath. Entries are
// SUSPECT, not wrong. Accelerate ageing; do not flush.
// -----------------------------------------------------------------------
module port_down_flusher #(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int IDX_W = 13,
parameter int AGE_W = 9,
parameter int FAST_AGE = 15, // seconds, while topology is suspect
parameter int SUSPECT_S = 30, // how long to stay accelerated
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic second_tick,
input logic [N_PORTS-1:0] link_up, // synchronised, debounced
// Walk interface into the store: one entry per cycle.
output logic walk_valid,
output logic [IDX_W-1:0] walk_index,
input logic walk_entry_valid,
input logic [PORT_BITS-1:0] walk_entry_port,
output logic invalidate,
output logic [AGE_W-1:0] age_limit_out, // to the store
input logic [AGE_W-1:0] age_limit_normal, // 300 by default
output logic [CNT_W-1:0] c_flushed,
output logic [CNT_W-1:0] c_flush_events,
output logic topology_suspect
);
logic [N_PORTS-1:0] link_up_q;
logic [N_PORTS-1:0] went_down, came_up;
assign went_down = link_up_q & ~link_up;
assign came_up = ~link_up_q & link_up;
typedef enum logic [1:0] { F_IDLE, F_WALK, F_DONE } fst_e;
fst_e st_q;
logic [PORT_BITS-1:0] target_port;
logic [IDX_W:0] idx_q;
logic [$clog2(512)-1:0] suspect_s_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
link_up_q <= '0;
st_q <= F_IDLE;
idx_q <= '0;
target_port <= '0;
walk_valid <= 1'b0;
walk_index <= '0;
invalidate <= 1'b0;
c_flushed <= '0;
c_flush_events <= '0;
suspect_s_q <= '0;
topology_suspect <= 1'b0;
end else begin
link_up_q <= link_up;
walk_valid <= 1'b0;
invalidate <= 1'b0;
unique case (st_q)
F_IDLE: begin
// Priority-encode the lowest port that just dropped. A second
// simultaneous drop is handled on the next pass -- the walk is
// 8192 cycles at 500 MHz, which is 16.4 us, far shorter than
// any plausible interval between two link events.
for (int p = N_PORTS-1; p >= 0; p--)
if (went_down[p]) begin
target_port <= PORT_BITS'(p);
st_q <= F_WALK;
idx_q <= '0;
end
if (|went_down)
if (!(&c_flush_events)) c_flush_events <= c_flush_events + 1'b1;
end
F_WALK: begin
walk_valid <= 1'b1;
walk_index <= idx_q[IDX_W-1:0];
// The entry presented THIS cycle was requested last cycle.
if (walk_entry_valid && (walk_entry_port == target_port)) begin
invalidate <= 1'b1;
if (!(&c_flushed)) c_flushed <= c_flushed + 1'b1;
end
if (idx_q[IDX_W-1:0] == {IDX_W{1'b1}}) st_q <= F_DONE;
else idx_q <= idx_q + 1'b1;
end
F_DONE: st_q <= F_IDLE;
default: st_q <= F_IDLE;
endcase
// A port COMING UP is a different event. Something was connected --
// possibly a whole switch with stations behind it, possibly the same
// cable back in the same socket. Entries are suspect, not wrong, so
// accelerate ageing rather than destroying information that is
// probably still correct.
if (|came_up) suspect_s_q <= $clog2(512)'(SUSPECT_S);
else if (second_tick && (suspect_s_q != '0))
suspect_s_q <= suspect_s_q - 1'b1;
topology_suspect <= (suspect_s_q != '0);
end
end
assign age_limit_out = (suspect_s_q != '0) ? AGE_W'(FAST_AGE)
: age_limit_normal;
endmoduleClassification: synthesizable.
What it teaches: that the two link events deserve opposite responses, and treating them the same is a real design error in both directions.
A link going down is certainty. The stations behind that port are not reachable through it — no inference, no timer, no waiting. Flushing is correct and immediate. Without it, every entry naming the dead port keeps answering lookups for up to age_limit seconds, and every frame to those stations is forwarded into a dead port and lost. At 802.1D's 300 s default that is five minutes of guaranteed loss after an event the switch was told about.
A link coming up is suspicion. Something was connected, and it may have been the same cable into the same socket — in which case every existing entry is still correct — or a whole switch with a hundred stations behind it, in which case many entries now name the wrong port. The switch cannot tell. Flushing on link-up destroys information that is probably correct and forces a flood of every conversation in the domain. Accelerating ageing instead lets the still-correct entries survive on their own refreshes while the wrong ones expire in 15 s rather than 300.
Deliberately simplified: one flush walk at a time, and a single global accelerated interval. Production designs flush per VLAN, handle several simultaneous link events with a pending mask, and derive the accelerated interval from the spanning-tree forward delay rather than a fixed constant.
Production implication: c_flushed after a link-down should be roughly the number of stations that were behind that port, and a flush that removes zero entries on a port that was carrying traffic means the walk is not matching — a port-width mismatch, or an off-by-one in the walk index, both of which leave the switch silently forwarding to a dead port. topology_suspect is worth exposing because it explains a transient the operator will otherwise report as a fault: for 30 seconds after any port comes up, flooding rises sharply and then subsides, which is the accelerated ageing doing exactly what it was built to do.
16. Properties Worth Asserting, and One Worth Refusing
Every property here is about the rule. Not one is about the conclusion, and Section 16's rejected property is the one that confuses the two.
Eligibility
// P1. A group source address is NEVER learned. One bit test, and the
// omission with the worst symptom-to-cause distance in this chapter.
property p_group_source_never_learned;
@(posedge clk) disable iff (!rst_n)
(frame_valid && frame_src[40]) |-> !learn_valid;
endproperty
a_group_src: assert property (p_group_source_never_learned);
// P2. A frame that failed Chapter 7.3's check sequence is not evidence.
property p_bad_fcs_never_learned;
@(posedge clk) disable iff (!rst_n)
(frame_valid && frame_bad_fcs) |-> !learn_valid;
endproperty
a_bad_fcs: assert property (p_bad_fcs_never_learned);
// P3. A runt is not evidence -- its source field may be a fragment.
property p_runt_never_learned;
@(posedge clk) disable iff (!rst_n)
(frame_valid && frame_runt) |-> !learn_valid;
endproperty
a_runt: assert property (p_runt_never_learned);
// P4. The all-zero source is refused.
property p_null_source_refused;
@(posedge clk) disable iff (!rst_n)
(frame_valid && (frame_src == '0)) |-> !learn_valid;
endproperty
a_null_src: assert property (p_null_source_refused);
// P5. Refusals are CLASSIFIED, not merely counted -- each value has a
// different operational meaning.
property p_refusal_classified;
@(posedge clk) disable iff (!rst_n)
(frame_valid && !learn_valid) |-> (refusal != LE_OK);
endproperty
a_refusal_classified: assert property (p_refusal_classified);
// P6. An ELIGIBLE frame always produces a learn. Learning is not
// conditional on the frame's forwarding outcome.
property p_eligible_always_learns;
@(posedge clk) disable iff (!rst_n)
(frame_valid && frame_eligible) |-> learn_valid;
endproperty
a_eligible_learns: assert property (p_eligible_always_learns);The inference itself
// P7. The learned port is the INGRESS port of the frame that produced it.
// Not the frame's destination port, not the previous entry's port.
property p_learn_port_is_ingress;
@(posedge clk) disable iff (!rst_n)
learn_valid |-> (learn_port == frame_ingress);
endproperty
a_learn_port: assert property (p_learn_port_is_ingress);
// P8. The learned address is the frame's SOURCE. The destination is
// never learned -- a switch has no evidence about where a destination is.
property p_learn_addr_is_source;
@(posedge clk) disable iff (!rst_n)
learn_valid |-> (learn_addr == frame_src);
endproperty
a_learn_addr: assert property (p_learn_addr_is_source);
// P9. One frame produces at most ONE learn.
property p_one_learn_per_frame;
@(posedge clk) disable iff (!rst_n)
learn_valid |=> !learn_valid until_with frame_valid;
endproperty
a_one_learn: assert property (p_one_learn_per_frame);
// P10. A known address on the SAME port is a refresh, never an insert --
// otherwise one station occupies many slots.
property p_same_port_is_refresh;
@(posedge clk) disable iff (!rst_n)
(learn_done && addr_known && (existing_port == learn_port))
|-> (learn_result == LR_REFRESH);
endproperty
a_same_port_refresh: assert property (p_same_port_is_refresh);
// P11. A known address on a DIFFERENT port is a move, and the displaced
// port is reported -- the pair of ports IS the diagnosis.
property p_diff_port_is_move;
@(posedge clk) disable iff (!rst_n)
(learn_done && addr_known && (existing_port != learn_port))
|-> ((learn_result == LR_MOVE) && (displaced_port == existing_port));
endproperty
a_diff_port_move: assert property (p_diff_port_is_move);Ageing
// P12. THE SECTION 8 PROPERTY. A lookup never touches an age field.
property p_lookup_does_not_refresh;
@(posedge clk) disable iff (!rst_n)
(lookup_valid && !learn_in_progress) |-> !age_written;
endproperty
a_lookup_no_refresh: assert property (p_lookup_does_not_refresh);
// P13. A refresh resets the age to zero, not to some smaller value.
property p_refresh_resets_age;
@(posedge clk) disable iff (!rst_n)
(learn_done && (learn_result == LR_REFRESH)) |=> (entry_age == '0);
endproperty
a_refresh_zeroes: assert property (p_refresh_resets_age);
// P14. An entry expires only at or after the configured interval.
property p_no_early_expiry;
@(posedge clk) disable iff (!rst_n)
entry_expired |-> (expired_age >= age_limit);
endproperty
a_no_early_expiry: assert property (p_no_early_expiry);
// P15. A statically configured entry NEVER ages. It was asserted, not
// inferred, so no timer may retract it.
property p_static_never_ages;
@(posedge clk) disable iff (!rst_n)
(entry_expired && entry_is_static) |-> 1'b0;
endproperty
a_static_persists: assert property (p_static_never_ages);
// P16. Expiry clears the entry's provenance -- a re-learn is a NEW claim
// and must not inherit a retracted claim's confidence.
property p_expiry_clears_evidence;
@(posedge clk) disable iff (!rst_n)
entry_expired |=> ((refreshes[$past(expired_index)] == '0) &&
(moves[$past(expired_index)] == '0));
endproperty
a_expiry_clears: assert property (p_expiry_clears_evidence);Capacity, rate and moves
// P17. A full table REFUSES rather than evicting a live entry, so
// invented addresses cannot displace real stations.
property p_full_refuses_not_evicts;
@(posedge clk) disable iff (!rst_n)
(learn_valid && table_full && !addr_known) |-> (learn_result == LR_REJECT);
endproperty
a_full_refuses: assert property (p_full_refuses_not_evicts);
// P18. The rate limit applies to NEW addresses only. Refreshes and moves
// consume no capacity, so limiting them breaks real traffic and stops
// nothing.
property p_known_addresses_not_limited;
@(posedge clk) disable iff (!rst_n)
(learn_req && addr_is_known) |-> learn_permit;
endproperty
a_known_unlimited: assert property (p_known_addresses_not_limited);
// P19. The budget is PER PORT -- a shared budget lets one abusive port
// starve every other port's legitimate learning.
property p_budget_is_per_port;
@(posedge clk) disable iff (!rst_n)
(!learn_permit && learn_req && !addr_is_known)
|-> (new_this_second[learn_port] >= CNT_W'(NEW_PER_SEC));
endproperty
a_per_port_budget: assert property (p_budget_is_per_port);
// P20. Occupancy never exceeds capacity.
property p_occupancy_bounded;
@(posedge clk) disable iff (!rst_n)
(occupancy <= (IDX_W+1)'(N_ENTRIES));
endproperty
a_occupancy_bounded: assert property (p_occupancy_bounded);
// P21. Every move records BOTH ports. One of them alone identifies
// nothing.
property p_move_records_both_ports;
@(posedge clk) disable iff (!rst_n)
(learn_done && (learn_result == LR_MOVE))
|=> ((last_moved_from != last_moved_to));
endproperty
a_move_both_ports: assert property (p_move_records_both_ports);
// P22. A flap verdict names the address. "Something is flapping" is not
// actionable; a 48-bit value is.
property p_flap_names_address;
@(posedge clk) disable iff (!rst_n)
flapping |-> (flapping_addr != '0);
endproperty
a_flap_named: assert property (p_flap_names_address);Conformance
// P23. The monitor's verdict is the conjunction of its violation counts,
// and it means the RULE was applied -- never that the table is true.
property p_conformant_means_no_violations;
@(posedge clk) disable iff (!rst_n)
conformant |-> ((v_missing_learn == '0) && (v_spurious_learn == '0) &&
(v_wrong_addr == '0) && (v_wrong_port == '0) &&
(v_lookup_refreshed == '0) && (v_early_expiry == '0));
endproperty
a_conformant_def: assert property (p_conformant_means_no_violations);
// P24. Every violation is attributed to a class. A single "error" bit
// would collapse six unrelated failures into one signal.
property p_violations_classified;
@(posedge clk) disable iff (!rst_n)
$rose(!conformant) |-> ($changed(v_missing_learn) ||
$changed(v_spurious_learn) ||
$changed(v_wrong_addr) ||
$changed(v_wrong_port) ||
$changed(v_lookup_refreshed) ||
$changed(v_early_expiry));
endproperty
a_violation_classified: assert property (p_violations_classified);
// P25. Confidence is advisory: the forwarding path must not consult it.
// A low-confidence entry is still the best answer available.
property p_confidence_does_not_gate_forwarding;
@(posedge clk) disable iff (!rst_n)
(lookup_valid && lookup_hit) |-> (lookup_port == entry_port);
endproperty
a_confidence_advisory: assert property (p_confidence_does_not_gate_forwarding);Flush and topology
// P26. A link going DOWN invalidates every entry naming that port. This
// is the one departure the switch is told about, and ignoring it wastes
// up to age_limit seconds of forwarding into a dead port.
property p_link_down_flushes_port;
@(posedge clk) disable iff (!rst_n)
(invalidate && walk_entry_valid) |-> (walk_entry_port == target_port);
endproperty
a_flush_targets_port: assert property (p_link_down_flushes_port);
// P27. A link going down NEVER invalidates an entry naming another port.
// A flush that over-reaches destroys correct information and floods the
// whole domain.
property p_flush_does_not_overreach;
@(posedge clk) disable iff (!rst_n)
(walk_valid && walk_entry_valid && (walk_entry_port != target_port))
|-> !invalidate;
endproperty
a_flush_precise: assert property (p_flush_does_not_overreach);
// P28. A link coming UP accelerates ageing and does NOT flush. Entries
// are suspect, not wrong -- the cable may have gone back into the same
// socket.
property p_link_up_does_not_flush;
@(posedge clk) disable iff (!rst_n)
($rose(|came_up)) |-> (!invalidate throughout topology_suspect[->1]);
endproperty
a_link_up_no_flush: assert property (p_link_up_does_not_flush);
// P29. While the topology is suspect, the store ages at the accelerated
// interval and not the normal one.
property p_accelerated_while_suspect;
@(posedge clk) disable iff (!rst_n)
topology_suspect |-> (age_limit_out == AGE_W'(FAST_AGE));
endproperty
a_fast_age_engaged: assert property (p_accelerated_while_suspect);
// P30. The accelerated interval EXPIRES. A switch that stays accelerated
// forever floods permanently, and nothing reports it.
property p_suspect_is_bounded;
@(posedge clk) disable iff (!rst_n)
$rose(topology_suspect) |-> ##[1:$] !topology_suspect;
endproperty
a_suspect_bounded: assert property (p_suspect_is_bounded);17. Verification Scenarios
Fifty-two scenarios. The eligibility ones are absolute; the ageing and capacity ones have expected outcomes that include forgetting and refusing.
Eligibility — which frames are evidence
| # | Scenario | Expected |
|---|---|---|
| 1 | Ordinary unicast source | learned, port = ingress |
| 2 | Source with I/G bit set | refused, LE_GROUP_SRC |
| 3 | Source 01:80:C2:00:00:00 | refused, LE_RESERVED_SRC |
| 4 | Source 01:80:C2:00:00:0F | refused — the whole reserved range |
| 5 | Source FF:FF:FF:FF:FF:FF | refused — group bit set |
| 6 | Source all zeros | refused, LE_NULL_SRC |
| 7 | Frame with bad FCS, valid-looking source | refused, LE_BAD_FCS |
| 8 | 40-octet runt | refused, LE_RUNT |
| 9 | Exactly 64 octets | learned |
| 10 | Port with learning disabled | refused, LE_PORT_DOWN |
| 11 | A frame that will be filtered by the lookup | still learned — learning is independent of outcome |
The inference and the store
| # | Scenario | Expected |
|---|---|---|
| 12 | First frame from a new address | LR_INSERT, occupancy +1 |
| 13 | Second frame, same address, same port | LR_REFRESH, occupancy unchanged |
| 14 | Same address, different port | LR_MOVE, displaced_port = old port |
| 15 | Learned port on every insert | equals the ingress port, never the egress |
| 16 | Learned address on every insert | equals the frame's source, never its destination |
| 17 | Destination address of every frame | never inserted |
| 18 | 1000 frames from one address | one insert, 999 refreshes, one table slot |
| 19 | Lookup for a learned address | hit, port equals the learned port |
| 20 | Lookup for an unlearned address | miss — flooding is Chapter 12.4's answer |
| 21 | Two addresses, same port | two entries, both naming that port |
Ageing
| # | Scenario | Expected |
|---|---|---|
| 22 | Entry idle for age_limit − 1 seconds | still valid |
| 23 | Entry idle for age_limit seconds | expires, c_expired increments |
| 24 | Entry refreshed at age_limit − 1 | age returns to zero, survives |
| 25 | Statically configured entry idle for an hour | never expires |
| 26 | Expiry of an entry | provenance cleared, not carried forward |
| 27 | Address re-learned after expiry | LR_INSERT, q_refreshes starts at zero |
| 28 | age_limit shortened to 10 s under steady traffic | miss_rate_high asserts |
| 29 | Full sweep of 8192 entries at 500 MHz | 16.4 µs, no lookup starved |
Capacity and rate limiting
| # | Scenario | Expected |
|---|---|---|
| 30 | 1000 new addresses/s on one port, cap 25 | limit_engaged asserts, worst_port names it |
| 31 | 25 new addresses/s on one port | all permitted, limiter silent |
| 32 | 1.4881 M invented addresses/s, no limiter | table full in 5.51 ms |
| 33 | Same, with the 25/s cap and 300 s ageing | occupancy converges near 7500, never full |
| 34 | Attack on port 3 while port 7 learns normally | port 7 unaffected — per-port budget |
| 35 | Refreshes from a known address during an attack | always permitted, never rate limited |
| 36 | Learn into a full table | LR_REJECT — no live entry displaced |
| 37 | Traffic between two already-learned stations during a full-table attack | unaffected throughout |
Moves, duplicates and loops
| # | Scenario | Expected |
|---|---|---|
| 38 | Station unplugged and moved to another port | one LR_MOVE, likely_cause = 1 |
| 39 | Two stations sharing one address, moderate traffic | moves on one address, likely_cause = 2 |
| 40 | Topology loop, line-rate duplication | moves across many addresses, likely_cause = 3 |
| 41 | Any move | last_moved_from and last_moved_to both recorded |
| 42 | Flap above FLAP_LIMIT | flapping asserts, flapping_addr names the address |
| 43 | Entry with 10 000 refreshes, no moves | q_confidence = strong |
Conformance
| # | Scenario | Expected |
|---|---|---|
| 44 | Eligible frame producing no learn | v_missing_learn increments |
| 45 | Ineligible frame producing a learn | v_spurious_learn increments |
| 46 | Learn carrying the wrong port | v_wrong_port increments |
| 47 | Age written during a lookup, no learn in flight | v_lookup_refreshed increments |
| 48 | Entry expiring below age_limit | v_early_expiry increments |
Flush and topology
| # | Scenario | Expected |
|---|---|---|
| 49 | Port 5 link drops with 40 stations learned behind it | exactly those 40 invalidated, c_flushed = 40 |
| 50 | Port 5 link drops | no entry naming another port is touched |
| 51 | Port 5 link comes back up | no flush; topology_suspect asserts, age_limit_out = 15 s |
| 52 | 30 s after the port came up | topology_suspect deasserts, interval returns to 300 s |
18. Debugging Learning
Every row here produces a switch with valid links, zero frame errors and correct-looking behaviour. The observable in the third column is what separates them.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| Everything floods, nothing forwards | learning never happens | c_insert at zero while frames arrive; check LE_PORT_DOWN |
| One host always floods, others fine | that host never transmits unsolicited | its entry absent; it appears immediately after the host is pinged |
| One host floods intermittently | its entry ages out between its transmissions | q_last_seen_s and age_limit — the host's idle period exceeds it |
| Flooding rose after a config change | age_limit shortened | miss_rate_high asserted, table not full |
| Multicast reaching only one port | a group source address was learned | LE_GROUP_SRC count non-zero, and a table entry with bit 40 set |
| Table full, everything floods | address flood, or a genuinely large domain | c_limited and worst_port; a real domain grows slowly, an attack fills in milliseconds |
| A host is intermittently unreachable | duplicate MAC address | flapping_addr, likely_cause = 2, and the two ports in last_moved_from/to |
| Everything slow, broadcast storm | topology loop | likely_cause = 3 — moves across many addresses at once |
| A host is reachable from some stations, not others | ageing refreshed on lookup | v_lookup_refreshed non-zero — the Section 8 bug |
| Entries vanish immediately after a topology change | correct — a change invalidates port information | c_expired spiking once, then settling |
| A host's traffic works, replies never arrive | the reply's destination entry names a stale port | that entry's q_moves and q_last_seen_s |
| New hosts unlearnable, old ones fine | table full and refusing rather than evicting | LR_REJECT count rising, occupancy at capacity — the intended behaviour |
19. Common Misconceptions
1 — "A switch learns the addresses it sees, including destinations."
The wrong model: the switch reads both addresses and records what it can about each.
What it costs: an entry created from a destination address is a claim that a station is on the port a frame was sent to it from — which is exactly backwards. The first frame to an unknown station would create an entry naming the sender's port, and every subsequent frame to that station would be forwarded back toward the sender. Traffic between two hosts would collapse into each host receiving its own transmissions echoed by the switch.
The corrected model: only the source address is evidence. A destination address is a question. The switch has no information whatever about where a destination is — that is why the question needs a table in the first place, and why a miss floods rather than guesses.
2 — "Ageing is garbage collection."
The wrong model: entries expire to free space, so ageing matters only when the table is under pressure.
What it costs: it makes the ageing interval look like a tuning knob for capacity, and an operator with a large table concludes that a long interval is harmless. It is not. A long interval means an entry for a departed station keeps answering lookups with a port the station has left, and every frame to it is forwarded into a segment where nothing is listening — silently, with no counter incrementing anywhere.
The corrected model: ageing is the expiry of a claim's validity. The entry was inferred from an observation, the observation is getting old, and there is no mechanism by which a station announces that it has left. Expiry is the design admitting that its evidence has gone stale, and it costs bandwidth (flooding) rather than delivery, which is the safe direction.
3 — "A lookup hit means the station is still there, so it should refresh the entry."
The wrong model: activity on an entry is evidence for it, the way it is in a cache.
What it costs: Section 17's directed test, in full. A station that has been unplugged keeps its entry alive indefinitely as long as somebody keeps sending to it — and the sender is exactly the party that then cannot reach it. The result is a host reachable from some stations and not others, with no error anywhere.
The corrected model: a lookup is evidence about the frame's source, never about its destination. B sending to A tells the switch where B is. It tells the switch only that B believes A exists, and B's belief is not information. Only S can produce evidence for S's entry.
4 — "A full MAC table means dropped frames."
The wrong model: the table fills, and traffic to the addresses that did not fit is discarded.
What it costs: it makes a full table look like a bounded, localised problem affecting a few hosts. The actual failure is unbounded and affects everyone: every unlearnable address misses, and a miss floods. The switch stops being a switch and becomes a hub — every port carries every unknown conversation, Chapter 12.1's 48× advantage collapses, and an attacker on any port receives copies of traffic intended for others.
The corrected model: a full table costs confidentiality and bandwidth, not delivery. Nothing is dropped — everything is delivered, to everybody. That is why Section 12's rate limit is a security mechanism and not a capacity optimisation.
5 — "If the learning logic is correct, the forwarding table is correct."
The wrong model: verify the rule, and the table follows.
What it costs: it produces Section 16's rejected property and, worse, the expectation behind it. An engineer holding this model treats a wrong entry as an implementation bug and looks for one — in a design where the rule was applied perfectly and the entry is wrong anyway, because a station moved and has not transmitted since, or because a frame carried a source address that was not its sender's.
The corrected model: the rule and the conclusion are different objects. conformant means the rule was applied. Nothing in a switch can mean the table is true, because the truth is a fact about the physical world that reaches the switch only through an unauthenticated field written by the sender. The design is responsible for applying the rule and for bounding the damage when the premise is false. It is not responsible for the premise.
20. Interview Reasoning
Q1 — "A switch is powered on with an empty table and plugged into a working network. Describe what happens to the first frame, and to the hundredth."
Reason through it. The first frame arrives, its source is learned onto the ingress port, and its destination misses — so it floods to every port except the ingress. That flood is what makes an empty table safe: the frame still reaches its destination. The destination replies, and the reply's source teaches the switch where the destination is, so one exchange populates both directions. By the hundredth frame the table holds every station that has transmitted, and most lookups hit. The strong answer names the self-healing loop — flood, reply, learn — and observes that nothing was configured and no station did anything unusual. It also names what is still missing: stations that have not transmitted are still unknown, and traffic to them still floods.
Q2 — "Why does a lookup hit not reset the entry's ageing timer?"
Reason through it. Because a hit is evidence about the frame's source, not its destination. B sending to A proves where B is; it proves only that B believes A exists. If a hit refreshed A's entry, then A being unplugged would not matter — B's continued sending would keep A's entry alive forever, naming a dead port. The strong answer states the failure precisely: A becomes unreachable specifically from the stations that keep addressing it, and reachable from those that let its entry expire, producing a host that works from some places and not others with no error counter anywhere. It then names the correct behaviour: the entry expires, the next frame floods, the flood finds A wherever it now is, and A's reply re-learns it.
Q3 — "An attacker on one port of a 24-port switch sends minimum-length frames with random source addresses. What happens, and what is the defence?"
Reason through it. At 1.4881 Mpps every frame inserts a new address, so an 8192-entry table fills in 8192 ÷ 1.4881 M = 5.51 ms. Then every unknown destination misses and every miss floods — the switch degrades into a hub, and the attacker receives copies of everybody's traffic. The defence is not detection, because a forged source address is indistinguishable from a real one. It is rate: cap new addresses per port below N_ENTRIES ÷ AGE_LIMIT, which for 8192 entries and 300 s is 27.3 per second. At a cap of 25, the steady-state count of invented entries is 25 × 300 = 7500, below capacity, so the table can never be filled from one port. The strong answer also names the two supporting choices: the budget is per port so an attacker cannot starve other ports' learning, and a full table refuses rather than evicting, so real entries are never displaced.
Q4 — "Traffic to one host is intermittently lost. Cables are fine, links are up, error counters are zero, and both switches report the host as present. What do you look at?"
Reason through it. Intermittent loss with clean statistics on both ends is the signature of a duplicate MAC address. Two stations sharing 48 bits cause the entry to alternate between two ports every time either one transmits, so each station receives the fraction of traffic that arrives while the table happens to be pointing at it. Neither station can detect this — a station has no way to know about frames addressed to it that went somewhere else. The observable is the move counter: flapping_addr names the address, likely_cause = 2 distinguishes a duplicate from a loop, and last_moved_from and last_moved_to name the two ports to go and look behind. The strong answer contrasts it with a loop, which produces moves across many addresses at line rate rather than on one address at the two stations' traffic rate — the same counters, a completely different shape.
21. Understanding Check
22. What's Next
This chapter built the table. It has said almost nothing about reading it.
Every module here treated the lookup as an output — lookup_hit and lookup_port leave the store and go somewhere. Chapter 12.3 — The Forwarding Decision is that somewhere: the destination lookup in full, the ingress-port filter rule that Chapter 12.1 §16 showed most reviewers get wrong, and what a hit actually licenses the switch to do.
Then Chapter 12.4 — Flooding owns the other half of the answer, and it is the half this chapter has been leaning on throughout. Expiry is only safe because a miss floods, and a full table is only survivable because a miss floods. Chapter 12.4 asks what that costs, and why it is nonetheless the only correct response to not knowing.
Chapter 12.5 then builds the table in real hardware — CAM structures, hashing, associativity, and what a hash collision does to the behavioural store Section 6 stood in for.
Continue learning
Related tutorials
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
- Related topic
CSMA/CD, Collision Domains and Slot Time
Slot time is the parameter the whole half-duplex MAC hangs on: it bounds medium acquisition, bounds a collision fragment, and is the retransmission quantum. Deriving it from round-trip propagation plus jam is what fixes Ethernet's minimum frame size — a timing constant wearing a frame-format costume.
- Related topic
Packet Switching
A circuit allocates capacity in advance and guarantees it; a packet network allocates on demand and guarantees nothing. The exchange is measurable in RTL — idle reserved slots against buffered, delayed and occasionally dropped packets — and it is why a packet must describe its own extent and destination.
- Related topic
From Coax to Twisted Pair to Switched Links
Coax, repeater, hub, bridge, switch — four steps, and only the last touched contention. A repeater reproduces a signal and cannot buffer, so it spends collision-domain budget and partitions nothing; a bridge holds the whole frame, and that buffer is what makes every other capability possible.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
