Skip to content
VLSI Mentor

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 S arrived on port P. Therefore S is reachable through P.

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

A frame arrives at an ingress port carrying a destination address and a source address. The destination address is a question the switch answers by consulting its table. The source address is evidence the switch uses to write a new entry, recording that the source is reachable through the ingress port on which the frame arrived. There is no exchange with the station, no acknowledgement, and no verification: the source address field is written by the sender and checked by no part of Ethernet, so the entry is an inference and not a fact.Frame at port Pdestination and sourceDestinationa question — where to?Lookupconsults the tableForward, filter orfloodChapter 12.1's threeoutcomesSourceevidence — came fromwhere?Write entry S to Pan inference, not a factNever authenticatedthe sender wrote thisfield12
Figure 1 — the switch is told nothing and asks nothing; every entry is inferred from a field the sender wrote and nobody checked.

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 addressesa registration protocol
configuration requirednoneevery station configured, or a server run
new station workson its first transmitted frameafter a registration exchange
a station that never transmitsnever learned — floods foreverregistered like any other
a station that leavesdetected by timeout onlyderegistration message
a station claiming another's addressacceptedrejectable
cost per frameone table writenone, plus protocol machinery
stations that must implement itzeroall 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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);
 
endmodule

Classification: 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 ruleWhat enters the tableThe symptom, and why it is hard
I/G bit on the sourcea multicast address, bound to one portmulticast 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 FCSa fictitious address, from corrupted bitsa real entry is displaced by a ghost; traffic to the real station floods until it transmits again
runtwhatever the fragment's first octets weresame as above, at whatever rate the faulty link produces fragments
all-zero sourceaddress 00:00:00:00:00:00harmless until something sends to it, then a flood becomes a forward
reserved rangea control-protocol addresscontrol frames become forwarded rather than flooded, breaking protocols that assume every port sees them
port learning statecorrect entries, at the wrong timea 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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));
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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;
 
endmodule

Classification: 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 lookupageing refreshes on transmit
A's entry after A is unpluggedrefreshed by every frame B sendsages normally
A's entry after 300 sstill present, still naming the dead portexpired
B's frames to Aforwarded to the port A leftflooded — reaching A wherever it is now
if A returns on a different portentry says the old port until A transmitsalready correct, or corrected on flood reply
the entry's justificationB's belief that A existsA'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.

A frame from station B to station A arrives at the switch. Its source address B is evidence that B is on the ingress port, so B's entry is refreshed. Its destination address A is a question, and the lookup produces a hit. Under a correct design the hit does not touch A's entry, so if A has been unplugged its entry ages out after the ageing interval and subsequent frames to A are flooded, which will find A wherever it now is. Under an incorrect design that refreshes on lookup, B's continued sending keeps A's entry alive forever, so frames to A are forwarded to a port A has left and A never receives them, with the switch reporting no error at any point.Frame B to AB still sending, AunpluggedSource B refreshedB transmitted — realevidenceLookup A hitsa question, not evidenceA's entry ages outthen frames to A floodRefresh on hitthe tempting shortcutA's entry neverexpireskept alive by B'sassumption12
Figure 2 — a lookup is evidence about the sender, never about the destination; refreshing on a hit lets a stale assumption preserve the entry it should be expiring.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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 portacross 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 moveduplicate addresstopology loop
what happenedone station physically relocatedtwo stations, same 48 bitsone frame arriving twice
moves per secondone, then nonea few — the two stations' traffic ratethousands — close to 1.4881 M
addresses involvedoneonemany at once
ports involvedold and newthe two stations' portsthe two loop ports, for every address
what the table doescorrect after the first framealternates foreveralternates for everything
user-visible symptombrief flooding, then normalintermittent unreachability for botheverything slow, broadcast storm
what the switch reportsnothingnothingnothing — 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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 sizeTime to fill from one port, unlimited
1024 entries0.69 ms
4096 entries2.75 ms
8192 entries5.51 ms
16 384 entries11.0 ms
131 072 entries88.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 portSteady-state bogus entriesTable filled?
unlimited8192 in 5.51 msyes, immediately
1000 /s1000 × 300 = 300 000yes, in 8.2 s
100 /s100 × 300 = 30 000yes, in 82 s
25 /s25 × 300 = 7500no — 7500 < 8192
10 /s3000no, 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.

FieldWidthWhy
addr48 bitsthe address itself
port5 bits24 ports needs ⌈log₂ 24⌉ = 5
age9 bits300 s at a 1 s tick needs ⌈log₂ 301⌉ = 9
valid1 bit
stat1 bitexempt from ageing
total64 bits63 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 latencyChapter 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.

A port offering minimum-length frames each carrying a different invented source address inserts new entries at up to 1.49 million per second, which fills an 8192-entry table in 5.5 milliseconds. Once the table is full no new address can be learned, every unknown destination misses, and every miss floods, so the switch degrades into a hub and the attacker receives copies of traffic intended for other ports. Applying a per-port cap on new addresses changes the arithmetic: entries are reclaimed by ageing at capacity divided by ageing interval, which for 8192 entries and 300 seconds is 27.3 per second, so a cap of 25 per second leaves the steady-state count of bogus entries at 7500, below the table's capacity, and the table can never be filled from one port.1.49 M newaddresses/sone port, legal framesTable full in 5.5 ms8192 entriesEvery lookup missesnothing new is learnableSwitch becomes a hubattacker sees everythingCap 25 new/s perportbelow 8192 ÷ 3007500 bogus, steadyageing outruns creationTable never fillsreal entries keep theirslots12
Figure 3 — the rate limit does not detect anything; it changes an inequality, and below the threshold the table's occupancy converges instead of filling.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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;
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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);
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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;
 
