Ethernet · Module 13
Access Ports, Trunk Ports and Tag Handling
A frame's VLAN is assigned by the receiving port, not carried by the frame. Across an untagged link it is destroyed and re-invented — which is why a native-VLAN mismatch leaks both ways with every check clean.
Chapter 13.2 parsed a tag that had already arrived and deferred, twice, the question of how it got there. Chapter 13.1 consumed a VID on every interface and never said who assigned it.
This is the answer, and it is not a configuration table. It is a transformation applied to the frame itself, at two places, asymmetrically.
At ingress the port assigns a VLAN. A tagged frame brings one; an untagged frame has none, and the receiving port supplies its own — the PVID. At egress the port represents that VLAN, by inserting a tag, stripping one, or leaving the frame alone.
And the consequence that organises the whole chapter: across an untagged link, a frame's VLAN does not exist. It is not in the frame, not on the wire, and not recoverable from anything transmitted. It exists only inside the switches at each end, as a number each of them chose independently — which is why Chapter 13.1 §12's isolation invariant can hold perfectly at both ends of a link that is leaking traffic between VLANs in both directions.
1. Scope — What This Chapter Owns
This chapter owns the transformation: the per-port configuration that drives it, the six ingress cases, the assignment rule, ingress filtering, the three egress actions, what stripping a tag does to a minimum-length frame, and the native-VLAN rule.
It does not own the tag's fields — Chapter 13.2 owns TPID, PCP, DEI and VID, and the parser that finds them. This chapter moves tags; that chapter reads them.
It does not own why VLANs exist — Chapter 13.1 established the requirement, the isolation invariant and what must become per-VLAN.
And it does not own the lookup datapath. The VID-to-index map, the shared table with its widened key, and PCP into egress queues are Chapter 13.4. This chapter delivers a VID to that chapter's input and represents its output on the wire.
One dependency is load-bearing throughout. Chapter 13.2 §13 established that everything a switch consumes precedes the insertion point at octet 12 — so every failure in this chapter forwards correctly, and the third column of Section 20's table matters more than the first two.
2. A Frame's VLAN Is Assigned, Not Carried
Start with the claim that sounds wrong and is the foundation of everything else.
An untagged Ethernet frame contains no VLAN information of any kind. There is no field, no flag, no implicit encoding. Chapter 5.1's layout is destination, source, EtherType, payload, FCS — and none of them says anything about a VLAN.
So when an untagged frame arrives at a port, the switch must invent one. The rule is that the port's configured PVID — its port VLAN identifier, sometimes called the default or native VLAN — becomes the frame's VLAN for as long as it is inside this switch.
Which produces a sequence worth following carefully:
| Step | Where the VLAN lives |
|---|---|
| a station transmits | nowhere — the station has no VLAN concept |
| the frame crosses the access link | nowhere — the wire carries no VID |
| it arrives at switch A's port 3 | assigned: port 3's PVID, say 10 |
| it crosses switch A's fabric | a descriptor field |
| it leaves on a trunk | octets 14–15 — a tag is inserted |
| it arrives at switch B | the tag's VID, 10 — carried, not invented |
| it leaves on switch B's access port | stripped — nowhere again |
| it arrives at the destination station | nowhere |
The VLAN exists for exactly the middle of that journey. At both ends it does not, and the two switches agree only because a tag carried it between them.
Remove the tag from that middle hop — make it an untagged link — and the two switches agree only because two operators configured the same number twice.
3. RTL 1 — The Per-Port Configuration
Five fields per port drive every transformation in this chapter, and three of them are one bit or a handful.
// -----------------------------------------------------------------------
// vlanport_pkg -- shared types for VLAN port modes.
//
// This chapter owns the TRANSFORMATION. Chapter 13.2 owns the tag's
// fields, Chapter 13.1 owns why VLANs exist, and Chapter 13.4 owns the
// lookup datapath this chapter feeds.
// -----------------------------------------------------------------------
package vlanport_pkg;
localparam int VID_W = 12;
localparam int PCP_W = 3;
// What a port will accept. The third value exists for a port facing a
// device that always tags -- a virtualisation host or a router -- and
// is the only place a station is expected to know what a VLAN is.
typedef enum logic [1:0] {
AFT_ADMIT_ALL = 2'd0,
AFT_TAGGED_ONLY = 2'd1,
AFT_UNTAGGED_ONLY = 2'd2
} accept_types_e;
// What the egress does to represent a VLAN on this port.
typedef enum logic [1:0] {
EG_STRIP = 2'd0, // VLAN is in this port's UNTAGGED set
EG_TAG = 2'd1, // VLAN is in this port's TAGGED set
EG_PASS = 2'd2, // already correct -- no transformation
EG_DROP = 2'd3 // port is not a member of this VLAN
} egress_action_e;
// Why a frame was refused. Each has a different remedy and only two of
// them are faults.
typedef enum logic [2:0] {
VR_OK = 3'd0,
VR_WRONG_TYPE = 3'd1, // acceptable-frame-types said no
VR_INGRESS_FILTER = 3'd2, // port is not a member of the assigned VID
VR_RESERVED_VID = 3'd3, // 0 handled separately, 4095 refused
VR_EGRESS_NOT_MEM = 3'd4, // Chapter 13.1's isolation, at the egress
VR_LENGTH = 3'd5 // transformation produced an illegal length
} vlan_reject_e;
// Per-port configuration. Section 4's matrix is entirely a function of
// these five fields plus Chapter 13.1's membership array.
typedef struct packed {
logic [VID_W-1:0] pvid; // assigned to untagged arrivals
accept_types_e accept;
logic ingress_filter; // Section 8 -- optional, and it matters
logic is_trunk; // annotation; behaviour comes from the sets
} port_cfg_t;
endpackage// -----------------------------------------------------------------------
// port_mode_config -- the per-port state that drives every transformation.
//
// "Access port" and "trunk port" are NOT modes in the hardware. They are
// names for particular settings of these fields, and a design that
// implements them as an enumerated mode has hard-coded two points of a
// continuous space and made the third case -- a hybrid port -- impossible.
// -----------------------------------------------------------------------
module port_mode_config
import vlanport_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int N_VLANS = 64,
parameter int VIDX_W = 6,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic cfg_valid,
input logic [4:0] cfg_port,
input port_cfg_t cfg_data,
input logic cfg_untagged_set_we,
input logic [VIDX_W-1:0] cfg_untagged_vidx,
input logic cfg_untagged_bit,
input logic [4:0] rd_port,
input logic [VIDX_W-1:0] rd_vidx,
output port_cfg_t cfg,
output logic vlan_is_untagged_here,
output logic [CNT_W-1:0] bits_pvid,
output logic [CNT_W-1:0] bits_untagged_sets,
output logic [CNT_W-1:0] bits_total,
output logic [5:0] n_access_like,
output logic [5:0] n_trunk_like
);
port_cfg_t cfgs [N_PORTS];
// THE UNTAGGED SET. One bit per VLAN per port, and it is a SEPARATE
// vector from Chapter 13.1's membership: a port may be a member of a
// VLAN and carry it tagged, or be a member and carry it untagged. The
// two questions are independent.
logic [N_VLANS-1:0] untagged [N_PORTS];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int p = 0; p < N_PORTS; p++) begin
cfgs[p] <= '0;
untagged[p] <= '0;
end
end else begin
if (cfg_valid) cfgs[cfg_port] <= cfg_data;
if (cfg_untagged_set_we) untagged[cfg_port][cfg_untagged_vidx] <= cfg_untagged_bit;
end
end
assign cfg = cfgs[rd_port];
assign vlan_is_untagged_here = untagged[rd_port][rd_vidx];
// Cost. Section 10 compares these against Chapter 13.1's 60 KiB.
assign bits_pvid = CNT_W'(N_PORTS) * CNT_W'(VID_W);
assign bits_untagged_sets = CNT_W'(N_PORTS) * CNT_W'(N_VLANS);
assign bits_total = bits_pvid + bits_untagged_sets +
(CNT_W'(N_PORTS) * CNT_W'(3));
// "Access-like" and "trunk-like" are DERIVED, not configured: a port
// whose untagged set has exactly one bit behaves as an access port, and
// one with several tagged VLANs behaves as a trunk. Reporting the
// derived shape catches a port configured as neither.
always_comb begin
n_access_like = 6'd0;
n_trunk_like = 6'd0;
for (int p = 0; p < N_PORTS; p++) begin
if ($countones(untagged[p]) == 1) n_access_like = n_access_like + 6'd1;
else if ($countones(untagged[p]) == 0) n_trunk_like = n_trunk_like + 6'd1;
end
end
endmoduleClassification: synthesizable.
What it teaches: that "access port" and "trunk port" are not modes in the hardware. They are names for particular settings of the five fields — an access port is a port whose untagged set contains exactly one VLAN and whose PVID is that VLAN; a trunk is one whose untagged set is empty and which is a member of many. A design that implements them as an enumerated mode has hard-coded two points of a continuous space, and the third case — a hybrid port carrying one VLAN untagged and several tagged, which is how every IP phone with a PC behind it is connected — becomes impossible to express.
And it teaches that the untagged set is separate from Chapter 13.1's membership array, because the two questions are independent. Membership asks may this VLAN's traffic use this port. The untagged set asks how is it represented when it does. A port can be a member of VLAN 10 and carry it tagged, or a member and carry it untagged — and a design that merges the arrays has made every member VLAN untagged, which on a trunk destroys every VID it forwards.
Deliberately simplified: a dense untagged vector over 64 configured VLANs. Chapter 13.1 §10 established that the full VID range makes these arrays 12 KiB each and that production designs put a VID-to-index map in front — Chapter 13.4 owns that map, and this module consumes its output as rd_vidx.
Production implication: n_access_like and n_trunk_like are derived rather than configured, and the gap between them and the port count is the interesting number. A port that is neither — an empty untagged set and membership of exactly one VLAN, or several untagged VLANs — is almost always a misconfiguration, and it is one that forwards traffic normally. Several untagged VLANs on one port is the worst case: every one of them arrives at the far end untagged and indistinguishable, so the far end assigns them all its own PVID and several VLANs have silently merged into one.
4. The Six Ingress Cases
Two things a frame can be, three things a port will accept. Six combinations, and four of them are the ones deployments actually meet.
| Frame arrives | AFT_ADMIT_ALL | AFT_TAGGED_ONLY | AFT_UNTAGGED_ONLY |
|---|---|---|---|
| untagged | assign PVID | refuse — VR_WRONG_TYPE | assign PVID |
| tagged, VID 1–4094 | use the carried VID | use the carried VID | refuse |
| tagged, VID 0 | assign PVID, keep PCP | assign PVID, keep PCP | refuse — it is tagged |
The bottom row is the case Chapter 13.2 §4 set up and could not finish. A frame with VID 0 is tagged — a TPID at octet 12, four octets that moved everything after them, and a PCP worth honouring — but it carries no VLAN assignment. So the port assigns its PVID, exactly as it would for an untagged frame, and keeps the priority the sender asked for.
That is the entire purpose of VID 0: it lets a station express a priority without expressing a VLAN, and it is why Chapter 13.2's c_vid_zero is a configuration signal rather than an error.
And the right-hand column is the one that surprises people. AFT_UNTAGGED_ONLY refuses a VID-0 frame because the test is on the frame's form, not its content — it arrived with a tag, so a port that accepts only untagged frames refuses it, priority-only or not.
Which is a distinction worth being precise about, because two plausible readings of "untagged" diverge here:
| "carries no tag" | "carries no VLAN assignment" | |
|---|---|---|
| a plain frame | yes | yes |
| a VID-0 frame | no | yes |
| the acceptable-frame-types test uses | this one | not this one |
| the PVID assignment rule uses | not this one | this one |
Two rules in the same module, using two different senses of the same word. Section 5 tests the form; Section 7 tests the content.
5. RTL 2 — Acceptable Frame Types
The first gate, and the only one in this chapter that a station can trip by doing something legal.
// -----------------------------------------------------------------------
// acceptable_frame_filter -- does this port accept a frame of this FORM?
//
// The test is on the frame's form -- did it arrive with a tag -- and NOT
// on whether it carries a VLAN assignment. A VID-0 frame is tagged and is
// refused by an untagged-only port, even though it assigns no VLAN.
// Section 4's table has the two senses side by side.
// -----------------------------------------------------------------------
module acceptable_frame_filter
import vlanport_pkg::*;
#(
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_valid,
input logic arrived_tagged, // Chapter 13.2's tag_count != 0
input logic [VID_W-1:0] carried_vid,
input accept_types_e accept,
output logic admit,
output vlan_reject_e reject,
output logic [CNT_W-1:0] c_admitted,
output logic [CNT_W-1:0] c_refused_type,
output logic [CNT_W-1:0] c_vid_zero_admitted,
output logic [CNT_W-1:0] c_reserved_refused
);
logic vid_reserved;
// Chapter 13.2 Section 4: 4095 is reserved and names no VLAN. Unlike 0,
// it has no defined alternative meaning, so it is refused outright.
assign vid_reserved = arrived_tagged && (carried_vid == 12'd4095);
always_comb begin
admit = 1'b0;
reject = VR_OK;
if (frame_valid) begin
if (vid_reserved) begin
reject = VR_RESERVED_VID;
end else begin
unique case (accept)
AFT_ADMIT_ALL: admit = 1'b1;
// THE FORM TEST. A VID-0 frame is TAGGED and passes here, even
// though Section 7 will assign it the PVID as though it were
// untagged.
AFT_TAGGED_ONLY: begin
admit = arrived_tagged;
if (!arrived_tagged) reject = VR_WRONG_TYPE;
end
AFT_UNTAGGED_ONLY: begin
admit = !arrived_tagged;
if (arrived_tagged) reject = VR_WRONG_TYPE;
end
default: admit = 1'b1;
endcase
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_admitted <= '0;
c_refused_type <= '0;
c_vid_zero_admitted <= '0;
c_reserved_refused <= '0;
end else if (frame_valid) begin
if (admit) begin
c_admitted <= c_admitted + 1'b1;
// A priority-only frame that was accepted. Chapter 13.2's
// c_vid_zero counted them at the parser; this counts the ones
// this port's policy let through.
if (arrived_tagged && (carried_vid == 12'd0))
if (!(&c_vid_zero_admitted))
c_vid_zero_admitted <= c_vid_zero_admitted + 1'b1;
end else begin
if (reject == VR_WRONG_TYPE)
if (!(&c_refused_type)) c_refused_type <= c_refused_type + 1'b1;
if (reject == VR_RESERVED_VID)
if (!(&c_reserved_refused))
c_reserved_refused <= c_reserved_refused + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the form test and the assignment test use two different senses of "untagged", in adjacent modules, and conflating them is a real bug. A VID-0 frame arrived with a tag — so AFT_UNTAGGED_ONLY refuses it — and carries no VLAN assignment — so Section 7 gives it the PVID. Both are correct and they disagree about the word.
And it teaches why 4095 is refused where 0 is not. Chapter 13.2 §4 established both as reserved, but 0 has a defined alternative meaning — priority-only — and 4095 has none. A frame carrying 4095 is malformed rather than special, and there is no assignment rule that could rescue it.
Deliberately simplified: a combinational decision on a single arrival. Production designs fold this into the same pipeline stage as Chapter 13.2's parse, because arrived_tagged is that parser's output and the two decisions have the same input.
Production implication: c_refused_type on an access port is the counter that catches the misconfiguration Chapter 13.2 §11's tagged_fraction_pct predicted. A port set to AFT_UNTAGGED_ONLY and receiving tagged frames refuses every one of them — which is loud, correct, and far better than the alternative: a port set to AFT_ADMIT_ALL receiving the same frames accepts them and uses their carried VID, placing a station's traffic in whatever VLAN it asked for. The permissive setting is the dangerous one, and that is not the intuition most people bring to an "accept everything" option.
6. The Assignment Rule
One rule, three inputs, and the only place in a switch where a frame acquires an attribute it did not arrive with.
If the frame carries a usable VID — tagged, 1 to 4094 — that is its VLAN. Otherwise the port's PVID is its VLAN.
"Otherwise" covers two cases that look different and behave identically: a plain untagged frame, and a tagged frame carrying VID 0.
| Arrival | VLAN assigned | PCP taken from |
|---|---|---|
| untagged | the port's PVID | the port's default priority |
| tagged, VID 0 | the port's PVID | the frame's PCP |
| tagged, VID 1–4094 | the frame's VID | the frame's PCP |
The middle row is the only one where the two halves of the tag are used differently, and it is worth being explicit about why. Chapter 13.2 §15 established that PCP and VID are independent fields with unrelated consumers. A VID-0 frame exercises that independence directly: its priority is honoured and its VLAN is discarded, because the sender expressed one and not the other.
And the top row's right-hand cell is a configuration item this chapter has not mentioned yet. A port assigning a PVID must also assign a priority, because the frame has none — and the usual default is 0, which is why Chapter 13.2 §4's c_by_pcp histogram on a network of untagged hosts is entirely in bucket 0.
7. RTL 3 — Assigning the VLAN
Four lines of logic that decide which VLAN a frame is in for the rest of its life inside this switch.
// -----------------------------------------------------------------------
// ingress_vid_assigner -- the one place a frame acquires an attribute it
// did not arrive with.
//
// After this module the frame's VLAN is fixed for the remainder of its
// path through the switch. Chapter 13.1's P6 asserted the VID does not
// change while crossing a switch; this module is where the value that
// property protects is established.
// -----------------------------------------------------------------------
module ingress_vid_assigner
import vlanport_pkg::*;
#(
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic admit,
input logic arrived_tagged,
input logic [VID_W-1:0] carried_vid,
input logic [PCP_W-1:0] carried_pcp,
input logic [VID_W-1:0] port_pvid,
input logic [PCP_W-1:0] port_default_pcp,
output logic assign_valid,
output logic [VID_W-1:0] assigned_vid,
output logic [PCP_W-1:0] assigned_pcp,
output logic vid_was_carried, // provenance -- Section 16
output logic vid_was_invented,
output logic [CNT_W-1:0] c_carried,
output logic [CNT_W-1:0] c_invented,
output logic [CNT_W-1:0] c_priority_only
);
// "Usable" is Chapter 13.2 Section 4's test: tagged, and not 0 or 4095.
// A VID-0 frame is tagged and NOT usable, so it falls to the PVID --
// which is the same outcome as an untagged frame by a different route.
logic vid_usable;
assign vid_usable = arrived_tagged &&
(carried_vid != 12'd0) && (carried_vid != 12'd4095);
assign assign_valid = admit;
assign assigned_vid = vid_usable ? carried_vid : port_pvid;
// THE ASYMMETRY. The VID falls back to the port; the PCP does not. A
// tagged frame's priority is honoured even when its VLAN is discarded,
// because Chapter 13.2 Section 15 established the two fields are
// independent and have unrelated consumers.
assign assigned_pcp = arrived_tagged ? carried_pcp : port_default_pcp;
// PROVENANCE. Section 16 needs to know whether this VLAN came off the
// wire or out of a register, because a mismatch is only possible in the
// second case.
assign vid_was_carried = admit && vid_usable;
assign vid_was_invented = admit && !vid_usable;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_carried <= '0;
c_invented <= '0;
c_priority_only <= '0;
end else if (admit) begin
if (vid_usable) begin
if (!(&c_carried)) c_carried <= c_carried + 1'b1;
end else begin
if (!(&c_invented)) c_invented <= c_invented + 1'b1;
if (arrived_tagged)
if (!(&c_priority_only)) c_priority_only <= c_priority_only + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the VID falls back to the port and the PCP does not, and the asymmetry is deliberate rather than an oversight. A tagged frame carrying VID 0 has a sender who expressed a priority and declined to express a VLAN — so the design honours the half that was expressed and supplies the half that was not. A design that discards the whole tag on VID 0 has thrown away the only thing the sender actually asked for.
And vid_was_carried against vid_was_invented is the provenance distinction Section 16 depends on. A VLAN that came off the wire was chosen by the transmitting switch and cannot be mismatched — both ends read the same 12 bits. A VLAN that was invented came out of a local register, and whether it matches the far end's intent is a question about two configurations that nothing in the frame can answer.
Deliberately simplified: a single default priority per port. Production designs support a priority regeneration table that remaps arriving PCP values on ingress, because a switch at an administrative boundary cannot trust a neighbour's priority marking any more than Chapter 12.2 §16 could trust its source addresses.
Production implication: c_invented against c_carried is the same measurement Chapter 13.2 §11's tagged_fraction_pct made from the other side, and the pair is more useful than either. On an access port every frame should be invented; on a trunk almost none should be. A trunk with a high c_invented is receiving untagged frames, which Section 14's native-VLAN rule is about to place somewhere by default — and that number is the earliest warning of the failure this chapter exists to explain.
8. Ingress Filtering, and Why It Is Optional
A frame has been admitted and assigned a VLAN. One question remains before it enters the forwarding path: is this port actually a member of that VLAN?
The surprising part is that checking is optional, and that switches ship with it disabled.
| ingress filtering on | ingress filtering off | |
|---|---|---|
| tagged frame, VID 10, port not in VLAN 10 | discarded | accepted into VLAN 10 |
| what the frame can then reach | nothing | every port that is in VLAN 10 |
| Chapter 13.1 §12's isolation | intact | intact — the egress is a member |
| what an operator sees | a counter | nothing |
Read the third row. Chapter 13.1's isolation invariant is about the egress port — no frame leaves a port that is not a member of the frame's VLAN. A frame injected into VLAN 10 through a non-member port satisfies it perfectly, because every port it then reaches is a member.
So ingress filtering is the check that closes the other half of the boundary, and its absence is not an isolation failure by Chapter 13.1's definition. It is an admission failure, and the two are different enough that a switch can be provably isolated and still let anybody into any VLAN.
Why it is ever off: because the check costs a membership lookup on the ingress path, and because a switch in the middle of a trunk-only topology is a member of everything anyway. Neither reason survives contact with a port facing a station.
9. RTL 4 — The Membership Check on the Way In
One comparator, and the counter that matters is the one it keeps when the check is disabled.
// -----------------------------------------------------------------------
// ingress_vlan_filter -- is this port a member of the VLAN the frame was
// just assigned?
//
// Chapter 13.1 Section 12's isolation invariant is about the EGRESS. This
// is the other half of the boundary, and a switch can satisfy that
// invariant perfectly while letting any station into any VLAN.
// -----------------------------------------------------------------------
module ingress_vlan_filter
import vlanport_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int N_VLANS = 64,
parameter int VIDX_W = 6,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic assign_valid,
input logic [VIDX_W-1:0] assigned_vidx,
input logic [4:0] ingress_port,
input logic vid_was_carried,
input logic [N_PORTS-1:0] vlan_members [N_VLANS],
input logic filter_enable,
output logic pass,
output vlan_reject_e reject,
output logic [CNT_W-1:0] c_filtered,
output logic [CNT_W-1:0] c_would_have_filtered, // with the check OFF
output logic [4:0] worst_port,
output logic admission_gap_open
);
logic is_member;
assign is_member = vlan_members[assigned_vidx][ingress_port];
always_comb begin
pass = 1'b0;
reject = VR_OK;
if (assign_valid) begin
if (filter_enable && !is_member) reject = VR_INGRESS_FILTER;
else pass = 1'b1;
end
end
logic [CNT_W-1:0] by_port [N_PORTS];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_filtered <= '0;
c_would_have_filtered <= '0;
for (int p = 0; p < N_PORTS; p++) by_port[p] <= '0;
worst_port <= '0;
admission_gap_open <= 1'b0;
end else if (assign_valid && !is_member) begin
if (filter_enable) begin
if (!(&c_filtered)) c_filtered <= c_filtered + 1'b1;
end else begin
// THE COUNTER THAT MATTERS. With the check disabled, this counts
// frames that WOULD have been filtered -- frames admitted into a
// VLAN through a port that is not a member of it. It costs one
// comparator and it is the only evidence the gap is being used.
if (!(&c_would_have_filtered))
c_would_have_filtered <= c_would_have_filtered + 1'b1;
by_port[ingress_port] <= by_port[ingress_port] + 1'b1;
worst_port <= ingress_port;
// A frame ENTERED a VLAN it had no business entering, and
// Chapter 13.1's isolation monitor will not see it, because every
// port it reaches IS a member.
admission_gap_open <= vid_was_carried;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that c_would_have_filtered costs one comparator and is the only evidence the gap is being used. A design that disables ingress filtering by omitting the check has no way to know whether it matters. A design that performs the comparison and acts on it only when enabled knows exactly how many frames entered a VLAN through a non-member port — and can turn the check on with an informed idea of what will break.
And admission_gap_open narrows it further, using Section 7's provenance. A frame whose VID was invented cannot violate membership: the PVID is a property of the port, and a sensibly configured port is a member of its own PVID. A frame whose VID was carried is the dangerous one — the sender chose the VLAN, and with filtering off the switch honoured that choice from a port that has no membership in it.
Deliberately simplified: a single membership lookup on the ingress path. The lookup is the same array Chapter 13.1 §9 built, read at a second point — which is one more read port on a structure that is already read once per frame at the egress, and a real reason designs sometimes skip it.
Production implication: the setting to worry about is not the check but the acceptable-frame-types setting behind it. A port set to AFT_ADMIT_ALL with ingress filtering off will place a station's traffic in any VLAN that station asks for, by tagging its own frames — and both settings are common defaults. Section 5's counter and this one together identify every port where that combination is actually being exercised, which is a much shorter list than the ports where it is configured.
10. Egress Is Not the Mirror of Ingress
Ingress does one thing: it assigns. Egress does three, and which one depends on a per-port, per-VLAN bit rather than on anything about the frame.
| The VLAN is… | Egress action | Frame length |
|---|---|---|
| in this port's untagged set | strip the tag if present | −4 if it had one |
| in this port's tagged set — a member, not untagged | insert a tag if absent | +4 if it had none |
| not a member at all | drop — Chapter 13.1 §12 | — |
And there is a fourth case that looks like a fourth action and is not: the frame already has the right form. A tagged frame leaving a tagged port, or an untagged frame leaving an untagged port, needs no transformation at all — and recognising that is worth doing, because Section 13 shows every transformation costs an FCS recomputation.
The asymmetry is worth stating plainly, because "strip on the way in, tag on the way out" is a common and wrong mental model:
| ingress | egress | |
|---|---|---|
| how many outcomes | assign, from two sources | strip, insert, pass or drop |
| what decides | the frame's form and the port's PVID | the port's untagged set |
| the frame's VLAN | is established here | is represented here |
| can it change the VLAN | yes — that is its job | no — Chapter 13.1's P6 |
| changes the frame's length | no | yes, by ±4 |
Read the last two rows together. Ingress changes the frame's meaning and not its bytes; egress changes its bytes and must not change its meaning. A design that alters the VID at egress has done Chapter 13.1 §12's v_vid_changed — relabelling, which that chapter established is worse than leaking.
11. RTL 5 — The Egress Transformation
Three actions plus a no-op, chosen by one bit, and every one of the three changes the frame.
// -----------------------------------------------------------------------
// egress_tag_transformer -- strip, insert, pass or drop.
//
// The action is chosen by a per-port per-VLAN bit and NOT by anything
// about the frame. Recognising the no-op case matters: Section 13 shows
// every real transformation costs an FCS recomputation, so a design that
// always strips-then-inserts pays that cost on every frame for nothing.
// -----------------------------------------------------------------------
module egress_tag_transformer
import vlanport_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int N_VLANS = 64,
parameter int VIDX_W = 6,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic eg_valid,
input logic [4:0] egress_port,
input logic [VIDX_W-1:0] vidx,
input logic [VID_W-1:0] vid,
input logic [PCP_W-1:0] pcp,
input logic frame_is_tagged, // as it sits in the fabric
input logic [13:0] frame_len,
input logic [N_PORTS-1:0] vlan_members [N_VLANS],
input logic vlan_untagged_here,
output logic act_valid,
output egress_action_e action,
output logic [13:0] new_len,
output logic [15:0] insert_tci,
output logic fcs_recompute,
output vlan_reject_e reject,
output logic [CNT_W-1:0] c_strip,
output logic [CNT_W-1:0] c_insert,
output logic [CNT_W-1:0] c_pass,
output logic [CNT_W-1:0] c_drop_nonmember
);
logic is_member;
assign is_member = vlan_members[vidx][egress_port];
always_comb begin
action = EG_DROP;
new_len = frame_len;
fcs_recompute = 1'b0;
reject = VR_OK;
act_valid = 1'b0;
if (eg_valid) begin
act_valid = 1'b1;
// Chapter 13.1 Section 12's isolation invariant, applied here. A
// non-member port never emits the frame, whatever its untagged set
// happens to say about a VLAN it does not carry.
if (!is_member) begin
action = EG_DROP;
reject = VR_EGRESS_NOT_MEM;
end else if (vlan_untagged_here) begin
if (frame_is_tagged) begin
action = EG_STRIP;
new_len = frame_len - 14'd4;
fcs_recompute = 1'b1;
end else begin
// THE NO-OP. Already in the right form. Section 13 explains
// why recognising this is worth the comparator.
action = EG_PASS;
end
end else begin
if (!frame_is_tagged) begin
action = EG_TAG;
new_len = frame_len + 14'd4;
fcs_recompute = 1'b1;
end else begin
action = EG_PASS;
end
end
end
end
// The tag this port would insert. Chapter 13.2 Section 4's TCI split,
// rebuilt from the frame's assigned VLAN and priority -- note that the
// DEI bit is not preserved across a strip-and-reinsert, which is a
// small honest loss worth knowing about.
assign insert_tci = {pcp, 1'b0, vid};
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_strip <= '0; c_insert <= '0; c_pass <= '0; c_drop_nonmember <= '0;
end else if (eg_valid) begin
unique case (action)
EG_STRIP: if (!(&c_strip)) c_strip <= c_strip + 1'b1;
EG_TAG: if (!(&c_insert)) c_insert <= c_insert + 1'b1;
EG_PASS: if (!(&c_pass)) c_pass <= c_pass + 1'b1;
EG_DROP: if (!(&c_drop_nonmember))
c_drop_nonmember <= c_drop_nonmember + 1'b1;
default: ;
endcase
end
end
endmoduleClassification: synthesizable.
What it teaches: that the no-op is worth recognising explicitly. A tagged frame leaving a tagged port and an untagged frame leaving an untagged port both need nothing done to them — and a design that unconditionally strips on ingress and inserts on egress pays an FCS recomputation and a realignment on every frame to arrive at the bytes it already had. Section 13 prices that.
And it teaches a small honest loss: the DEI bit does not survive a strip-and-reinsert. The frame's tag is destroyed at one hop and rebuilt at another from the VLAN and priority the switch carried internally — and unless the descriptor carries DEI too, the rebuilt tag has it clear. That is a real, standards-visible difference between a frame that passed through and one that was retagged, and it is the kind of thing that shows up only when somebody is relying on the bit.
Deliberately simplified: the membership array read at the egress, matching Chapter 13.1 §9. The insert_tci is built combinationally; a real design carries the whole TCI in the descriptor so that DEI and any provider-specific bits survive.
Production implication: c_strip, c_insert and c_pass in proportion describe what a port is actually doing, and the healthy shapes are distinctive. An access port should be almost entirely c_strip and c_pass. A trunk should be almost entirely c_insert and c_pass. A port with a substantial mixture of all three is a hybrid — which is legitimate for a phone-and-PC port and a misconfiguration everywhere else, and no other counter in the switch distinguishes those two cases.
12. Stripping a Tag Can Make a Runt
The transformation changes the frame's length by four octets, and at one end of the size range that crosses a boundary Chapter 5.6 established twenty years before VLANs existed.
A minimum-length tagged frame is 64 octets on the wire:
DA 6 + SA 6 + TAG 4 + EtherType 2 + payload/pad 42 + FCS 4 = 64
Strip the tag and it becomes:
DA 6 + SA 6 + EtherType 2 + payload/pad 42 + FCS 4 = 60 octets — a runt.
Chapter 7.3 requires every frame to be at least 64 octets, so the stripped frame is illegal and every receiver will discard it. The egress must re-pad, adding four octets and taking the payload floor back from 42 to 46 — which is exactly Chapter 13.2 §6's arithmetic run backwards.
The full table, and it is worth having in one place:
| Arrives | Strip → | Insert → | Pass → |
|---|---|---|---|
| 64 | 60 — runt, re-pad to 64 | 68 | 64 |
| 68 | 64 | 72 | 68 |
| 1518 | 1514 | 1522 — legal only because tagged max is 1522 | 1518 |
| 1522 | 1518 | 1526 — legal only as a double tag | 1522 |
Both ends of the range have a trap and they are different traps.
At the bottom, stripping produces an illegal frame that must be repaired. A design that strips and forwards without re-padding emits runts that every downstream device discards — and Chapter 13.2 §13's split means the addresses are intact, so the frame looks structurally fine right up to the length check.
At the top, inserting produces a frame that is legal only if the length checker knows about tags. Chapter 13.2 §6 established that a switch checking against a fixed 1518 discards legal 1522-octet frames — and here the switch is the one that made them 1522. A design that inserts a tag and then applies an untagged length check rejects its own output.
13. RTL 6 — Repairing the Length
Four octets of padding, one FCS recomputation, and a check that the transformation did not produce something illegal at either end.
// -----------------------------------------------------------------------
// egress_length_repair -- makes the transformed frame legal again.
//
// A strip can produce a runt; an insert can produce an oversize. Both are
// the switch's own doing, and both must be detected against the limits
// that apply AFTER the transformation rather than before it.
// -----------------------------------------------------------------------
module egress_length_repair
import vlanport_pkg::*;
#(
parameter int MIN_FRAME = 64,
parameter int MAX_UNTAG = 1518,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic act_valid,
input egress_action_e action,
input logic [13:0] new_len,
input logic [2:0] tags_after, // tag count AFTER the action
output logic [13:0] final_len,
output logic [13:0] pad_octets,
output logic need_pad,
output logic fcs_recompute,
output logic length_ok,
output vlan_reject_e reject,
output logic [CNT_W-1:0] c_repadded,
output logic [CNT_W-1:0] c_oversize_self_inflicted,
output logic [13:0] max_after
);
// THE LIMIT THAT APPLIES IS THE ONE FOR THE TAG COUNT AFTER THE
// TRANSFORMATION. Chapter 13.2 Section 6: 1518 untagged, 1522 with one
// tag, 1526 with two. A design that checks the pre-transformation limit
// rejects its own output.
assign max_after = 14'(MAX_UNTAG) + (14'(tags_after) * 14'd4);
// A strip can take a 64-octet frame to 60. Chapter 7.3 requires 64, so
// the four octets come back as padding -- and Chapter 5.6 established
// that padding beyond the payload's own declared length is invisible to
// the layer above.
assign need_pad = act_valid && (new_len < 14'(MIN_FRAME));
assign pad_octets = need_pad ? (14'(MIN_FRAME) - new_len) : 14'd0;
assign final_len = need_pad ? 14'(MIN_FRAME) : new_len;
// Every transformation changes the frame, so the check sequence over it
// is no longer valid. Chapter 5.8's CRC-32 is already in the datapath.
assign fcs_recompute = act_valid &&
((action == EG_STRIP) || (action == EG_TAG) || need_pad);
always_comb begin
length_ok = 1'b1;
reject = VR_OK;
if (act_valid && (final_len > max_after)) begin
length_ok = 1'b0;
reject = VR_LENGTH;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_repadded <= '0;
c_oversize_self_inflicted <= '0;
end else if (act_valid) begin
if (need_pad)
if (!(&c_repadded)) c_repadded <= c_repadded + 1'b1;
// An oversize the switch CREATED by inserting a tag. Distinct from
// an oversize that arrived, because the remedy is different: this
// one means the far end's limit and ours disagree about tagging.
if (!length_ok && (action == EG_TAG))
if (!(&c_oversize_self_inflicted))
c_oversize_self_inflicted <= c_oversize_self_inflicted + 1'b1;
end
end
endmoduleClassification: synthesizable.
What it teaches: that the length limit that applies is the one for the tag count after the transformation, and getting that wrong is a design error the switch inflicts on itself. A frame that arrived at 1518 octets untagged, checked correctly against 1518, becomes 1522 when this port inserts a tag — and a checker still holding 1518 rejects it. The switch has created a frame and then refused to emit it.
And c_oversize_self_inflicted is deliberately separate from an ordinary oversize count, because the two mean different things. An oversize that arrived means a neighbour is sending frames longer than this port accepts. An oversize this port created means our tagging and the far end's limits disagree — and the remedy is a configuration conversation, not a filter.
Deliberately simplified: padding as a length adjustment rather than an octet-level insertion. A real design writes the padding octets and must decide what to write; zeros are conventional and the standard does not require any particular value, which is a small and genuine source of frames that differ byte-for-byte between vendors.
Production implication: c_repadded should be non-zero on any access port carrying small frames, and its absence is the finding. A port stripping tags from minimum-length frames and never re-padding is emitting 60-octet runts — which every downstream receiver discards under Chapter 7.3, and which appears at those receivers as an error counter with no local cause. Chapter 12.6 §9's relocated-evidence problem, produced by a transformation rather than by a cable.
14. The Native VLAN
A trunk carries many VLANs tagged. It may also carry exactly one untagged — the native VLAN — and that single exception is the largest silent failure surface in VLAN deployment.
Mechanically it is nothing new. The native VLAN is simply the trunk port's PVID, and the trunk's untagged set contains exactly that one VLAN. Section 7's assignment rule and Section 11's egress transformation handle it without a special case.
Operationally it is a hole in Section 2's diagram. Every other VLAN on the trunk travels tagged, so its identity is carried on the wire and the two switches read the same 12 bits. The native VLAN travels untagged, so its identity is destroyed at transmit and re-invented at receive — from a number the receiving switch's operator configured independently.
Which produces the failure. Two switches, trunk between them:
| switch A | switch B | |
|---|---|---|
| trunk native VLAN | 1 | 99 |
| A sends VLAN 1 | untagged — no VID on the wire | assigns PVID 99 |
| the frame is now in | VLAN 1 | VLAN 99 |
| B sends VLAN 99 | untagged | assigns PVID 1 |
| the frame is now in | VLAN 1 | VLAN 99 |
The leak is bidirectional, complete, and invisible to every check either switch performs.
Chapter 13.1 §12's isolation monitor is clean at both ends. Switch A emitted a VLAN 1 frame out a port that is a member of VLAN 1 — correct. Switch B emitted a VLAN 99 frame out a port that is a member of VLAN 99 — also correct. No frame left a non-member port. No frame's VID changed while crossing a switch, because inside each switch the VID never changed; it changed between them, where neither switch was looking.
And there is no counter for it, because there is nothing anomalous to count. Both switches received an untagged frame on a trunk — which is exactly what a native VLAN is — and assigned their PVID, which is exactly the rule.
15. RTL 7 — Detecting a Mismatch From Local Evidence
Neither switch can see the other's PVID. What each one can see is a pattern that a mismatch produces and a match does not.
// -----------------------------------------------------------------------
// native_vlan_detector -- infers a native-VLAN mismatch from evidence
// available at one end.
//
// The far end's PVID is not observable. What IS observable: which
// addresses appear in which VLAN, and whether an address seen tagged in
// VLAN X on this trunk is also seen untagged. A mismatch makes those two
// populations disagree in a way a match never does.
// -----------------------------------------------------------------------
module native_vlan_detector
import vlanport_pkg::*;
#(
parameter int N_TRACK = 16,
parameter int ADDR_W = 48,
parameter int VIDX_W = 6,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic learn_valid,
input logic [ADDR_W-1:0] learn_mac,
input logic [VIDX_W-1:0] learn_vidx,
input logic [4:0] learn_port,
input logic arrived_tagged,
input logic port_is_trunk,
input logic [VIDX_W-1:0] port_native_vidx,
output logic [CNT_W-1:0] c_untagged_on_trunk,
output logic [CNT_W-1:0] c_addr_in_two_vlans_one_trunk,
output logic [ADDR_W-1:0] suspect_mac,
output logic [VIDX_W-1:0] suspect_vidx_a,
output logic [VIDX_W-1:0] suspect_vidx_b,
output logic native_mismatch_suspected,
output logic [4:0] suspect_port
);
logic [ADDR_W-1:0] t_mac [N_TRACK];
logic [VIDX_W-1:0] t_vidx [N_TRACK];
logic t_tag [N_TRACK];
logic t_val [N_TRACK];
logic [$clog2(N_TRACK)-1:0] wr_ptr;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < N_TRACK; i++) begin
t_mac[i] <= '0; t_vidx[i] <= '0; t_tag[i] <= 1'b0; t_val[i] <= 1'b0;
end
wr_ptr <= '0;
c_untagged_on_trunk <= '0;
c_addr_in_two_vlans_one_trunk <= '0;
suspect_mac <= '0;
suspect_vidx_a <= '0;
suspect_vidx_b <= '0;
suspect_port <= '0;
native_mismatch_suspected <= 1'b0;
end else if (learn_valid && port_is_trunk) begin
// Untagged arrivals on a trunk are the native VLAN's traffic. They
// are NORMAL -- this counter is a denominator, not an alarm.
if (!arrived_tagged)
if (!(&c_untagged_on_trunk))
c_untagged_on_trunk <= c_untagged_on_trunk + 1'b1;
begin
automatic int hit = -1;
for (int i = 0; i < N_TRACK; i++)
if (t_val[i] && (t_mac[i] == learn_mac)) hit = i;
if (hit >= 0) begin
// THE SIGNATURE. One address, seen on ONE trunk port, in TWO
// different VLANs, where at least one sighting was untagged.
// With matching natives this cannot happen: the untagged
// traffic is always the same VLAN at both ends. With a
// mismatch, the far end's native traffic arrives untagged and
// lands in OUR native VLAN, while that same station's tagged
// traffic lands in its real one.
if ((t_vidx[hit] != learn_vidx) &&
(!t_tag[hit] || !arrived_tagged)) begin
if (!(&c_addr_in_two_vlans_one_trunk))
c_addr_in_two_vlans_one_trunk <= c_addr_in_two_vlans_one_trunk + 1'b1;
suspect_mac <= learn_mac;
suspect_vidx_a <= t_vidx[hit];
suspect_vidx_b <= learn_vidx;
suspect_port <= learn_port;
// Chapter 13.1 Section 14 established that one address in two
// VLANs is NORMAL -- a router with several legs. What is not
// normal is that pattern arriving through ONE trunk port with
// an untagged sighting among them.
native_mismatch_suspected <=
(learn_vidx == port_native_vidx) ||
(t_vidx[hit] == port_native_vidx);
end
t_vidx[hit] <= learn_vidx;
t_tag[hit] <= arrived_tagged;
end else begin
t_mac[wr_ptr] <= learn_mac;
t_vidx[wr_ptr] <= learn_vidx;
t_tag[wr_ptr] <= arrived_tagged;
t_val[wr_ptr] <= 1'b1;
wr_ptr <= wr_ptr + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable, sized as a small tracking window rather than a full table.
What it teaches: that a mismatch is detectable from one end even though the far end's configuration is not observable, and the evidence is a pattern rather than a value. With matching natives, the untagged traffic on a trunk is always the same VLAN at both ends — one address's untagged frames and its tagged frames land in different VLANs only if it genuinely has legs in both. With a mismatch, the far end's native traffic arrives untagged and is placed in our native VLAN, while that same station's tagged traffic lands in its real one — so one address appears in two VLANs through one trunk port, with an untagged sighting among them.
And it teaches why Chapter 13.1 §14's tracker is not sufficient on its own. That module established that one address in two VLANs is normal — a router with several legs — and this one adds the two qualifiers that make it suspicious: through a single trunk port, and with at least one sighting untagged. A router's legs arrive on different ports or arrive tagged; neither qualifier fits.
Deliberately simplified: a 16-entry window and a single-bit tagged/untagged history. A production detector samples during Chapter 12.5 §13's ageing sweep and keeps a per-address tagged-sighting bitmap, so the pattern can be established across the whole table rather than a recent sample.
Production implication: c_untagged_on_trunk is a denominator, not an alarm — untagged frames on a trunk are the native VLAN working as designed, and a switch that flags them is flagging normal operation. native_mismatch_suspected is the alarm, and it is deliberately named as a suspicion: the detector cannot prove a mismatch, because proving it requires the far end's PVID. What it can do is point at one address, two VLANs and one port — which is enough for an operator to compare two configurations and settle it in a minute.
16. What a Mismatch Actually Does
"Traffic leaks between VLANs" understates it. Work out precisely what is joined to what, because the answer is not symmetric and it is not a leak in the usual sense.
Switch A's native is VLAN 1; switch B's is VLAN 99.
| Traffic | Leaves A as | Arrives at B as | Net effect |
|---|---|---|---|
| A's VLAN 1 | untagged | VLAN 99 | A's VLAN 1 is joined to B's VLAN 99 |
| B's VLAN 99 | untagged | VLAN 1 | the same join, from the other side |
| A's VLAN 10 | tagged, VID 10 | VLAN 10 | correct |
| B's VLAN 10 | tagged, VID 10 | VLAN 10 | correct |
| A's VLAN 99 | tagged, VID 99 | VLAN 99 | correct — and it now shares B's VLAN 99 with A's VLAN 1 |
The last row is the one that makes this worse than a simple swap. A's VLAN 99 traffic travels tagged — A is a member of VLAN 99 and it is not A's native — so it arrives correctly in B's VLAN 99. And A's VLAN 1 traffic arrives there too.
So B's VLAN 99 now contains two of A's VLANs. Two broadcast domains that A keeps separate have been merged at B, and every one of Chapter 12.4 §10's arithmetic arguments now applies to their union.
What each side observes:
| Observation | Switch A | Switch B |
|---|---|---|
| Chapter 13.1 §12 isolation | clean | clean |
v_vid_changed | zero — the VID never changed inside A | zero |
| ingress filtering violations | none — the trunk is a member of its native | none |
| a length or format error | none — the frames are perfectly formed | none |
| Chapter 12.2 §9's move detector | quiet | quiet |
| the only signal | — | Section 15's pattern |
Every mechanism this track has built reports success, and it is not that they are failing to look — there is nothing anomalous to see. Each switch received a well-formed untagged frame on a trunk and applied its configured rule.
17. RTL 8 — Conformance for a Transformation
The monitor's difficulty here is that Chapter 12.1's P11 — a frame is never modified — is false by design in this chapter. The check must be the narrower invariant Section 12's callout stated.
// -----------------------------------------------------------------------
// portmode_conformance_monitor -- checks a datapath whose job is to
// modify frames.
//
// Chapter 12.1's P11 asserted a frame is never modified in transit. This
// chapter strips, inserts, re-pads and recomputes the FCS. So the
// invariant is restated: the addresses, the EtherType, the payload up to
// its declared length, and the VLAN ASSIGNMENT are unmodified, and
// everything else is the switch's to change.
// -----------------------------------------------------------------------
module portmode_conformance_monitor
import vlanport_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int N_VLANS = 64,
parameter int VIDX_W = 6,
parameter int ADDR_W = 48,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic [N_PORTS-1:0] vlan_members [N_VLANS],
input logic tx_valid,
input logic [4:0] egress_port,
input logic [VIDX_W-1:0] frame_vidx,
input logic [VIDX_W-1:0] ingress_vidx,
input logic vlan_untagged_here,
input logic emitted_tagged,
input logic [VID_W-1:0] emitted_vid,
input logic [VID_W-1:0] assigned_vid,
input logic [13:0] emitted_len,
input logic [2:0] emitted_tags,
input logic [ADDR_W-1:0] in_da, input logic [ADDR_W-1:0] out_da,
input logic [ADDR_W-1:0] in_sa, input logic [ADDR_W-1:0] out_sa,
input logic fcs_recomputed,
output logic [CNT_W-1:0] v_form_wrong, // tagged where untagged
output logic [CNT_W-1:0] v_vid_rewritten, // relabelled at egress
output logic [CNT_W-1:0] v_addr_modified,
output logic [CNT_W-1:0] v_illegal_length,
output logic [CNT_W-1:0] v_stale_fcs, // changed without recompute
output logic [CNT_W-1:0] v_nonmember_emit,
output logic conformant
);
logic [13:0] max_after;
assign max_after = 14'd1518 + (14'(emitted_tags) * 14'd4);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
v_form_wrong <= '0;
v_vid_rewritten <= '0;
v_addr_modified <= '0;
v_illegal_length <= '0;
v_stale_fcs <= '0;
v_nonmember_emit <= '0;
end else if (tx_valid) begin
// THE FORM CHECK. The emitted frame's tagged-ness must match this
// port's untagged set for this VLAN. A tagged frame on an untagged
// port reaches a station that cannot parse it; an untagged frame on
// a trunk lands in the far end's native VLAN.
if (emitted_tagged == vlan_untagged_here)
if (!(&v_form_wrong)) v_form_wrong <= v_form_wrong + 1'b1;
// Chapter 13.1's P6, at the point the tag is built. Egress may
// change the frame's FORM and never its VLAN.
if (emitted_tagged && (emitted_vid != assigned_vid))
if (!(&v_vid_rewritten)) v_vid_rewritten <= v_vid_rewritten + 1'b1;
if (frame_vidx != ingress_vidx)
if (!(&v_vid_rewritten)) v_vid_rewritten <= v_vid_rewritten + 1'b1;
// THE RESTATED P11. Addresses survive every transformation.
if ((out_da != in_da) || (out_sa != in_sa))
if (!(&v_addr_modified)) v_addr_modified <= v_addr_modified + 1'b1;
// The limit for the tag count AFTER the transformation -- Section 13.
if ((emitted_len < 14'd64) || (emitted_len > max_after))
if (!(&v_illegal_length)) v_illegal_length <= v_illegal_length + 1'b1;
// A frame whose length changed and whose FCS was not recomputed is
// a frame carrying a check sequence over bytes it no longer has.
if ((emitted_tagged != (emitted_tags != 3'd0)) && !fcs_recomputed)
if (!(&v_stale_fcs)) v_stale_fcs <= v_stale_fcs + 1'b1;
// Chapter 13.1 Section 12's invariant, unchanged.
if (!vlan_members[frame_vidx][egress_port])
if (!(&v_nonmember_emit)) v_nonmember_emit <= v_nonmember_emit + 1'b1;
end
end
assign conformant = (v_form_wrong == '0) && (v_vid_rewritten == '0) &&
(v_addr_modified == '0) && (v_illegal_length == '0) &&
(v_stale_fcs == '0) && (v_nonmember_emit == '0);
endmoduleClassification: synthesizable, and intended to stay in silicon.
What it teaches: that v_form_wrong is the check with no analogue anywhere else in the track. Every other conformance monitor asks whether the frame's content is right. This one asks whether its form matches the port it is leaving through — and both errors are silent in opposite directions. A tagged frame emitted onto an untagged access port reaches a station that parses the TPID as an EtherType and misreads everything after it. An untagged frame emitted onto a trunk lands in the far end's native VLAN, which is Section 16's merge produced by one bit rather than by two configurations.
And v_stale_fcs catches the failure a transformation makes uniquely easy. The frame's bytes changed; the check sequence over them did not. Every receiver discards it, the ingress port that made the change reports nothing, and Chapter 12.6 §9's relocated evidence appears one hop away.
Deliberately simplified: address comparison against captured ingress values. A production monitor tags the descriptor with a hash of the immutable fields at ingress and compares the hash at egress, because carrying 96 bits of address alongside every frame to check them is more expensive than the check is worth.
Production implication: conformant here means the transformation was the one this port's configuration calls for, it preserved everything it was required to preserve, and it changed everything it was required to change. It does not mean the frame is in the VLAN anybody intended — Section 16 established that no signal in this switch can mean that, because the intent lives in a configuration at the far end. Section 18's rejected property is the attempt to claim it anyway.
18. Properties Worth Asserting, and One Worth Refusing
Every property here is about one switch's transformation. Not one is about what the frame's VLAN means at the far end, and this section's rejected class is why.
Acceptance and assignment
// P1. The acceptable-frame-types test is on the frame's FORM. A VID-0
// frame is TAGGED and is refused by an untagged-only port, even though
// Section 7 will assign it the PVID as though it were untagged.
property p_form_test_uses_form;
@(posedge clk) disable iff (!rst_n)
(frame_valid && (accept == AFT_UNTAGGED_ONLY) && arrived_tagged) |-> !admit;
endproperty
a_form_test: assert property (p_form_test_uses_form);
// P2. A tagged-only port refuses untagged frames.
property p_tagged_only_refuses_untagged;
@(posedge clk) disable iff (!rst_n)
(frame_valid && (accept == AFT_TAGGED_ONLY) && !arrived_tagged)
|-> (!admit && (reject == VR_WRONG_TYPE));
endproperty
a_tagged_only: assert property (p_tagged_only_refuses_untagged);
// P3. VID 4095 is refused outright -- unlike 0, it has no defined
// alternative meaning and no assignment rule could rescue it.
property p_vid_4095_refused;
@(posedge clk) disable iff (!rst_n)
(frame_valid && arrived_tagged && (carried_vid == 12'd4095))
|-> (!admit && (reject == VR_RESERVED_VID));
endproperty
a_4095_refused: assert property (p_vid_4095_refused);
// P4. THE ASSIGNMENT RULE. A usable carried VID is the frame's VLAN;
// anything else falls back to the port's PVID.
property p_assignment_rule;
@(posedge clk) disable iff (!rst_n)
assign_valid |-> (assigned_vid ==
((arrived_tagged && (carried_vid != 12'd0) &&
(carried_vid != 12'd4095)) ? carried_vid : port_pvid));
endproperty
a_assignment: assert property (p_assignment_rule);
// P5. THE ASYMMETRY. The VID falls back to the port; the PCP does not.
// A VID-0 frame's priority is honoured even though its VLAN is
// discarded, because the sender expressed one and not the other.
property p_pcp_survives_vid_fallback;
@(posedge clk) disable iff (!rst_n)
(assign_valid && arrived_tagged) |-> (assigned_pcp == carried_pcp);
endproperty
a_pcp_honoured: assert property (p_pcp_survives_vid_fallback);
// P6. An untagged frame takes the port's default priority, because it
// expressed none.
property p_untagged_takes_default_pcp;
@(posedge clk) disable iff (!rst_n)
(assign_valid && !arrived_tagged) |-> (assigned_pcp == port_default_pcp);
endproperty
a_default_pcp: assert property (p_untagged_takes_default_pcp);
// P7. PROVENANCE. Every assignment is recorded as carried or invented,
// because only an invented VLAN can be mismatched with the far end.
property p_provenance_recorded;
@(posedge clk) disable iff (!rst_n)
assign_valid |-> (vid_was_carried ^ vid_was_invented);
endproperty
a_provenance: assert property (p_provenance_recorded);Ingress filtering
// P8. With filtering enabled, a frame assigned a VLAN the ingress port is
// not a member of is discarded.
property p_ingress_filter_drops_nonmember;
@(posedge clk) disable iff (!rst_n)
(assign_valid && filter_enable && !is_member)
|-> (!pass && (reject == VR_INGRESS_FILTER));
endproperty
a_ingress_filter: assert property (p_ingress_filter_drops_nonmember);
// P9. With filtering DISABLED the frame passes -- and the comparison is
// still performed, because c_would_have_filtered is the only evidence
// the gap is being used.
property p_gap_is_measured;
@(posedge clk) disable iff (!rst_n)
(assign_valid && !filter_enable && !is_member)
|=> (c_would_have_filtered > $past(c_would_have_filtered));
endproperty
a_gap_measured: assert property (p_gap_is_measured);
// P10. An invented VID never trips the filter -- a sensibly configured
// port is a member of its own PVID, so only a CARRIED VID can violate
// membership.
property p_invented_vid_is_a_member;
@(posedge clk) disable iff (!rst_n)
(assign_valid && vid_was_invented) |-> is_member;
endproperty
a_pvid_is_member: assert property (p_invented_vid_is_a_member);
// P11. Chapter 13.1's isolation invariant is about the EGRESS, so a
// missing ingress filter does not violate it. The two are different
// halves of the boundary.
property p_ingress_gap_is_not_isolation;
@(posedge clk) disable iff (!rst_n)
(tx_valid && !filter_enable) |-> vlan_members[frame_vidx][egress_port];
endproperty
a_egress_still_holds: assert property (p_ingress_gap_is_not_isolation);The egress transformation
// P12. THE ACTION IS CHOSEN BY THE PORT, not by the frame.
property p_action_from_untagged_set;
@(posedge clk) disable iff (!rst_n)
(act_valid && is_member)
|-> (action == (vlan_untagged_here
? (frame_is_tagged ? EG_STRIP : EG_PASS)
: (frame_is_tagged ? EG_PASS : EG_TAG)));
endproperty
a_action_choice: assert property (p_action_from_untagged_set);
// P13. A non-member port drops, whatever its untagged set says about a
// VLAN it does not carry.
property p_nonmember_drops;
@(posedge clk) disable iff (!rst_n)
(act_valid && !is_member) |-> (action == EG_DROP);
endproperty
a_nonmember_drop: assert property (p_nonmember_drops);
// P14. THE NO-OP IS RECOGNISED. A frame already in the right form is not
// stripped and reinserted -- Section 13 shows that costs an FCS
// recomputation for nothing.
property p_noop_recognised;
@(posedge clk) disable iff (!rst_n)
(act_valid && (action == EG_PASS)) |-> !fcs_recompute;
endproperty
a_noop_free: assert property (p_noop_recognised);
// P15. Every real transformation changes the length by exactly 4.
property p_length_delta_is_four;
@(posedge clk) disable iff (!rst_n)
(act_valid && (action inside {EG_STRIP, EG_TAG}))
|-> ((new_len == frame_len - 14'd4) || (new_len == frame_len + 14'd4));
endproperty
a_delta_four: assert property (p_length_delta_is_four);
// P16. Egress changes the frame's FORM and never its VLAN. Chapter 13.1's
// P6, at the point the tag is built.
property p_egress_never_relabels;
@(posedge clk) disable iff (!rst_n)
(tx_valid && emitted_tagged) |-> (emitted_vid == assigned_vid);
endproperty
a_no_relabel: assert property (p_egress_never_relabels);
// P17. THE FORM CHECK. The emitted frame's tagged-ness matches this
// port's untagged set for this VLAN.
property p_emitted_form_matches_port;
@(posedge clk) disable iff (!rst_n)
tx_valid |-> (emitted_tagged != vlan_untagged_here);
endproperty
a_form_matches: assert property (p_emitted_form_matches_port);Length repair
// P18. A strip that produces a runt is RE-PADDED. Chapter 7.3 requires
// 64 octets and a 64-octet tagged frame becomes 60 without one.
property p_runt_is_repadded;
@(posedge clk) disable iff (!rst_n)
(act_valid && (new_len < 14'd64)) |-> (need_pad && (final_len == 14'd64));
endproperty
a_repad: assert property (p_runt_is_repadded);
// P19. The limit that applies is the one for the tag count AFTER the
// transformation. A design checking the pre-transformation limit rejects
// its own output.
property p_limit_is_post_transform;
@(posedge clk) disable iff (!rst_n)
act_valid |-> (max_after == (14'd1518 + (14'(tags_after) * 14'd4)));
endproperty
a_post_limit: assert property (p_limit_is_post_transform);
// P20. Every length change recomputes the FCS. A frame whose bytes
// changed and whose check sequence did not is discarded by every
// receiver, and the port that changed it reports nothing.
property p_fcs_follows_every_change;
@(posedge clk) disable iff (!rst_n)
(act_valid && ((action inside {EG_STRIP, EG_TAG}) || need_pad))
|-> fcs_recompute;
endproperty
a_fcs_recompute: assert property (p_fcs_follows_every_change);
// P21. A self-inflicted oversize is counted SEPARATELY from one that
// arrived -- the remedies differ.
property p_self_inflicted_counted_apart;
@(posedge clk) disable iff (!rst_n)
(act_valid && !length_ok && (action == EG_TAG))
|=> (c_oversize_self_inflicted > $past(c_oversize_self_inflicted));
endproperty
a_self_oversize: assert property (p_self_inflicted_counted_apart);
// P22. THE RESTATED P11. Chapter 12.1 asserted a frame is never modified;
// this chapter modifies frames by design. What survives every
// transformation is the addresses.
property p_addresses_survive;
@(posedge clk) disable iff (!rst_n)
tx_valid |-> ((out_da == in_da) && (out_sa == in_sa));
endproperty
a_addresses_intact: assert property (p_addresses_survive);Native VLAN and conformance
// P23. Untagged arrivals on a trunk are NORMAL -- the native VLAN
// working as designed. The counter is a denominator, not an alarm.
property p_untagged_on_trunk_is_not_an_error;
@(posedge clk) disable iff (!rst_n)
(learn_valid && port_is_trunk && !arrived_tagged) |-> (reject == VR_OK);
endproperty
a_native_is_normal: assert property (p_untagged_on_trunk_is_not_an_error);
// P24. A mismatch is a SUSPICION, not a verdict -- proving it needs the
// far end's PVID, which is not observable.
property p_mismatch_is_advisory;
@(posedge clk) disable iff (!rst_n)
native_mismatch_suspected |-> (suspect_vidx_a != suspect_vidx_b);
endproperty
a_suspicion_named: assert property (p_mismatch_is_advisory);
// P25. The signature requires BOTH qualifiers: one trunk port, and an
// untagged sighting. A router with legs in several VLANs has neither.
property p_signature_needs_both_qualifiers;
@(posedge clk) disable iff (!rst_n)
$rose(native_mismatch_suspected) |-> port_is_trunk;
endproperty
a_both_qualifiers: assert property (p_signature_needs_both_qualifiers);
// P26. A stale FCS is caught. The frame's bytes changed and the check
// sequence over them did not.
property p_no_stale_fcs;
@(posedge clk) disable iff (!rst_n)
tx_valid |-> (v_stale_fcs == $past(v_stale_fcs));
endproperty
a_no_stale_fcs: assert property (p_no_stale_fcs);
// P27. Chapter 13.1 Section 12's invariant survives every transformation
// in this chapter.
property p_isolation_survives_transformation;
@(posedge clk) disable iff (!rst_n)
tx_valid |-> vlan_members[frame_vidx][egress_port];
endproperty
a_isolation_holds: assert property (p_isolation_survives_transformation);
// P28. Conformance means the transformation was the one this port's
// configuration calls for -- never that the frame is in the VLAN anybody
// intended.
property p_conformant_definition;
@(posedge clk) disable iff (!rst_n)
conformant |-> ((v_form_wrong == '0) && (v_vid_rewritten == '0) &&
(v_addr_modified == '0) && (v_illegal_length == '0) &&
(v_stale_fcs == '0) && (v_nonmember_emit == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);
// P29. "Access port" and "trunk port" are DERIVED from the untagged set,
// not configured as a mode -- a hybrid port must remain expressible.
property p_mode_is_derived;
@(posedge clk) disable iff (!rst_n)
($countones(untagged[rd_port]) == 1) |-> access_like[rd_port];
endproperty
a_mode_derived: assert property (p_mode_is_derived);
// P30. Several untagged VLANs on one port is expressible and is almost
// always wrong -- they all arrive at the far end indistinguishable.
property p_multiple_untagged_flagged;
@(posedge clk) disable iff (!rst_n)
($countones(untagged[rd_port]) > 1) |-> multi_untagged_warning[rd_port];
endproperty
a_multi_untagged: assert property (p_multiple_untagged_flagged);19. Verification Scenarios
Seventy-two scenarios. The transformation scenarios have no acceptable failure; the native-VLAN ones have expected outcomes that include two switches both behaving correctly while traffic crosses between VLANs.
Acceptance
| # | Scenario | Expected |
|---|---|---|
| 1 | Untagged frame, AFT_ADMIT_ALL | admitted |
| 2 | Untagged frame, AFT_TAGGED_ONLY | refused, VR_WRONG_TYPE |
| 3 | Untagged frame, AFT_UNTAGGED_ONLY | admitted |
| 4 | Tagged VID 10, AFT_ADMIT_ALL | admitted, VLAN 10 |
| 5 | Tagged VID 10, AFT_UNTAGGED_ONLY | refused |
| 6 | Tagged VID 0, AFT_UNTAGGED_ONLY | refused — it arrived tagged |
| 7 | Tagged VID 0, AFT_ADMIT_ALL | admitted, c_vid_zero_admitted |
| 8 | Tagged VID 4095, any setting | refused, VR_RESERVED_VID |
| 9 | Access port, AFT_ADMIT_ALL, station tags its own frames | the station's chosen VLAN is honoured |
| 10 | Same port, AFT_UNTAGGED_ONLY | refused — the safe setting is the restrictive one |
Assignment
| # | Scenario | Expected |
|---|---|---|
| 11 | Untagged arrival, PVID 10 | VLAN 10, vid_was_invented |
| 12 | Tagged VID 20 arrival, PVID 10 | VLAN 20, vid_was_carried |
| 13 | Tagged VID 0, PVID 10, PCP 5 | VLAN 10, PCP 5 — VLAN discarded, priority kept |
| 14 | Untagged arrival, port default PCP 3 | PCP 3 |
| 15 | Any assignment | vid_was_carried ^ vid_was_invented |
| 16 | Trunk with high c_invented | receiving untagged frames — Section 14's warning |
| 17 | Access port with high c_carried | stations are tagging; check accept |
Ingress filtering
| # | Scenario | Expected |
|---|---|---|
| 18 | Tagged VID 10, port not a member, filter on | discarded, VR_INGRESS_FILTER |
| 19 | Same, filter off | admitted into VLAN 10 |
| 20 | Same, filter off | c_would_have_filtered increments |
| 21 | Same, filter off, then the frame is forwarded | Chapter 13.1 §12 isolation still clean |
| 22 | Untagged arrival, filter on, port is a member of its PVID | passes — an invented VID never trips it |
| 23 | Port not a member of its own PVID | a configuration error; every untagged frame filtered |
| 24 | AFT_ADMIT_ALL + filter off on a station port | any station can enter any VLAN |
Egress transformation
| # | Scenario | Expected |
|---|---|---|
| 25 | Tagged frame, VLAN in the untagged set | EG_STRIP, −4, FCS recomputed |
| 26 | Untagged frame, VLAN in the tagged set | EG_TAG, +4, FCS recomputed |
| 27 | Tagged frame, VLAN in the tagged set | EG_PASS — no transformation, no FCS work |
| 28 | Untagged frame, VLAN in the untagged set | EG_PASS |
| 29 | Port not a member of the VLAN | EG_DROP, whatever the untagged set says |
| 30 | Any emitted frame | tagged-ness matches the port's untagged set |
| 31 | Emitted VID | equals the assigned VID — never rewritten |
| 32 | Access port over a run | mostly c_strip and c_pass |
| 33 | Trunk over a run | mostly c_insert and c_pass |
| 34 | Substantial mixture of all three | a hybrid port — legitimate or a misconfiguration |
| 35 | Strip-then-reinsert of the same frame | DEI is lost unless the descriptor carries it |
Length
| # | Scenario | Expected |
|---|---|---|
| 36 | 64-octet tagged frame, stripped | 60 — a runt; re-padded to 64 |
| 37 | Same, not re-padded | every downstream receiver discards it |
| 38 | 68-octet tagged frame, stripped | 64 — no padding needed |
| 39 | 1518-octet untagged frame, tag inserted | 1522 — legal, tagged maximum |
| 40 | Same, checked against a fixed 1518 | the switch rejects its own output |
| 41 | 1522-octet tagged frame, second tag inserted | 1526 — legal only as a double tag |
| 42 | Any transformation | FCS recomputed |
| 43 | Length changed, FCS not recomputed | v_stale_fcs |
| 44 | Self-inflicted oversize | counted apart from an arriving one |
| 45 | Access port with small frames, c_repadded = 0 | runts are being emitted |
Native VLAN
| # | Scenario | Expected |
|---|---|---|
| 46 | Trunk, natives match at both ends | untagged traffic stays in one VLAN |
| 47 | A native 1, B native 99, A sends VLAN 1 | arrives in B's VLAN 99 |
| 48 | Same, B sends VLAN 99 | arrives in A's VLAN 1 — bidirectional |
| 49 | Same, A sends VLAN 99 tagged | arrives correctly in B's VLAN 99 |
| 50 | Result at B | A's VLAN 1 and A's VLAN 99 are merged |
| 51 | Chapter 13.1 §12 isolation at A and B | both clean |
| 52 | v_vid_changed at A and B | both zero |
| 53 | Chapter 12.2 §9's move detector | quiet |
| 54 | Section 15's detector | one address, two VLANs, one trunk port, untagged sighting |
| 55 | Same, a router with legs in two VLANs | not flagged — neither qualifier fits |
| 56 | Trunk with an empty untagged set | untagged arrivals refused — the failure becomes a counter |
Configuration shape and conformance
| # | Scenario | Expected |
|---|---|---|
| 57 | Untagged set with exactly one VLAN | access_like |
| 58 | Empty untagged set | trunk_like |
| 59 | Several untagged VLANs on one port | flagged — they merge at the far end |
| 60 | Per-port config state, 24 ports, 4094 VLANs | 12.0 KiB — a fifth of Chapter 13.1's 60 KiB |
| 61 | Addresses across every transformation | unchanged |
| 62 | Healthy run, mixed port modes, one million frames | conformant high throughout |
| 63 | Station tags its own frames, AFT_ADMIT_ALL, filter off | the station chooses its VLAN |
| 64 | Same, AFT_UNTAGGED_ONLY | c_refused_type — the attempt becomes a counter |
| 65 | Trunk-to-trunk tagged frame, Section 11's transformer | zero FCS recomputations, EG_PASS both ends |
| 66 | Same, strip-on-ingress/insert-on-egress design | two recomputations and two realignments for no net change |
| 67 | Priority regeneration table, boundary port | arriving PCP 7 remapped to 0 — 24 bits per port |
| 68 | Trunk to a peer under the same administration | PCP passed through unchanged |
| 69 | Access port, tagged frame emitted onto it | v_form_wrong — the station cannot parse it |
| 70 | Trunk, untagged frame emitted onto it | lands in the far end's native VLAN — the same bit, opposite direction |
| 71 | Membership array merged with the untagged set | every member VLAN untagged — a trunk destroys every VID |
| 72 | Per-port state, 24 ports, 4094 VLANs | 12.0 KiB — a fifth of Chapter 13.1's 60 KiB |
20. Debugging Port Modes
Every row produces a switch that forwards frames. Several produce two switches that are both provably correct while traffic crosses between VLANs.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| Two VLANs behaving as one across a trunk | native VLAN mismatch | native_mismatch_suspected — nothing else fires |
| Same, and both switches report clean isolation | expected — Section 16's table | compare the two PVIDs by hand; no device can |
| A station reaches a VLAN it should not | AFT_ADMIT_ALL + ingress filtering off | c_would_have_filtered, worst_port |
| Same, and isolation is clean | expected — the gap is at the ingress | admission_gap_open |
| A station receives frames it cannot parse | a tagged frame emitted onto an access port | v_form_wrong |
| Downstream FCS errors from one port | stale FCS after a transformation | v_stale_fcs, and c_repadded = 0 on small frames |
| Downstream runt errors from one port | stripping without re-padding | c_repadded = 0 while c_strip is high |
| The switch rejects frames it just created | length checked against the pre-transform limit | c_oversize_self_inflicted |
| A trunk's traffic lands in one VLAN | the far end sends untagged; our PVID absorbs it | high c_invented on a trunk |
| Several VLANs merged at the far end | several untagged VLANs on one port | $countones(untagged[port]) > 1 |
| A phone-and-PC port called misconfigured | it is a legitimate hybrid | c_strip, c_insert and c_pass all substantial |
| DEI lost on a path | strip-and-reinsert rebuilt the tag | c_strip and c_insert on consecutive hops |
| Priority honoured from an untrusted port | no regeneration table at the boundary | Chapter 13.2 §4's c_by_pcp collapsed into one bucket |
| Two FCS recomputations per frame on a trunk path | strip-on-ingress/insert-on-egress | c_strip and c_insert both high where c_pass should be |
| A frame the sender and receiver disagree about byte-for-byte | re-padding after a strip | c_repadded — the switch is not a transparent relay |
| A trunk carrying untagged traffic nobody configured | the far end's native, arriving as ours | c_invented on a trunk, before any user symptom |
| Two VLANs that behave as one only for broadcast | merged at the far end by a shared native | Section 16's table — the merge is at B, not at A |
| An access port whose station cannot see the network | AFT_TAGGED_ONLY on a port facing an untagged host | c_refused_type at 100% of arrivals |
| Isolation verified, admission never checked | the two halves of the boundary confused | c_would_have_filtered — isolation cannot see the ingress |
21. Common Misconceptions
1 — "A frame carries its VLAN."
The wrong model: the VLAN is an attribute of the frame, like its addresses.
What it costs: the entire native-VLAN failure becomes inexplicable, and Section 18's rejected property looks reasonable. An untagged frame contains no VLAN information of any kind — no field, no flag, no encoding — so its VLAN is a number the receiving port invented milliseconds after the station finished transmitting.
The corrected model: a VLAN is carried only on a tagged link, in octets 14–15. Everywhere else it is a descriptor field inside a switch, and across an untagged link it is destroyed at transmit and re-invented at receive. Continuity is a property of two configurations agreeing.
2 — "Access port and trunk port are hardware modes."
The wrong model: a port is set to one of two modes and behaves accordingly.
What it costs: the hybrid port becomes impossible to express — one VLAN untagged and several tagged, which is how every IP phone with a PC behind it is connected. A design implementing two enumerated modes has hard-coded two points of a continuous space.
The corrected model: the behaviour comes from five configuration fields plus the untagged set, and "access" and "trunk" are names for particular settings. An access port's untagged set has exactly one VLAN; a trunk's is empty. Section 3 derives the names from the settings rather than the other way round, which keeps the third case expressible and makes a port that is neither visible.
3 — "Ingress filtering is a minor optimisation."
The wrong model: the egress membership check provides isolation, so the ingress check is redundant.
What it costs: with AFT_ADMIT_ALL and filtering off, any station can place its traffic in any VLAN by tagging its own frames — and Chapter 13.1 §12's isolation invariant stays clean throughout, because every port the frame then reaches is a member of the VLAN it claimed.
The corrected model: isolation is about the egress; admission is about the ingress, and they are different halves of the boundary. A switch can be provably isolated and still let anybody into any VLAN. c_would_have_filtered costs one comparator and is the only evidence the gap is being used.
4 — "Stripping a tag is the reverse of adding one."
The wrong model: the two transformations are symmetric, so what one does the other undoes.
What it costs: the runt. A minimum-length tagged frame is 64 octets; stripping four leaves 60, which Chapter 7.3 makes illegal and every receiver discards. The strip must be followed by a re-pad, and the padding is four octets that the original frame did not have — so the frame the receiver sees is not the frame the sender sent.
The corrected model: every transformation changes the frame's length, its FCS, and — at the bottom of the size range — its padding. Chapter 12.1's P11, that a frame is never modified, is false by design in this chapter, and Section 12's callout restates it into what must survive, what must change and what must never change.
5 — "A native VLAN mismatch shows up as an error."
The wrong model: something that joins two broadcast domains must trip something.
What it costs: weeks. Every check this track has built reports success — isolation clean at both ends, v_vid_changed zero at both ends, no filtering violations, no format errors, Chapter 12.2 §9's move detector quiet. And they are not failing to look: nothing anomalous happened. Each switch received a well-formed untagged frame on a trunk and applied its configured rule.
The corrected model: the disagreement is between two registers in two buildings, and no frame ever carried either of them. The only signal is Section 15's pattern — one address, two VLANs, one trunk port, with an untagged sighting among them — and it is a suspicion rather than a proof, because proving it needs the far end's PVID.
6 — "Carrying a native VLAN is free."
The wrong model: it is just one more VLAN on the trunk.
What it costs: it is the one VLAN on the trunk whose identity is not transmitted. Every tagged VLAN's VID is in the frame, so two ends disagreeing about what VLAN 10 means is a naming disagreement that cannot move traffic. The native VLAN's identity is in two registers that nothing compares, and a disagreement there moves traffic between broadcast domains.
The corrected model: the mitigation is to carry no native VLAN — an empty untagged set, every VLAN tagged, and AFT_TAGGED_ONLY so untagged arrivals are refused rather than assigned. The cost is that the trunk cannot carry untagged traffic, and the benefit is that the failure becomes a counter. Given that Section 16's table shows every other observable is clean, that is not a close trade.
22. Interview Reasoning
Q1 — "Where does an untagged frame's VLAN come from?"
Reason through it. From the receiving port's configured PVID, invented at ingress. An untagged Ethernet frame contains no VLAN information of any kind — Chapter 5.1's layout has destination, source, EtherType, payload and FCS, and none of them says anything about a VLAN. The strong answer traces the whole journey: the station has no VLAN concept, the access link carries no VID, the port assigns one, a trunk carries it in octets 14–15, and the far end's access port strips it again — so the VLAN exists for the middle of the journey and nowhere else. And it draws the consequence: across any untagged link, continuity is a property of two configurations agreeing rather than of the frame.
Q2 — "What does a port do with a frame tagged VID 0?"
Reason through it. Two rules apply and they use different senses of "untagged". The acceptable-frame-types test is on the frame's form — it arrived tagged, so an AFT_UNTAGGED_ONLY port refuses it. The assignment rule is on its content — VID 0 names no VLAN, so the port supplies its PVID. The strong answer names the asymmetry that makes VID 0 useful at all: the VID falls back to the port and the PCP does not — the frame's priority is honoured even though its VLAN is discarded, because Chapter 13.2 §15 established the two fields are independent and the sender expressed one and not the other. A design that discards the whole tag on VID 0 has thrown away the only thing the sender asked for.
Q3 — "Ingress filtering is disabled and isolation is verified working. Is the switch secure?"
Reason through it. No, and the two facts are compatible. Isolation is about the egress: no frame leaves a port that is not a member of its VLAN. A frame injected into VLAN 10 through a non-member ingress port satisfies that perfectly, because every port it then reaches is a member. The strong answer names the combination that makes it exploitable: AFT_ADMIT_ALL plus filtering off means a station can place its traffic in any VLAN by tagging its own frames, and both are common defaults. And it names the evidence: c_would_have_filtered costs one comparator, is the only record that the gap is being used, and — with Section 7's provenance — admission_gap_open narrows it to the frames whose VID was carried, since an invented VID cannot violate membership.
Q4 — "A 64-octet tagged frame leaves an access port. What does the switch have to do?"
Reason through it. Strip the tag, which takes the frame to 60 octets — a runt — then re-pad to 64 and recompute the FCS. Chapter 7.3 requires 64 octets, so the stripped frame is illegal and every receiver discards it. The strong answer notes what that costs in honesty: the frame the receiver sees is not the frame the sender sent — different length, different FCS, and four octets of padding that were not there. Chapter 12.1's P11, that a frame is never modified in transit, is false by design here, and the invariant has to be restated as the addresses, the EtherType and the payload up to its declared length survive; everything else is the switch's to change.
Q5 — "Two switches, trunk between them, natives 1 and 99. What happens, and what fires?"
Reason through it. A's VLAN 1 traffic crosses untagged and lands in B's VLAN 99; B's VLAN 99 traffic crosses untagged and lands in A's VLAN 1 — a bidirectional join. And it is worse than a swap: A's VLAN 99 traffic crosses tagged and arrives correctly in B's VLAN 99, so B's VLAN 99 now contains two of A's VLANs, merged. The strong answer enumerates what fires: nothing. Isolation clean at both ends, v_vid_changed zero at both, no filtering violations, no format errors, the move detector quiet — and not because the checks are broken, but because each switch received a well-formed untagged frame on a trunk and applied its configured rule. The only signal is the pattern: one address in two VLANs through one trunk port with an untagged sighting among them.
Q6 — "Why can't a switch assert that a frame stays in the VLAN it started in?"
Reason through it. Because on any path with an untagged link, the frame's "source VLAN" is not a property of the frame — it is a number the first switch's ingress port invented from a register. The property compares two values, neither of which the frame carried. The strong answer places it against the track's other rejected properties: Chapter 12.4's subject never travelled back, Chapter 12.6's had not been transmitted yet, and this one's is not on the wire at all, in either direction, ever. What is assertable is the local transformation, the provenance and the pattern — the assignment rule applied exactly, the egress form matching the port, one bit recording whether the VLAN was carried or invented, and a suspicion naming an address, two VLANs and a port when the evidence supports one.
23. Understanding Check
24. What's Next
Module 13 has one chapter left, and it is the one that builds what the previous three have been specifying.
Chapter 13.4 — VLAN Processing in the Switch Datapath assembles it: the VID-to-index map Chapter 13.1 §10 priced at 4 KiB against a dense array's 60, the shared table with its 60-bit key that chapter's Section 8 argued for, and the mapping from Chapter 13.2's PCP field into egress queues — the one piece of per-VLAN behaviour Chapter 13.1 §6 deliberately categorised as belonging to a different mechanism. It also recomputes Chapter 12.5's capacity arithmetic at 60 bits, which changes less than most people expect and one thing by exactly 25%.
Then Module 14 — Flow Control takes up the question every chapter since Chapter 12.1 has deferred. That chapter's §6 established that discarding is a switch's specified response to congestion and rejected backpressure as a cure, on the grounds that it converts a local congestion into a global one. Module 14 asks what a mechanism that applies backpressure anyway has to look like — where the queues actually build, what the PAUSE frame does, and why the pathology Chapter 12.1 predicted is exactly what happens.
And this chapter's PCP field is how that mechanism is eventually made per-class rather than per-link, which is where Module 14 ends.
Continue learning
Related tutorials
- Related topic
Frame Format Overview
Every field exists to let a receiver make one decision at one moment, and the field order is the order those decisions must be made. The check value comes last because it covers everything before it — which makes every decision taken before it provisional.
- Related topic
Why VLANs Exist
Segmentation is the only lever on the broadcast-domain limit, and it turns every singleton in a switch into a vector. Supporting all 4094 VLANs costs 60 KiB — 0.94× the entire forwarding table.
- Related topic
VLAN Processing in the Switch Datapath
Widening the key to {VID, MAC} changes the capacity fractions by nothing and the memory by exactly 25%. The index map that makes 4094 VLANs affordable is also where two VLANs can silently become one.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
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.
