Ethernet · Module 7
Address Filtering in Hardware
The receive filter is not a predicate but a priority-ordered set of accept reasons that overlap by design — and a station that cannot say which reason took a frame cannot explain the traffic it is receiving.
Chapter 5.4 explained why group matching is a one-sided test — a hash filter that says definitely not subscribed cheaply and can never say definitely subscribed — and built the mechanism that makes it affordable.
This chapter builds the engine around it, and the engine turns out not to be a predicate at all.
The instinct is that a filter answers one question: is this frame for me? A real filter answers several, and they overlap on purpose:
- is this address in my perfect-match table?
- is it a group address whose hash bit is set?
- is it the broadcast address?
- am I in all-multicast mode?
- am I in promiscuous mode?
A frame can satisfy three of those at once, and every one of them is a legitimate reason to accept it. The filter's output is not "yes" — it is "yes, and here is why" — and the chapter's argument is that a design which discards the why cannot explain the traffic it is receiving.
1. Scope — What This Chapter Owns
Chapter 5.4 owns the hash — why group matching must be approximate, why the error is one-sided, how the subscription vector is maintained, and how software completes the match. None of that is rebuilt here.
Chapter 5.3 owns the address — its layout, the individual/group flag that arrives first on the wire, and the comparison that can reject early.
This chapter owns the engine: the perfect-match table for the station's own addresses, the modes and how they compose, the priority order, the accept-reason encoding, and the bypass path that management traffic needs.
It does not own what happens after acceptance — Chapter 7.2 owns delivery and its provisionality — nor group management protocols, which sit above this layer.
The question this chapter answers that its neighbours do not: when several reasons to accept a frame apply at once, which one does the design report — and what breaks if it reports none?
2. Accept Reasons Overlap by Design
The overlap is not accidental and cannot be removed.
Promiscuous mode subsumes everything. By definition it accepts every frame, so for any frame it would have accepted for another reason, both reasons are true. A design cannot make promiscuous exclusive of the others without changing what promiscuous means.
All-multicast subsumes the hash. Every group address is accepted, including every subscribed one — so a subscribed multicast frame in all-multicast mode has two reasons.
Broadcast is a group address (Chapter 5.3), so it is also covered by all-multicast. And it can be additionally present in a perfect-match table, because nothing prevents software writing the all-ones address into an entry.
So a broadcast frame, on a station in promiscuous mode with all-multicast enabled and the broadcast address in its table, satisfies four reasons simultaneously. That is not a misconfiguration — it is a perfectly ordinary state for a bridge port that also runs its own protocol stack.
The accept decision is therefore an OR, and the reported reason is a priority selection over the reasons that happened to be true. Section 5 fixes the order and argues each position.
3. RTL 1 — The Perfect-Match Table
// SYNTHESIZABLE.
//
// Exact match against the station's own addresses. A small table, and
// the smallness is the design:
//
// a station has ONE burned-in address, plus a few more for virtual
// interfaces, containers, or a management identity. Eight to sixteen
// entries covers essentially every real device.
//
// Chapter 5.4 §3 showed why the GROUP case cannot be done this way -- a
// station may subscribe to hundreds of groups and the comparison must
// finish within a frame time. That is the hash's job, not this table's.
//
// The comparison is STREAMING, per Chapter 5.3 §6: octets are compared
// as they arrive and a mismatch retires an entry immediately. On traffic
// whose addresses are unrelated the first octet retires almost every
// entry, so the average work is one octet rather than six.
package rxfilter_pkg;
typedef enum logic [2:0] {
AR_NONE,
AR_PROMISCUOUS, // the filter was not filtering
AR_PERFECT, // this station's own address
AR_BROADCAST, // the all-ones address
AR_ALL_MULTICAST, // every group address accepted
AR_HASH // a subscribed group -- possibly a false positive
} accept_reason_e;
localparam int unsigned ADDR_OCTETS = 6;
endpackage
module perfect_match_table
import rxfilter_pkg::*;
import macaddr_pkg::*;
#(
parameter int unsigned ENTRIES = 8
) (
input logic clk,
input logic rst_n,
// Entry programming. An entry is only live once BOTH the address and
// its valid bit are written -- a half-programmed entry that matches on
// a partial address is the failure this ordering prevents.
input logic prog_valid,
input logic [$clog2(ENTRIES)-1:0] prog_index,
input mac_addr_t prog_addr,
input logic prog_enable,
input logic addr_start,
input logic oct_valid,
input logic [7:0] oct_data,
output logic match_valid,
output logic match_found,
output logic [$clog2(ENTRIES)-1:0] match_index,
// Octets consumed before the verdict. Telemetry, not control: it
// measures how well early rejection is working on this segment.
output logic [2:0] octets_compared
);
mac_addr_t entry_addr [ENTRIES];
logic [ENTRIES-1:0] entry_live;
logic [ENTRIES-1:0] alive_q; // entries still matching this frame
logic [2:0] idx_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
entry_live <= '0;
alive_q <= '0;
idx_q <= '0;
match_valid <= 1'b0;
match_found <= 1'b0;
match_index <= '0;
octets_compared <= '0;
for (int i = 0; i < ENTRIES; i++) entry_addr[i] <= '0;
end else begin
match_valid <= 1'b0;
if (prog_valid) begin
entry_addr[prog_index] <= prog_addr;
entry_live[prog_index] <= prog_enable;
end
if (addr_start) begin
// Every live entry is a candidate until an octet retires it.
alive_q <= entry_live;
idx_q <= '0;
end else if (oct_valid && (|alive_q)) begin
for (int i = 0; i < ENTRIES; i++)
if (alive_q[i] && (entry_addr[i][8*idx_q +: 8] != oct_data))
alive_q[i] <= 1'b0;
if (idx_q == 3'(ADDR_OCTETS - 1)) begin
match_valid <= 1'b1;
match_found <= |next_alive();
match_index <= first_alive();
octets_compared <= 3'(ADDR_OCTETS);
end else begin
idx_q <= idx_q + 1'b1;
end
end else if (oct_valid && !(|alive_q)) begin
// Every entry retired. The verdict is already known and the
// remaining octets are consumed without comparison -- which is
// the whole point of a streaming compare.
if (idx_q == 3'(ADDR_OCTETS - 1)) begin
match_valid <= 1'b1;
match_found <= 1'b0;
octets_compared <= idx_q + 3'd1;
end else begin
idx_q <= idx_q + 1'b1;
end
end
end
end
function automatic logic [ENTRIES-1:0] next_alive();
for (int i = 0; i < ENTRIES; i++)
next_alive[i] = alive_q[i] && (entry_addr[i][8*idx_q +: 8] == oct_data);
endfunction
function automatic logic [$clog2(ENTRIES)-1:0] first_alive();
first_alive = '0;
for (int i = ENTRIES-1; i >= 0; i--) if (next_alive()[i]) first_alive = i[$clog2(ENTRIES)-1:0];
endfunction
endmoduleClassification: synthesizable.
What it teaches: that the programming order is a correctness property, not a convention. An entry becomes live when its valid bit is set, and if software writes the valid bit before the address — or writes a six-octet address through a narrower register interface without an atomic commit — the entry is briefly live with a partial address, matching frames it should not. The table above takes address and enable in one transaction for exactly this reason.
Deliberately simplified: eight entries, no masking. Some designs add a per-entry mask so an entry can match a range, which is useful and doubles the comparison cost — and is a different structure from the hash, which trades exactness for size rather than adding flexibility.
Production implication: octets_compared is telemetry rather than control, and it measures something Chapter 5.3 §6 identified: the distribution of where the comparison rejects is a property of the segment. Rejections concentrated at octet zero mean unrelated addresses and early rejection working; rejections at the later octets mean the segment's addresses share prefixes — one manufacturer, or one hypervisor's locally administered range — which weakens both the timing argument and any hash keyed on leading octets.
4. The Modes, and What Each One Means
Five accept reasons, and the three that are modes rather than lookups deserve stating precisely, because their names are ambiguous in practice.
Promiscuous accepts every frame regardless of address. It is not a filter setting so much as a filter suspension — the address logic still runs, and its verdict is ignored.
All-multicast accepts every frame whose individual/group bit is set. It subsumes both the hash and broadcast. A bridge, a router running multicast protocols, and any device doing snooping will have it on.
Broadcast acceptance is the special one and it deserves its own control rather than being folded into all-multicast. Chapter 5.4 §10 established why: broadcast is sender-controlled, a station has no decision to make about it, and a station that silently stops accepting it comes up healthy and can reach nothing it has not already learned.
Which produces a design rule rather than a preference: the broadcast control may exist, and disabling it must be loud — a mode a design reports, not a default it can drift into.
And the composition is a union, never an intersection. Enabling all-multicast does not narrow anything; it adds a reason. Turning on promiscuous does not replace the other reasons; it adds one that happens to cover everything. A design that treats modes as a selector — one active mode at a time — has a structure that cannot express a bridge port that is promiscuous and has a perfect-match entry for its own management address.
5. RTL 2 — Composing the Modes, With the Order Written Down
// SYNTHESIZABLE.
//
// Composes the five accept reasons. Two outputs from the same inputs,
// answering two different questions:
//
// accept -- the OR. Any reason suffices, so this is a union and never
// an intersection or a selection.
// reason -- a PRIORITY selection among the reasons that applied.
//
// The priority order is a design decision and every position is argued:
//
// 1. PROMISCUOUS -- reported first because it is the most alarming.
// If the filter is not filtering, that is the finding, whatever
// else also matched.
// 2. PERFECT -- the frame really is this station's. The most
// specific reason, and the one a client cares about.
// 3. BROADCAST -- explicit, so a broadcast frame is never reported
// as generic multicast (Chapter 5.4 §10 kept them distinct).
// 4. ALL_MULTICAST -- a mode, so it outranks the hash: if all-multicast
// is on, the hash's opinion was not needed.
// 5. HASH -- the only reason that may be a FALSE POSITIVE
// (Chapter 5.4 §4), so it is reported last and should be treated
// as provisional by anything reading it.
module filter_mode_composer
import rxfilter_pkg::*;
(
input logic clk,
input logic rst_n,
// Independent enables. NOT an enumerated mode -- Section 4 explains
// why a selector cannot express a promiscuous port that still needs to
// recognise its own address.
input logic en_promiscuous,
input logic en_all_multicast,
input logic en_broadcast,
input logic en_hash,
input logic en_perfect,
input logic verdict_valid,
input logic is_group,
input logic is_broadcast,
input logic perfect_hit,
input logic hash_hit,
output logic accept,
output accept_reason_e reason,
output logic reason_valid,
// All reasons that applied, not only the reported one. Kept because a
// frame accepted for three reasons is a different situation from one
// accepted for a single reason, and the priority output hides that.
output logic [4:0] reasons_applied
);
wire r_promisc = en_promiscuous;
wire r_perfect = en_perfect && perfect_hit;
wire r_bcast = en_broadcast && is_broadcast;
wire r_allmc = en_all_multicast && is_group;
wire r_hash = en_hash && is_group && hash_hit;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
accept <= 1'b0;
reason <= AR_NONE;
reason_valid <= 1'b0;
reasons_applied <= '0;
end else begin
reason_valid <= verdict_valid;
if (verdict_valid) begin
// THE UNION. Any reason suffices.
accept <= r_promisc || r_perfect || r_bcast || r_allmc || r_hash;
reasons_applied <= {r_hash, r_allmc, r_bcast, r_perfect, r_promisc};
// THE PRIORITY. Exactly one reported, and the order is the
// header comment's.
if (r_promisc) reason <= AR_PROMISCUOUS;
else if (r_perfect) reason <= AR_PERFECT;
else if (r_bcast) reason <= AR_BROADCAST;
else if (r_allmc) reason <= AR_ALL_MULTICAST;
else if (r_hash) reason <= AR_HASH;
else reason <= AR_NONE;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that reasons_applied and reason are both needed and answer different questions. The priority output says how to label the frame; the bit vector says how many reasons were true, which is a different and often more useful fact. A frame accepted for one reason and a frame accepted for four look identical in the priority output, and the second is a configuration worth knowing about — it usually means a mode was enabled that makes another mode redundant.
Deliberately simplified: the enables are independent bits with no interlocks. A production design often refuses to clear en_broadcast without an explicit override, for Chapter 5.4 §10's reason — a station that silently declines broadcast appears healthy and can reach nothing new.
Production implication: the priority order puts promiscuous first and hash last, and both ends are deliberate. Promiscuous first because the filter is not filtering is the most alarming fact about a frame and should not be masked by a coincidental perfect match. Hash last because it is the only reason that can be a false positive — Chapter 5.4 §4's one-sided test — so a frame reported as AR_HASH may not be subscribed at all, and anything acting on the reason should treat that one as provisional.
6. RTL 3 — Encoding the Reason So It Survives
// SYNTHESIZABLE.
//
// Carries the accept reason alongside the frame and accumulates the
// per-reason statistics that answer "why am I receiving this traffic".
//
// The counters are the point. A receive path that counts only "frames
// accepted" can report a load and nothing about its cause; one that
// counts by reason distinguishes:
//
// a monitoring tool left in promiscuous mode
// an all-multicast bit set by a daemon that has since exited
// a stale perfect-match entry from a torn-down virtual interface
// a hash false positive rate that has drifted (Chapter 5.4 §8)
//
// All four present identically as "receive load is higher than expected".
module accept_reason_encoder
import rxfilter_pkg::*;
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic reason_valid,
input logic accept,
input accept_reason_e reason,
input logic [4:0] reasons_applied,
input logic [13:0] frame_octets,
output logic [CNT_W-1:0] c_by_reason [6],
output logic [CNT_W-1:0] o_by_reason [6], // octets, not frames
output logic [CNT_W-1:0] c_rejected,
// Frames accepted for more than one reason. A configuration signal
// rather than a traffic one: it means an enabled mode is subsuming
// another, which is usually not what somebody intended.
output logic [CNT_W-1:0] c_multi_reason,
output logic [4:0] most_recent_multi,
// Sticky: promiscuous mode ever having accepted a frame is worth
// keeping across a counter clear, because it is a configuration event
// and not a rate.
output logic promiscuous_ever_used
);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
bump = (&v) ? v : (v + 1'b1);
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 6; i++) begin
c_by_reason[i] <= '0;
o_by_reason[i] <= '0;
end
c_rejected <= '0;
c_multi_reason <= '0;
most_recent_multi <= '0;
promiscuous_ever_used <= 1'b0;
end else begin
if (clear) begin
for (int i = 0; i < 6; i++) begin
c_by_reason[i] <= '0;
o_by_reason[i] <= '0;
end
c_rejected <= '0;
c_multi_reason <= '0;
// promiscuous_ever_used deliberately survives.
end else if (reason_valid) begin
if (accept) begin
c_by_reason[reason] <= bump(c_by_reason[reason]);
// Octets as well as frames, because a reason accepting a few
// large frames and one accepting many small ones are different
// findings that a frame count alone cannot separate.
o_by_reason[reason] <= o_by_reason[reason] + CNT_W'(frame_octets);
if ($countones(reasons_applied) > 1) begin
c_multi_reason <= bump(c_multi_reason);
most_recent_multi <= reasons_applied;
end
if (reason == AR_PROMISCUOUS) promiscuous_ever_used <= 1'b1;
end else begin
c_rejected <= bump(c_rejected);
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that octets per reason and frames per reason are different findings. A hash false-positive rate that has drifted produces many small frames; an all-multicast mode left on by an exited daemon may produce a few very large ones. The frame counts look similar and the octet counts do not, and a design with only frame counts cannot tell which is consuming the receive path's capacity — which is Chapter 5.6 §12's frames-versus-octets distinction arriving on the accept side.
Deliberately simplified: six flat counters. A production design usually adds a small leaderboard of source addresses per reason, because "promiscuous mode is accepting 90% of the load" is a finding and "…and it is all from one address" is a fix.
Production implication: promiscuous_ever_used is sticky and survives clear because it is a configuration event rather than a rate. A device that has ever been in promiscuous mode may have delivered traffic to a client that was not addressed to it, and that fact matters after the mode is turned off — for a security review, for an explanation of a captured trace, or for understanding why a counter jumped last Tuesday. A rate can be cleared; a thing having happened cannot be un-happened.
7. The Bypass, and Why It Needs Its Own Guard
Some frames must reach a device even when its filter would reject them, and the reason is uncomfortable: the filter's configuration may be the thing that is broken.
A device whose perfect-match table was programmed wrongly, whose hash vector was corrupted, or whose modes were left in a state that rejects the traffic needed to fix them, cannot be repaired over the network — because the repair traffic is exactly what the filter is dropping.
So a bypass exists, and control-plane protocols rely on it: link-layer discovery, spanning-tree participation, and the reserved multicast addresses that Chapter 5.3's standard prefixes set aside for exactly this.
And a bypass is a hole in the mechanism the rest of this chapter is about, so it needs three constraints that a general one does not have:
It must be restricted by address, to a small set of reserved destinations — not "anything the management client asks for", which is a filter with extra steps.
It must be restricted by destination. A bypassed frame goes to the management client and nowhere else. Delivering it to the ordinary data path as well means the bypass has silently widened the filter for that address.
And it must be counted separately, because a bypass that is carrying volume is either under attack or misconfigured, and a bypass folded into the accept counters is invisible.
The failure mode if any of the three is missing is the same: the bypass becomes a second, undocumented promiscuous mode — with no enable bit, no counter, and nothing in the register map that says the filter is not filtering.
8. RTL 4 — The Guarded Bypass
// SYNTHESIZABLE.
//
// Delivers a narrow set of frames to the management client regardless of
// the filter's verdict, and cannot be widened at run time.
//
// Three guards, and each closes a different way for this to become a
// general promiscuous mode:
//
// 1. the address set is a PARAMETER, not a table software writes.
// A software-writable bypass list is a second perfect-match table
// that ignores the filter -- which is a filter, badly.
// 2. the destination is the management client only. A bypassed frame
// never reaches the data path, so a bypass cannot widen the filter
// for ordinary traffic.
// 3. the feature is counted separately and its rate is bounded, so a
// bypass carrying volume is visible rather than absorbed.
module mgmt_bypass_path
import rxfilter_pkg::*;
import macaddr_pkg::*;
#(
// The reserved destinations this device honours. Fixed at integration
// because the set is a property of which protocols the device
// participates in, not of its runtime configuration.
parameter int unsigned BYPASS_ENTRIES = 4,
// Frames per window above which the bypass is considered abused. A
// control-plane path carries a trickle; volume means something else.
parameter int unsigned BYPASS_RATE_LIMIT = 64,
parameter int unsigned WINDOW_BITS = 20,
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input mac_addr_t bypass_addr [BYPASS_ENTRIES],
input logic [BYPASS_ENTRIES-1:0] bypass_live,
input logic verdict_valid,
input mac_addr_t dest_addr,
input logic filter_accept,
// To the management client ONLY. There is deliberately no data-path
// output on this module.
output logic mgmt_deliver,
output logic bypassed, // accepted only because of this
output logic [CNT_W-1:0] c_bypassed,
output logic [CNT_W-1:0] win_bypassed,
output logic bypass_rate_exceeded,
output logic bypass_ever_used
);
logic [WINDOW_BITS-1:0] win_q;
logic [CNT_W-1:0] wcnt_q;
logic hit;
always_comb begin
hit = 1'b0;
for (int i = 0; i < BYPASS_ENTRIES; i++)
if (bypass_live[i] && (dest_addr == bypass_addr[i])) hit = 1'b1;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mgmt_deliver <= 1'b0;
bypassed <= 1'b0;
c_bypassed <= '0;
win_bypassed <= '0;
wcnt_q <= '0;
win_q <= '0;
bypass_rate_exceeded <= 1'b0;
bypass_ever_used <= 1'b0;
end else begin
mgmt_deliver <= 1'b0;
bypassed <= 1'b0;
if (clear) begin
c_bypassed <= '0;
// bypass_ever_used and bypass_rate_exceeded deliberately survive.
end
if (verdict_valid && hit) begin
mgmt_deliver <= 1'b1;
// `bypassed` marks only the frames the filter would have
// REJECTED. A reserved-address frame the filter accepted anyway
// is not a bypass event, and counting it as one would make the
// bypass counter track ordinary control traffic.
if (!filter_accept) begin
bypassed <= 1'b1;
bypass_ever_used <= 1'b1;
if (!(&c_bypassed)) c_bypassed <= c_bypassed + 1'b1;
wcnt_q <= wcnt_q + 1'b1;
end
end
if (&win_q) begin
win_bypassed <= wcnt_q;
if (wcnt_q > CNT_W'(BYPASS_RATE_LIMIT)) bypass_rate_exceeded <= 1'b1;
wcnt_q <= '0;
win_q <= '0;
end else begin
win_q <= win_q + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that bypassed must mark only the frames the filter would have rejected. A reserved-address frame that the filter accepted anyway — because all-multicast was on, say — reached the management client through the ordinary path, and counting it as a bypass event makes the bypass counter track normal control traffic. The counter then reads non-zero permanently and stops being a signal at all, which is how a guard becomes decoration.
Deliberately simplified: the address set arrives as an input array with live bits. A production design fixes the set at elaboration for the reason in the header comment — a software-writable bypass list is a second perfect-match table that ignores the filter's modes, which is a filter with the safety removed.
Production implication: this module has no data-path output, and that absence is the guard. A bypassed frame reaching the ordinary receive path would mean the bypass had widened the filter for that address — the frame goes to the client and to bridging, or and to the protocol stack. Making the omission structural rather than a condition means no future edit can add the path by accident, which is the same argument Chapter 5.9 §9 made for a state with no early exit.
9. RTL 5 — Checking That the Modes Mean What They Say
// SYNTHESIZABLE MONITOR.
//
// Verifies that each mode means what it is documented to mean, stated as
// a relation between an enable and an outcome.
//
// The failures this catches are composition failures rather than lookup
// failures: every individual reason can be correct while the union is
// wrong, because the union is where a design accidentally implements a
// selector instead (Section 4).
//
// And it checks the one relation that is NOT symmetric: broadcast
// acceptance being disabled is legal and consequential, so it is
// reported rather than forbidden (Chapter 5.4 §10).
module filter_conformance_monitor
import rxfilter_pkg::*;
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic en_promiscuous,
input logic en_all_multicast,
input logic en_broadcast,
input logic en_hash,
input logic en_perfect,
input logic reason_valid,
input logic accept,
input accept_reason_e reason,
input logic [4:0] reasons_applied,
input logic is_group,
input logic is_broadcast,
input logic perfect_hit,
output logic semantic_error,
output logic [CNT_W-1:0] c_semantic_error,
output logic [2:0] first_error_kind,
// Not an error. A configuration this device is in, reported because a
// station that declines broadcast appears healthy and can reach
// nothing it has not already learned.
output logic broadcast_declined_mode,
output logic [CNT_W-1:0] c_broadcast_declined,
output logic any_violation // sticky
);
localparam logic [2:0] E_PROMISC_REJECTED = 3'd1;
localparam logic [2:0] E_ALLMC_REJECTED = 3'd2;
localparam logic [2:0] E_PERFECT_REJECTED = 3'd3;
localparam logic [2:0] E_ACCEPT_NO_REASON = 3'd4;
localparam logic [2:0] E_REASON_NOT_APPLIED = 3'd5;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
semantic_error <= 1'b0; c_semantic_error <= '0; first_error_kind <= '0;
broadcast_declined_mode <= 1'b0; c_broadcast_declined <= '0;
any_violation <= 1'b0;
end else begin
semantic_error <= 1'b0;
broadcast_declined_mode <= 1'b0;
if (clear) begin
c_semantic_error <= '0; c_broadcast_declined <= '0;
// first_error_kind and any_violation deliberately survive.
end else if (reason_valid) begin
automatic logic [2:0] kind = 3'd0;
// Promiscuous means NOTHING is rejected. The strongest relation
// and the one a selector-shaped design breaks first.
if (en_promiscuous && !accept) kind = E_PROMISC_REJECTED;
// All-multicast means no GROUP frame is rejected.
else if (en_all_multicast && is_group && !accept) kind = E_ALLMC_REJECTED;
// A perfect-match hit, with perfect matching enabled, is always
// accepted -- whatever else is or is not on.
else if (en_perfect && perfect_hit && !accept) kind = E_PERFECT_REJECTED;
// An acceptance with no reason applied is a decision with no
// justification, which means the union and the priority encoder
// were computed from different inputs.
else if (accept && (reasons_applied == 5'd0)) kind = E_ACCEPT_NO_REASON;
// And the reported reason must be one that actually applied.
else if (accept && (reason != AR_NONE) &&
!reasons_applied[reason_index(reason)]) kind = E_REASON_NOT_APPLIED;
if (kind != 3'd0) begin
semantic_error <= 1'b1;
c_semantic_error <= c_semantic_error + 1'b1;
any_violation <= 1'b1;
if (first_error_kind == 3'd0) first_error_kind <= kind;
end
// Reported, not forbidden.
if (is_broadcast && !en_broadcast && !accept) begin
broadcast_declined_mode <= 1'b1;
c_broadcast_declined <= c_broadcast_declined + 1'b1;
end
end
end
end
function automatic int unsigned reason_index(input accept_reason_e r);
case (r)
AR_PROMISCUOUS: reason_index = 0;
AR_PERFECT: reason_index = 1;
AR_BROADCAST: reason_index = 2;
AR_ALL_MULTICAST: reason_index = 3;
default: reason_index = 4;
endcase
endfunction
endmoduleClassification: synthesizable monitor.
What it teaches: that the mode semantics are relations, not values, and they are what a selector-shaped design breaks. Every individual lookup can be correct — the table matches, the hash matches, the group bit is decoded — while the composition implements "one active mode" and rejects a frame that promiscuous mode should have taken. E_PROMISC_REJECTED is the single most valuable check in this module, because a design that fails it has the structural error of Section 4 and no other check would see it.
Deliberately simplified: five relations. A design with more modes — unicast-promiscuous separate from full promiscuous, per-VLAN filtering — adds one relation each, and the pattern does not change.
Production implication: broadcast_declined_mode is reported rather than forbidden, and the distinction is the whole design of the output. Declining broadcast is a legal configuration; it is also one in which a station comes up, shows a good link, passes every local test, and can reach nothing it has not already learned (Chapter 5.4 §10). An assertion would forbid a legal state; a counter makes an unusual one visible, which is the correct instrument for a configuration that is legitimate and consequential.
10. What the Filter Does Not Do
It is worth stating the negative results, because a mechanism that decides which frames a station receives invites an assumption it cannot support.
The filter is not a security boundary. It decides what this station processes, not what reaches it — every frame on the segment still arrives at the physical layer, and Chapter 5.4 §2 costed that: an other-unicast frame is discarded at the first octet, cheaply, but it was received.
It does not authenticate anything. A destination address is a claim by the sender about who should receive the frame, and Chapter 5.3 §10 established that address uniqueness is an administrative claim with five links in its chain, only the first of which any mechanism enforces. A perfect-match hit means the address matched, not that the sender was entitled to use it.
Its group verdict is one-sided. Chapter 5.4 §4: a hash accept means possibly subscribed. A design that treats AR_HASH as proof of subscription has mistaken a cheap test for an exact one, which is why the priority order reports it last and why software completes the match.
And it can be turned off entirely. Promiscuous mode is a bit — a driver sets it, a capture tool sets it, and a stale one leaves it set. promiscuous_ever_used exists because a filter that can be disabled is not a boundary, and knowing that it was disabled matters after the fact.
So what is the filter for? Exactly one thing: reducing the work this station does on traffic that is not its concern. That is a performance function, and it is valuable — Chapter 5.6 §11 showed per-frame work is what runs out first — but it is not protection, and treating it as protection puts a security property on a mechanism with an enable bit.
11. Assertions — Union, Priority and the Overlap That Must Not Be Forbidden
// ---------------------------------------------------------------------
// P1 -- THE UNION. Accept is the OR of the reasons. Not a selection, not
// an intersection -- Section 5's central relation.
// ---------------------------------------------------------------------
property p_accept_is_union;
@(posedge clk) disable iff (!rst_n)
reason_valid |-> (accept == (|reasons_applied));
endproperty
a_accept_is_union: assert property (p_accept_is_union)
else $error("accept is not the OR of the applicable reasons");
// ---------------------------------------------------------------------
// P2 -- The reported reason is one that actually applied.
// ---------------------------------------------------------------------
property p_reported_reason_applied;
@(posedge clk) disable iff (!rst_n)
(reason_valid && accept) |-> reasons_applied[reason_index(reason)];
endproperty
a_reported_reason_applied: assert property (p_reported_reason_applied);
// ---------------------------------------------------------------------
// P3 -- THE PRIORITY. The reported reason is the HIGHEST-priority one
// that applied. This is the correct shape -- compare the rejected
// property below, which asks for exclusivity instead.
// ---------------------------------------------------------------------
property p_reason_is_highest_priority;
@(posedge clk) disable iff (!rst_n)
(reason_valid && accept) |-> (reason == highest_priority(reasons_applied));
endproperty
a_reason_is_highest_priority: assert property (p_reason_is_highest_priority);
// ---------------------------------------------------------------------
// P4 -- No reasons applied means no acceptance.
// ---------------------------------------------------------------------
property p_no_reason_no_accept;
@(posedge clk) disable iff (!rst_n)
(reason_valid && (reasons_applied == 5'd0)) |-> !accept;
endproperty
a_no_reason_no_accept: assert property (p_no_reason_no_accept);
// ---------------------------------------------------------------------
// P5 -- PROMISCUOUS MEANS PROMISCUOUS. Nothing is rejected. The relation
// a selector-shaped design breaks first.
// ---------------------------------------------------------------------
property p_promiscuous_accepts_everything;
@(posedge clk) disable iff (!rst_n)
(reason_valid && en_promiscuous) |-> accept;
endproperty
a_promiscuous_accepts_everything: assert property (p_promiscuous_accepts_everything)
else $error("a frame was rejected while promiscuous mode was enabled");
// ---------------------------------------------------------------------
// P6 -- All-multicast accepts every group frame.
// ---------------------------------------------------------------------
property p_all_multicast_accepts_groups;
@(posedge clk) disable iff (!rst_n)
(reason_valid && en_all_multicast && is_group) |-> accept;
endproperty
a_all_multicast_accepts_groups: assert property (p_all_multicast_accepts_groups);
// ---------------------------------------------------------------------
// P7 -- A perfect-match hit is always accepted when perfect matching is
// enabled, regardless of any other mode.
// ---------------------------------------------------------------------
property p_perfect_hit_accepted;
@(posedge clk) disable iff (!rst_n)
(reason_valid && en_perfect && perfect_hit) |-> accept;
endproperty
a_perfect_hit_accepted: assert property (p_perfect_hit_accepted);
// ---------------------------------------------------------------------
// P8 -- A promiscuous port still recognises its own address. The
// perfect-match verdict is not discarded when promiscuous is on, which
// is Section 4's bridge-port case.
// ---------------------------------------------------------------------
property p_perfect_visible_under_promiscuous;
@(posedge clk) disable iff (!rst_n)
(reason_valid && en_promiscuous && en_perfect && perfect_hit)
|-> reasons_applied[1];
endproperty
a_perfect_visible_under_promiscuous: assert property (p_perfect_visible_under_promiscuous);
// ---------------------------------------------------------------------
// P9 -- The hash reason only ever applies to group addresses. Chapter
// 5.3: the individual/group bit selects the pipeline.
// ---------------------------------------------------------------------
property p_hash_only_for_groups;
@(posedge clk) disable iff (!rst_n)
(reason_valid && reasons_applied[4]) |-> is_group;
endproperty
a_hash_only_for_groups: assert property (p_hash_only_for_groups);
// ---------------------------------------------------------------------
// P10 -- Broadcast is a group address, so anything reporting it as an
// individual address has mis-decoded the first bit.
// ---------------------------------------------------------------------
property p_broadcast_is_group;
@(posedge clk) disable iff (!rst_n)
is_broadcast |-> is_group;
endproperty
a_broadcast_is_group: assert property (p_broadcast_is_group);
// ---------------------------------------------------------------------
// P11 -- A half-programmed perfect-match entry never matches. Address
// and enable arrive together, so an entry cannot be live with a partial
// address (Section 3).
// ---------------------------------------------------------------------
property p_no_partial_entry_match;
@(posedge clk) disable iff (!rst_n)
(match_valid && match_found) |-> entry_live[match_index];
endproperty
a_no_partial_entry_match: assert property (p_no_partial_entry_match);
// ---------------------------------------------------------------------
// P12 -- The streaming compare never consumes more than the address.
// ---------------------------------------------------------------------
property p_compare_bounded;
@(posedge clk) disable iff (!rst_n)
match_valid |-> (octets_compared <= 3'(ADDR_OCTETS));
endproperty
a_compare_bounded: assert property (p_compare_bounded);
// ---------------------------------------------------------------------
// P13 -- THE BYPASS GUARD. A bypassed frame reaches the management
// client and nothing else. Written as an absence, because the module has
// no data-path output at all (Section 8).
// ---------------------------------------------------------------------
property p_bypass_is_mgmt_only;
@(posedge clk) disable iff (!rst_n)
bypassed |-> (mgmt_deliver && !data_path_deliver);
endproperty
a_bypass_is_mgmt_only: assert property (p_bypass_is_mgmt_only)
else $error("a bypassed frame reached the data path");
// ---------------------------------------------------------------------
// P14 -- The bypass only fires on the reserved address set.
// ---------------------------------------------------------------------
property p_bypass_only_reserved;
@(posedge clk) disable iff (!rst_n)
bypassed |-> bypass_hit;
endproperty
a_bypass_only_reserved: assert property (p_bypass_only_reserved);
// ---------------------------------------------------------------------
// P15 -- A frame the filter accepted anyway is not a bypass event. The
// counter must not track ordinary control traffic (Section 8).
// ---------------------------------------------------------------------
property p_bypass_only_when_filter_rejected;
@(posedge clk) disable iff (!rst_n)
bypassed |-> !filter_accept;
endproperty
a_bypass_only_when_filter_rejected: assert property (p_bypass_only_when_filter_rejected);
// ---------------------------------------------------------------------
// P16 -- Configuration events survive a counter clear.
// ---------------------------------------------------------------------
property p_config_events_survive_clear;
@(posedge clk) disable iff (!rst_n)
clear |=> ($stable(promiscuous_ever_used) && $stable(bypass_ever_used));
endproperty
a_config_events_survive_clear: assert property (p_config_events_survive_clear);
// ---------------------------------------------------------------------
// P17 -- COVERAGE. A frame accepted for more than one reason, which is
// the state the rejected property below would forbid.
// ---------------------------------------------------------------------
c_multi_reason_accept: cover property (
@(posedge clk) disable iff (!rst_n)
(reason_valid && accept && ($countones(reasons_applied) > 1))
);
// ---------------------------------------------------------------------
// P18 -- COVERAGE. Each reason reported at least once, including the
// bypass and the promiscuous case.
// ---------------------------------------------------------------------
c_reason_promiscuous: cover property (@(posedge clk) disable iff (!rst_n) (reason == AR_PROMISCUOUS));
c_reason_perfect: cover property (@(posedge clk) disable iff (!rst_n) (reason == AR_PERFECT));
c_reason_broadcast: cover property (@(posedge clk) disable iff (!rst_n) (reason == AR_BROADCAST));
c_reason_allmc: cover property (@(posedge clk) disable iff (!rst_n) (reason == AR_ALL_MULTICAST));
c_reason_hash: cover property (@(posedge clk) disable iff (!rst_n) (reason == AR_HASH));
c_bypass_used: cover property (@(posedge clk) disable iff (!rst_n) bypassed);12. Verification — Twenty-Four Scenarios and a Frame With Four Reasons
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Own unicast, perfect enabled | address in entry 0 | accept; reason = AR_PERFECT |
| 2 | Other unicast, nothing enabled | unrelated address | reject; reasons_applied = 0 (P4) |
| 3 | Early rejection | address differing in octet 0 | octets_compared = 1 — the streaming compare |
| 4 | Late rejection | address differing in octet 5 | octets_compared = 6 — the worst case |
| 5 | Full match | all six octets match | match_found; match_index correct |
| 6 | Half-programmed entry | write the address, not the enable | no match (P11) |
| 7 | Entry disabled mid-traffic | clear an enable | subsequent frames reject; in-flight unaffected |
| 8 | Broadcast, broadcast enabled | all-ones address | AR_BROADCAST — not AR_ALL_MULTICAST |
| 9 | Broadcast, broadcast disabled | all-ones, en_broadcast low | rejected; broadcast_declined_mode reported, not an error |
| 10 | Subscribed group | hash bit set | AR_HASH |
| 11 | Unsubscribed group, hash bit set | a colliding address | AR_HASH — a false positive (Chapter 5.4 §8) |
| 12 | Group, all-multicast on | any group address | AR_ALL_MULTICAST — outranks the hash |
| 13 | Promiscuous | any address at all | accept; AR_PROMISCUOUS (P5) |
| 14 | Promiscuous plus perfect hit | own address, promiscuous on | AR_PROMISCUOUS reported, reasons_applied[1] still set (P8) |
| 15 | Promiscuous, nothing else | unrelated address | accepted, and reasons_applied has exactly one bit |
| 16 | Hash on a unicast address | individual address, hash bit set | hash reason must not apply (P9) |
| 17 | Four reasons at once | broadcast, promiscuous + all-mc + broadcast + perfect entry | $countones(reasons_applied) = 4; c_multi_reason increments |
| 18 | Octets by reason | large frames on one reason, small on another | o_by_reason separates them where c_by_reason does not |
| 19 | Promiscuous stickiness | enable promiscuous, use it, disable, clear | promiscuous_ever_used survives (P16) |
| 20 | Bypass, filter rejects | reserved address, filter off | mgmt_deliver; bypassed; no data-path delivery (P13) |
| 21 | Bypass, filter accepts | reserved address, all-multicast on | mgmt_deliver; bypassed low (P15) |
| 22 | Bypass on a non-reserved address | any other address, filter rejects | bypassed low (P14) |
| 23 | Bypass rate | 200 bypassed frames in a window, limit 64 | bypass_rate_exceeded |
| 24 | Semantic violation | force a reject while promiscuous is on | semantic_error; first_error_kind = promiscuous-rejected |
13. Debugging — Why Am I Receiving This
Symptom — receive load far higher than the traffic addressed to this station.
Read c_by_reason before anything else. AR_PROMISCUOUS dominating means the filter is not filtering — a monitoring tool, a capture left running, a driver that set the bit and exited. AR_ALL_MULTICAST dominating means a mode enabled by a routing or bridging daemon. AR_HASH dominating means either real subscriptions or Chapter 5.4 §8's false positives, and that chapter's delivered-versus-false split separates them.
Symptom — the frame count by reason looks reasonable and the receive path is saturated.
Read o_by_reason instead. A few large frames on one reason and many small ones on another have similar frame counts and very different octet counts, and the receive path's capacity is consumed by whichever is larger. It is Chapter 5.6 §12's frames-versus-octets distinction, on the accept side.
Symptom — a bridge port stops responding to management while forwarding normally.
The classic selector failure of Section 4. The port is promiscuous, and the design implemented modes as one-at-a-time, so the perfect-match verdict is discarded and management frames go to the forwarding path with everything else. Confirm with reasons_applied[1] on a frame addressed to the management identity: if it is zero while a perfect entry holds that address, the composition is a selector.
Symptom — a station comes up healthy and cannot reach anything new.
Check broadcast_declined_mode. A station that is not accepting broadcast misses the discovery and resolution traffic that lets it find peers it has not already learned — Chapter 5.4 §10 — and it looks entirely healthy from every local test. The counter exists precisely because this configuration is legal and its symptom is silence.
Symptom — traffic reaching the management client that was never addressed to it.
The bypass has widened. Check that bypassed only ever fires on the reserved address set (P14) and that no data-path delivery accompanies it (P13). A software-writable bypass list is the usual cause: it becomes a second perfect-match table that ignores the filter's modes, which is Section 8's first guard.
Symptom — the bypass counter is rising steadily.
A control-plane path carries a trickle. Volume means either an attack or a misconfiguration in which ordinary traffic is hitting a reserved address. bypass_rate_exceeded is the signal, and the first thing to check is whether bypassed is being asserted on frames the filter would have accepted anyway — which makes the counter track normal control traffic and stop being a signal.
Symptom — early rejection is not saving anything; octets_compared averages near six.
The segment's addresses share prefixes — one manufacturer, or one hypervisor's locally administered range — so the comparison runs to the later octets before retiring an entry. Not a fault, and worth knowing: Chapter 5.3 §6 showed it weakens both the timing argument for streaming comparison and the quality of any hash keyed on leading octets.
14. Common Misconceptions
"The filter answers one question: is this frame for me?"
The wrong model: a predicate with a yes-or-no answer.
What it costs: a station receiving unexpected traffic has no way to ask why. A monitoring tool left in promiscuous mode, a stale multicast subscription, and a leftover perfect-match entry all present identically as "load is higher than expected, and every frame was accepted correctly".
The corrected model: the filter is a set of accept reasons that overlap, composed by a union with a priority for reporting. The useful output is not the decision but the reason, because the reason names the configuration that produced the traffic.
"Modes are alternatives — one is active at a time."
The wrong model: an enumerated mode register.
What it costs: a promiscuous port cannot recognise its own address, because selecting promiscuous discards the perfect-match verdict. The bridge works and stops responding to management, delivering its own management frames into the forwarding path while every counter shows frames accepted.
The corrected model: independent enables composed by union. Enabling all-multicast does not narrow anything; it adds a reason. A bridge port is legitimately promiscuous and perfect-matching at the same time.
"Accept reasons should be mutually exclusive."
The wrong model: one frame, one reason — the hygiene Chapter 7.3 required for validity classes.
What it costs: Section 11's rejected property, and then the masking fix that satisfies it — which is exactly the selector failure above, arrived at through an assertion rather than through a register design.
The corrected model: validity classes are exclusive because the standard's counting rule makes them so. Accept reasons are deliberately overlapping, and the correct property is that the reported reason is the highest-priority one that applied — a statement about the label, not about the underlying facts.
"A perfect-match table is how you do multicast; just make it bigger."
The wrong model: one exact-match structure for all addresses.
What it costs: Chapter 5.4 §3's scaling wall. A station may subscribe to hundreds of groups and the comparison must complete within a frame time, so the table becomes hundreds of parallel comparators or a multi-cycle search that does not fit.
The corrected model: two structures for two problems. A small exact table for the station's own addresses — a handful, always — and a hash for groups, whose unbounded count is exactly why an approximate one-sided test is the affordable answer.
"The management bypass is just another accept path."
The wrong model: a bypass is a filter entry that happens to be higher priority.
What it costs: a general bypass is a second promiscuous mode with no enable bit, no counter, and nothing in the register map saying the filter is not filtering.
The corrected model: a bypass needs three guards a normal accept path does not: a fixed address set rather than a software-writable list, delivery to the management client only and never the data path, and its own counter and rate bound — because a control-plane path carries a trickle and volume means something is wrong.
15. Interview Reasoning
"How does a NIC decide whether to accept a frame?"
The weak answer is "it compares the destination address". The answer that ends the topic reframes it: the filter is not a predicate, it is a set of accept reasons — promiscuous, perfect match, broadcast, all-multicast, hash — and they overlap on purpose. Acceptance is the union; the reported reason is a priority selection. The payoff is why the reason matters more than the decision: a station receiving unexpected traffic is asking why, and the answer is a mode somebody enabled rather than an address somebody sent.
"Why can't the filter modes be one enumerated setting?"
Because they are not alternatives. A bridge port is promiscuous and needs to recognise its own management address — and an enumerated mode discards the perfect-match verdict when promiscuous is selected. The failure is silent: the bridge forwards everything, its own management frames go into the forwarding path, and the station stops responding to management while its counters show it receiving every frame on the segment.
"Should the accept reasons be mutually exclusive?"
No, and the contrast with frame validity is the point. Validity classes are exclusive, because the standard counts a frame by exactly one status. Accept reasons overlap by construction — promiscuous subsumes everything, all-multicast subsumes the hash, broadcast is a group address and can also be in a perfect table. The strong close is what an exclusivity assertion would do: it fires on ordinary configurations, and the cheapest fix is masking, which breaks the promiscuous bridge port.
"Why does a management bypass need special treatment?"
Because it exists to work when the filter's configuration is the thing that is broken — so it cannot be built out of the filter. The complete answer gives the three guards: a fixed address set, because a software-writable one is a filter with the safety removed; delivery to the management client only, so a bypass cannot widen the filter for ordinary traffic; and a counter with a rate bound, because a control-plane path carries a trickle and a bypass carrying volume is either an attack or a misconfiguration.
16. Understanding Check
Because several reasons to accept a frame can be true at once, and each is legitimate.
Promiscuous mode accepts everything by definition, so for any frame another reason would have matched, both are true. All-multicast accepts every group address, which subsumes the hash. Broadcast is a group address and can additionally sit in a perfect-match table.
A broadcast frame on a promiscuous bridge port with all-multicast enabled and the broadcast address in its table satisfies four reasons simultaneously — and that is an ordinary configuration for a bridge that also runs its own protocol stack.
So the accept decision is a union: any reason suffices. And the reported reason is a priority selection among the ones that happened to apply.
Which makes the reason the output that matters. The accept bit answers a question nobody asks — of course the frame was accepted, it arrived. The question a station actually has is why am I receiving this, and that is answered by a reason, which names a configuration, not by a decision.
17. What's Next
The claim this chapter defended: the filter is a priority-ordered set of accept reasons that overlap by design, and the reason is the output that matters.
Acceptance is a union — promiscuous, perfect match, broadcast, all-multicast, hash, any one of which suffices — and several are routinely true at once on an entirely ordinary device. So the modes must be independent enables rather than a selector, or a promiscuous bridge port loses the ability to recognise its own address and delivers its management traffic into the forwarding path while every counter reads normally. And exclusivity, which was required one chapter ago for validity classes, is here exactly the wrong property: the honest form is that the reported reason is the highest-priority one that applied.
Which is why the useful outputs are per-reason counters in frames and octets, a vector recording every reason that applied, sticky records of configuration events like promiscuous mode ever having been used, and a bypass that is narrow by construction rather than by policy.
Module 7 ends here. Four chapters took the MAC's datapaths from an ordered transmit assembly, through a receive path whose every decision is provisional, to a validity classification with three owners and a filter with five reasons.
Chapter 8.1 — Serialization Delay opens the performance module with the latency term that is most often left out.** Serialization delay is simply the frame's size divided by the line rate — the time to clock the octets onto the wire — and it is arithmetic anybody can do. What makes it worth a chapter is that its relative size changes by four orders of magnitude across the rates this track has covered, so a latency budget that is dominated by serialization at 10 Mb/s is dominated by something else entirely at 100 Gb/s. And the change surprises people in the direction they do not expect.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
One Frame, End to End
A frame's journey down the stack and back up the other side, stage by stage. The transmit path decides and the receive path must discover — at four layers, not one — and that asymmetry is why the receive half of every Ethernet design is the larger, later and buggier one.
- Related topic
Unicast, Multicast and Broadcast
A group address is not looked up, it is tested — and the test is one-sided. Hardware can say definitely-not-subscribed cheaply and can never say definitely-subscribed, so the filter's job is to be cheap and to be wrong only in the direction that costs work rather than the direction that loses frames.
- Related topic
CRC Checking and the Residue
A receiver feeds the data and the check sequence through one division and tests for a fixed constant — no held value, no captured FCS, no need to know where the payload ended. And that constant is a fingerprint of all four conventions at once.
- Related topic
The Receive Path
A transmitter assembles from settled quantities; a receiver discovers, and every decision stays provisional until the check sequence at the very end — so the pipeline either waits for the verdict or acquires the ability to withdraw what it has already delivered.
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.