endmodule

Classification: 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

#ScenarioExpected
1Ordinary unicast sourcelearned, port = ingress
2Source with I/G bit setrefused, LE_GROUP_SRC
3Source 01:80:C2:00:00:00refused, LE_RESERVED_SRC
4Source 01:80:C2:00:00:0Frefused — the whole reserved range
5Source FF:FF:FF:FF:FF:FFrefused — group bit set
6Source all zerosrefused, LE_NULL_SRC
7Frame with bad FCS, valid-looking sourcerefused, LE_BAD_FCS
840-octet runtrefused, LE_RUNT
9Exactly 64 octetslearned
10Port with learning disabledrefused, LE_PORT_DOWN
11A frame that will be filtered by the lookupstill learned — learning is independent of outcome

The inference and the store

#ScenarioExpected
12First frame from a new addressLR_INSERT, occupancy +1
13Second frame, same address, same portLR_REFRESH, occupancy unchanged
14Same address, different portLR_MOVE, displaced_port = old port
15Learned port on every insertequals the ingress port, never the egress
16Learned address on every insertequals the frame's source, never its destination
17Destination address of every framenever inserted
181000 frames from one addressone insert, 999 refreshes, one table slot
19Lookup for a learned addresshit, port equals the learned port
20Lookup for an unlearned addressmiss — flooding is Chapter 12.4's answer
21Two addresses, same porttwo entries, both naming that port

Ageing

#ScenarioExpected
22Entry idle for age_limit − 1 secondsstill valid
23Entry idle for age_limit secondsexpires, c_expired increments
24Entry refreshed at age_limit − 1age returns to zero, survives
25Statically configured entry idle for an hournever expires
26Expiry of an entryprovenance cleared, not carried forward
27Address re-learned after expiryLR_INSERT, q_refreshes starts at zero
28age_limit shortened to 10 s under steady trafficmiss_rate_high asserts
29Full sweep of 8192 entries at 500 MHz16.4 µs, no lookup starved

Capacity and rate limiting

#ScenarioExpected
301000 new addresses/s on one port, cap 25limit_engaged asserts, worst_port names it
3125 new addresses/s on one portall permitted, limiter silent
321.4881 M invented addresses/s, no limitertable full in 5.51 ms
33Same, with the 25/s cap and 300 s ageingoccupancy converges near 7500, never full
34Attack on port 3 while port 7 learns normallyport 7 unaffected — per-port budget
35Refreshes from a known address during an attackalways permitted, never rate limited
36Learn into a full tableLR_REJECTno live entry displaced
37Traffic between two already-learned stations during a full-table attackunaffected throughout

Moves, duplicates and loops

#ScenarioExpected
38Station unplugged and moved to another portone LR_MOVE, likely_cause = 1
39Two stations sharing one address, moderate trafficmoves on one address, likely_cause = 2
40Topology loop, line-rate duplicationmoves across many addresses, likely_cause = 3
41Any movelast_moved_from and last_moved_to both recorded
42Flap above FLAP_LIMITflapping asserts, flapping_addr names the address
43Entry with 10 000 refreshes, no movesq_confidence = strong

Conformance

#ScenarioExpected
44Eligible frame producing no learnv_missing_learn increments
45Ineligible frame producing a learnv_spurious_learn increments
46Learn carrying the wrong portv_wrong_port increments
47Age written during a lookup, no learn in flightv_lookup_refreshed increments
48Entry expiring below age_limitv_early_expiry increments

Flush and topology

#ScenarioExpected
49Port 5 link drops with 40 stations learned behind itexactly those 40 invalidated, c_flushed = 40
50Port 5 link dropsno entry naming another port is touched
51Port 5 link comes back upno flush; topology_suspect asserts, age_limit_out = 15 s
5230 s after the port came uptopology_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.

SymptomLikely causeThe observable that decides it
Everything floods, nothing forwardslearning never happensc_insert at zero while frames arrive; check LE_PORT_DOWN
One host always floods, others finethat host never transmits unsolicitedits entry absent; it appears immediately after the host is pinged
One host floods intermittentlyits entry ages out between its transmissionsq_last_seen_s and age_limit — the host's idle period exceeds it
Flooding rose after a config changeage_limit shortenedmiss_rate_high asserted, table not full
Multicast reaching only one porta group source address was learnedLE_GROUP_SRC count non-zero, and a table entry with bit 40 set
Table full, everything floodsaddress flood, or a genuinely large domainc_limited and worst_port; a real domain grows slowly, an attack fills in milliseconds
A host is intermittently unreachableduplicate MAC addressflapping_addr, likely_cause = 2, and the two ports in last_moved_from/to
Everything slow, broadcast stormtopology looplikely_cause = 3 — moves across many addresses at once
A host is reachable from some stations, not othersageing refreshed on lookupv_lookup_refreshed non-zero — the Section 8 bug
Entries vanish immediately after a topology changecorrect — a change invalidates port informationc_expired spiking once, then settling
A host's traffic works, replies never arrivethe reply's destination entry names a stale portthat entry's q_moves and q_last_seen_s
New hosts unlearnable, old ones finetable full and refusing rather than evictingLR_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 outputlookup_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

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.