Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Computer Networks  Module 5 – Switching, spanning tree and VLANs

VLANs, trunks and switch security

How 802.1Q tags carry VLANs over trunks, how traffic moves between VLANs, and how port security, DHCP snooping and ARP inspection protect a LAN.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Explain how an 802.1Q tag carries a VLAN ID across a trunk
  • Describe access and trunk ports and how traffic is routed between VLANs
  • Describe port security, DHCP snooping and dynamic ARP inspection and the problems they stop
  • Recognise LACP link aggregation and LLDP neighbour discovery

Before you start

A VLAN (virtual LAN) divides one switched network into several separate ones. Every switch port is placed in a VLAN, and a switch hands a frame only to ports of the frame’s own VLAN, so each VLAN is a broadcast domain of its own, even when its members are spread over many switches. Between switches, a single cable called a trunk carries the frames of many VLANs at once. Tagged frames carry a 4-byte IEEE 802.1Q tag whose 12-bit VLAN ID identifies their VLAN; admitted untagged frames follow the receiving port’s native-VLAN policy. Traffic from one VLAN to another has to go through a router or a layer-3 switch. Because every device reaches the network through a switch port, the access switch is also the natural place to check what devices send: port security limits the addresses a port may use, DHCP snooping lets only trusted ports answer address requests, and dynamic ARP inspection drops ARP messages that contradict what DHCP handed out.

A VLAN bounds layer-two broadcast delivery

Without VLANs, every port of every connected switch is one broadcast domain: one ARP request or DHCP request reaches every device, and any device can talk to any other directly. VLANs let one set of switches carry several networks that stay apart, which is what most offices, campuses and data centres need:

  • Separation. Staff laptops, a guest network, security cameras and IP phones can share the same switches without sharing a broadcast domain. Traffic between them has to cross a router or a firewall, where it can be filtered.
  • Smaller broadcast domains. A broadcast reaches only the ports of its VLAN, so a thousand devices split into ten VLANs each hear about a tenth of the broadcasts.
  • Moves without recabling. Putting a desk into another network is a change to one port’s setting.

Inside a switch, a VLAN behaves like a separate switch: the MAC address table of the first lesson in this module is kept per VLAN, and flooding stays inside the VLAN. This design gives each VLAN one IPv4 subnet: 192.168.10.0/24 for VLAN 10 and 192.168.20.0/24 for VLAN 20. A host compares IP prefixes to choose direct delivery or its gateway; port and tag configuration determine VLAN membership, which an IP address alone does not identify.

Subnet Calculator Plan one subnet per VLAN: work out the range, mask and gateway address that each VLAN's hosts will use.

VLAN IDs run from 1 to 4094. VLAN 1 is the default: a Cisco Catalyst switch creates it at start-up and it cannot be deleted, and a Linux bridge with VLAN filtering turned on puts untagged frames into VLAN 1 unless told otherwise.

Two switches joined by a trunk, each with a host in VLAN 10 and one in VLAN 20, and a router on a stick attached to SW1 by a trunk.Router on a sticketh0.10: 192.168.10.1/24eth0.20: 192.168.20.1/24SW1SW2PC A, VLAN 10192.168.10.25PC B, VLAN 20192.168.20.31PC C, VLAN 10192.168.10.26Printer D, VLAN 20192.168.20.40trunk: every frametagged 10 or 20trunk: every frametagged 10 or 20access port,VLAN 10: untaggedaccess port,VLAN 20: untaggedaccess port,VLAN 10: untaggedaccess port,VLAN 20: untagged

Two VLANs, one trunk and a router on a stick

Text description of the diagram

A router at the top is connected to switch SW1 by a trunk. SW1 is connected to switch SW2 by a second trunk. Frames on both trunks carry an 802.1Q tag with VLAN ID 10 or 20.

  1. SW1 has PC A (192.168.10.25) on an access port in VLAN 10 and PC B (192.168.20.31) on an access port in VLAN 20.
  2. SW2 has PC C (192.168.10.26) on an access port in VLAN 10 and printer D (192.168.20.40) on an access port in VLAN 20.
  3. Access ports send and receive untagged frames, so the hosts never see a tag.
  4. The router has one subinterface per VLAN on the same cable: eth0.10 with 192.168.10.1/24, the gateway of VLAN 10, and eth0.20 with 192.168.20.1/24, the gateway of VLAN 20.
  5. PC A reaches PC C directly through the trunk, in VLAN 10. A packet from PC A to printer D goes to the router tagged 10 and comes back tagged 20.

Follow VLAN 10 and VLAN 20 hosts through the same switch hardware while retaining distinct forwarding contexts.

Interactive · stage by stage A VLAN bounds layer-two broadcast delivery Follow VLAN 10 and VLAN 20 hosts through the same switch hardware while retaining distinct forwarding contexts.
A VLAN bounds layer-two broadcast delivery, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original VLAN 10 and VLAN 20 hosts share hardware but retain separate forwarding contexts.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original comparison separates VLAN 10 and VLAN 20.

Checkpoint

Check: A VLAN bounds layer-two broadcast delivery

  1. Question 1 of 2 Do IP subnet numbers by themselves determine port VLAN membership?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, VLAN membership follows layer-two port and tag configuration

    The original VLAN boundary is configured at layer two. IP subnet design normally aligns with VLANs but does not replace membership rules.

  2. Question 2 of 2 How many VLAN forwarding contexts are in the original VLAN 10 and VLAN 20 comparison?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2 VLANs

    The original comparison separates VLAN 10 and VLAN 20.

An 802.1Q tag carries priority and a VLAN ID

A switch that sends a frame over a trunk inserts a 4-byte tag straight after the two MAC addresses, where the EtherType used to start, and recomputes the frame check sequence, because the CRC covers the tag too:

  • TPID, 2 bytes: 0x8100. It sits where a receiver expects the EtherType, so the receiver knows a tag follows. The original EtherType comes after the tag.
  • PCP, 3 bits: a priority from 0 to 7, which switches use to pick a queue.
  • DEI, 1 bit: marks a frame that may be dropped first when a link is congested.
  • VID, 12 bits: the VLAN ID. Of its 4,096 values, 0 means that the tag carries only a priority and no VLAN (a Linux bridge puts such frames into the port’s default VLAN) and 4095 is reserved, which leaves 4,094 VLANs.

Split TCI 0xA00A into PCP 5, DEI 0 and VID 10, then account for ordinary IDs 1 through 4094.

Interactive · stage by stage An 802.1Q tag carries priority and a VLAN ID Split TCI 0xA00A into PCP 5, DEI 0 and VID 10, then account for ordinary IDs 1 through 4094.
An 802.1Q tag carries priority and a VLAN ID, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original PCP 5/DEI 0/VID 10 produces TCI 0xA00A; 12 bits leave IDs 1–4094 ordinary.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The twelve-bit VLAN ID field leaves ordinary IDs 1 through 4094.

Checkpoint

Check: An 802.1Q tag carries priority and a VLAN ID

  1. Question 1 of 2 Which field holds the original VLAN ID?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The low twelve bits of the TCI

    The original mask is 0x0FFF. Priority occupies a separate three-bit field.

  2. Question 2 of 2 How many ordinary VLAN IDs remain when 0 and 4095 are excluded from twelve-bit values?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 4094 IDs

    The twelve-bit VLAN ID field leaves ordinary IDs 1 through 4094.

A tag adds four bytes to the original frame

The tag adds 4 bytes without taking any from the data, so the largest frame grows from 1,518 to 1,522 bytes, frame check sequence included, while the largest packet inside stays 1,500 bytes. The program below builds a short frame, tags it for VLAN 10 with priority 5, removes the tag again and checks the frame check sequence each time:

Inserting and removing an 802.1Q tag Python · original-vlan_tag/vlan_tag.py
"""Insert and strip an IEEE 802.1Q tag, recomputing the frame check sequence each time."""
import zlib

TPID = 0x8100  # the EtherType that announces a C-tag


def fcs(frame):
    """The frame check sequence: CRC-32 over the frame, sent least significant byte first."""
    return zlib.crc32(frame).to_bytes(4, "little")


def add_tag(frame, vid, pcp=0, dei=0):
    """Insert the 4-byte tag right after the destination and source addresses (the first 12 bytes)."""
    tci = pcp << 13 | dei << 12 | vid  # 3-bit priority, 1-bit drop eligible indicator, 12-bit VLAN ID
    return frame[:12] + TPID.to_bytes(2, "big") + tci.to_bytes(2, "big") + frame[12:]


def strip_tag(frame):
    """Remove the tag and return the frame and the tag's fields (None if the frame is untagged)."""
    if int.from_bytes(frame[12:14], "big") != TPID:
        return frame, None
    tci = int.from_bytes(frame[14:16], "big")
    return frame[:12] + frame[16:], {"pcp": tci >> 13, "dei": tci >> 12 & 1, "vid": tci & 0x0FFF}


def show(name, frame):
    total = len(frame) + 4
    print(f"{name}: {len(frame)} bytes + FCS = {total} bytes, FCS {fcs(frame).hex(' ')}")
    print(f"  first 20 bytes: {frame[:20].hex(' ')}")


destination = bytes.fromhex("00005e005302")  # documentation addresses
source = bytes.fromhex("00005e005301")
data = b"hello, VLAN 10".ljust(46, b"\x00")  # padded to the 46-byte minimum
untagged = destination + source + (0x88B5).to_bytes(2, "big") + data  # EtherType for local experiments

tagged = add_tag(untagged, vid=10, pcp=5)
show("untagged", untagged)
show("tagged  ", tagged)
stripped, fields = strip_tag(tagged)
print(f"tag read back: {fields}")
print(f"stripped frame equals the original: {stripped == untagged}")
print(f"tagged and untagged FCS differ: {fcs(tagged) != fcs(untagged)}")
for name, frame in (("untagged", untagged), ("tagged", tagged)):
    residue = zlib.crc32(frame + fcs(frame))
    print(f"receiver's check over {name} frame + FCS: {residue:#010x}")

Output

untagged: 60 bytes + FCS = 64 bytes, FCS 3e 22 8d 0d
  first 20 bytes: 00 00 5e 00 53 02 00 00 5e 00 53 01 88 b5 68 65 6c 6c 6f 2c
tagged  : 64 bytes + FCS = 68 bytes, FCS a5 c5 10 db
  first 20 bytes: 00 00 5e 00 53 02 00 00 5e 00 53 01 81 00 a0 0a 88 b5 68 65
tag read back: {'pcp': 5, 'dei': 0, 'vid': 10}
stripped frame equals the original: True
tagged and untagged FCS differ: True
receiver's check over untagged frame + FCS: 0x2144df1c
receiver's check over tagged frame + FCS: 0x2144df1c

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vlan_tag.py

One program · 14 languages Insert and strip the original VLAN tag and FCS O(n) for n frame bytes

Python 3.14 · main.py

from zlib import crc32 as crc


def fcs(f: bytes) -> bytes:
    return crc(f).to_bytes(4, "little")


plain = bytes.fromhex("00005e00530200005e00530188b5") + b"hello, VLAN 10".ljust(
    46, b"\0"
)
tag = plain[:12] + bytes([129, 0, 160, 10]) + plain[12:]
tci = int.from_bytes(tag[14:16], "big")
for name, f in [("untagged", plain), ("tagged  ", tag)]:
    size = len(f)
    print(f"{name}: {size} bytes + FCS = {size + 4} bytes, FCS {fcs(f).hex(' ')}")
    print(f"  first 20 bytes: {f[:20].hex(' ')}")
print(
    f"tag read back: {{'pcp': {tci >> 13}, 'dei': {(tci >> 12) & 1}, 'vid': {tci & 4095}}}"
)
print(f"stripped frame equals the original: {tag[:12] + tag[16:] == plain}")
print(f"tagged and untagged FCS differ: {fcs(tag) != fcs(plain)}")
for name, f in [("untagged", plain), ("tagged", tag)]:
    print(f"receiver's check over {name} frame + FCS: {crc(f + fcs(f)):#010x}")

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

function crc(a) {
  let c = 0xffffffff;
  for (const b of a) {
    c ^= b;
    for (let j = 0; j < 8; j++) c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0);
  }
  return (c ^ 0xffffffff) >>> 0;
}
const hex = (a) => a.map((b) => b.toString(16).padStart(2, "0")).join(" ");
const a = [0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181];
a.push(...Array.from("hello, VLAN 10", (c) => c.charCodeAt(0)), ...Array(32).fill(0));
const b = [...a.slice(0, 12), 129, 0, 160, 10, ...a.slice(12)];
const back = [...b.slice(0, 12), ...b.slice(16)],
  t = (b[14] << 8) | b[15];
const frames = [a, b],
  names = ["untagged", "tagged  "];
const sums = frames.map(crc),
  checks = frames.map((f, i) => {
    const n = f.length,
      c = sums[i];
    const tail = Array.from({ length: 4 }, (_, j) => (c >>> (8 * j)) & 255);
    console.log(`${names[i]}: ${n} bytes + FCS = ${n + 4} bytes, FCS ${hex(tail)}`);
    console.log(`  first 20 bytes: ${hex(f.slice(0, 20))}`);
    return crc([...f, ...tail]);
  });
console.log(`tag read back: {'pcp': ${t >>> 13}, 'dei': ${(t >>> 12) & 1}, 'vid': ${t & 4095}}`);
console.log(`stripped frame equals the original: ${hex(back) === hex(a) ? "True" : "False"}`);
console.log(`tagged and untagged FCS differ: ${sums[0] !== sums[1] ? "True" : "False"}`);
for (let i = 0; i < 2; i++) {
  const value = checks[i].toString(16).padStart(8, "0");
  console.log(`receiver's check over ${names[i].trim()} frame + FCS: 0x${value}`);
}

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.Arrays;
import java.util.zip.CRC32;

public class Main {
  static int crc(byte[] a, int n) {
    var c = new CRC32();
    c.update(a, 0, n);
    return (int) c.getValue();
  }

  static String hex(byte[] a, int start, int n) {
    String[] r = new String[n];
    for (int i = 0; i < n; i++) r[i] = String.format("%02x", a[start + i] & 255);
    return String.join(" ", r);
  }

  public static void main(String[] args) {
    byte[] a = new byte[64], b = new byte[68], back = new byte[60];
    String head = "00005e00530200005e00530188b5";
    for (int i = 0; i < 14; i++)
      a[i] = (byte) Integer.parseInt(head.substring(2 * i, 2 * i + 2), 16);
    for (int i = 0; i < 14; i++) a[14 + i] = (byte) "hello, VLAN 10".charAt(i);
    System.arraycopy(a, 0, b, 0, 12);
    b[12] = (byte) 129;
    b[14] = (byte) 160;
    b[15] = 10;
    System.arraycopy(a, 12, b, 16, 48);
    System.arraycopy(b, 0, back, 0, 12);
    System.arraycopy(b, 16, back, 12, 48);
    byte[][] frames = {a, b};
    int[] sizes = {60, 64};
    String[] names = {"untagged", "tagged  "};
    int[] sums = new int[2], checks = new int[2];
    for (int i = 0; i < 2; i++) {
      byte[] f = frames[i];
      int n = sizes[i], c = crc(f, n);
      sums[i] = c;
      for (int j = 0; j < 4; j++) f[n + j] = (byte) (c >>> (8 * j));
      checks[i] = crc(f, n + 4);
      System.out.printf(
          "%s: %d bytes + FCS = %d bytes, FCS %s\n", names[i], n, n + 4, hex(f, n, 4));
      System.out.println("  first 20 bytes: " + hex(f, 0, 20));
    }
    int t = (b[14] & 255) << 8 | b[15] & 255;
    System.out.printf(
        "tag read back: {'pcp': %d, 'dei': %d, 'vid': %d}\n", t >>> 13, t >>> 12 & 1, t & 4095);
    System.out.printf(
        "stripped frame equals the original: %s\n",
        Arrays.equals(back, Arrays.copyOf(a, 60)) ? "True" : "False");
    System.out.printf(
        "tagged and untagged FCS differ: %s\n", sums[0] != sums[1] ? "True" : "False");
    for (int i = 0; i < 2; i++)
      System.out.printf(
          "receiver's check over %s frame + FCS: 0x%08x\n", names[i].trim(), checks[i]);
  }
}

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <stdint.h>
#include <stdio.h>
#include <string.h>
unsigned crc(const unsigned char *a, int n) {
  unsigned c = ~0u;
  for (int i = 0; i < n; i++) {
    c ^= a[i];
    for (int j = 0; j < 8; j++)
      c = (c >> 1) ^ ((c & 1) ? 0xedb88320u : 0);
  }
  return ~c;
}
void hex(const unsigned char *a, int n) {
  for (int i = 0; i < n; i++)
    printf("%s%02x", i ? " " : "", a[i]);
}
int main(void) {
  unsigned char a[64] = {0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181},
                b[68] = {0}, back[60];
  memcpy(a + 14, "hello, VLAN 10", 14);
  memcpy(b, a, 12);
  b[12] = 129;
  b[14] = 160;
  b[15] = 10;
  memcpy(b + 16, a + 12, 48);
  memcpy(back, b, 12);
  memcpy(back + 12, b + 16, 48);
  unsigned char *frames[] = {a, b};
  int sizes[] = {60, 64};
  const char *names[] = {"untagged", "tagged  "};
  unsigned sums[2], checks[2];
  for (int i = 0; i < 2; i++) {
    int n = sizes[i];
    unsigned char *f = frames[i];
    unsigned c = crc(f, n);
    sums[i] = c;
    for (int j = 0; j < 4; j++)
      f[n + j] = (unsigned char)(c >> (8 * j));
    checks[i] = crc(f, n + 4);
    printf("%s: %d bytes + FCS = %d bytes, FCS ", names[i], n, n + 4);
    hex(f + n, 4);
    printf("\n  first 20 bytes: ");
    hex(f, 20);
    puts("");
  }
  unsigned t = (unsigned)b[14] << 8 | b[15];
  printf("tag read back: {'pcp': %u, 'dei': %u, 'vid': %u}\n", t >> 13,
         t >> 12 & 1, t & 4095);
  printf("stripped frame equals the original: %s\n",
         memcmp(back, a, 60) ? "False" : "True");
  printf("tagged and untagged FCS differ: %s\n",
         sums[0] != sums[1] ? "True" : "False");
  for (int i = 0; i < 2; i++)
    printf("receiver's check over %s frame + FCS: 0x%08x\n",
           i ? "tagged" : "untagged", checks[i]);
  return 0;
}

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

static uint Crc(byte[] a, int n)
{
    uint c = uint.MaxValue;
    for (int i = 0; i < n; i++)
    {
        c ^= a[i];
        for (int j = 0; j < 8; j++) c = (c >> 1) ^ ((c & 1) == 1 ? 0xedb88320u : 0);
    }
    return ~c;
}
static string Hex(byte[] a, int from, int n) =>
    string.Join(" ", a.Skip(from).Take(n).Select(x => x.ToString("x2")));
byte[] a = new byte[64], b = new byte[68], back = new byte[60];
byte[] head = [0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181]; head.CopyTo(a, 0);
for (int i = 0; i < 14; i++) a[14 + i] = (byte)"hello, VLAN 10"[i];
Array.Copy(a, 0, b, 0, 12); b[12] = 129; b[14] = 160; b[15] = 10; Array.Copy(a, 12, b, 16, 48);
Array.Copy(b, 0, back, 0, 12); Array.Copy(b, 16, back, 12, 48);
byte[][] frames = [a, b]; int[] sizes = [60, 64]; string[] names = ["untagged", "tagged  "];
uint[] sums = new uint[2], checks = new uint[2];
for (int i = 0; i < 2; i++)
{
    byte[] f = frames[i]; int n = sizes[i]; uint c = Crc(f, n); sums[i] = c;
    for (int j = 0; j < 4; j++) f[n + j] = (byte)(c >> (8 * j)); checks[i] = Crc(f, n + 4);
    Console.WriteLine($"{names[i]}: {n} bytes + FCS = {n + 4} bytes, FCS {Hex(f, n, 4)}");
    Console.WriteLine($"  first 20 bytes: {Hex(f, 0, 20)}");
}
int t = b[14] << 8 | b[15];
Console.WriteLine($"tag read back: {{'pcp': {t >> 13}, 'dei': {t >> 12 & 1}, 'vid': {t & 4095}}}");
Console.WriteLine($"stripped frame equals the original: {back.SequenceEqual(a.Take(60))}");
Console.WriteLine($"tagged and untagged FCS differ: {sums[0] != sums[1]}");
for (int i = 0; i < 2; i++)
    Console.WriteLine($"receiver's check over {names[i].Trim()} frame + FCS: 0x{checks[i]:x8}");

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdint.h>
#include <stdio.h>
#include <string.h>
unsigned crc(const unsigned char *a, int n) {
  unsigned c = ~0u;
  for (int i = 0; i < n; i++) {
    c ^= a[i];
    for (int j = 0; j < 8; j++)
      c = (c >> 1) ^ ((c & 1) ? 0xedb88320u : 0);
  }
  return ~c;
}
void hex(const unsigned char *a, int n) {
  for (int i = 0; i < n; i++)
    printf("%s%02x", i ? " " : "", a[i]);
}
int main(void) {
  unsigned char a[64] = {0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181},
                b[68] = {0}, back[60];
  memcpy(a + 14, "hello, VLAN 10", 14);
  memcpy(b, a, 12);
  b[12] = 129;
  b[14] = 160;
  b[15] = 10;
  memcpy(b + 16, a + 12, 48);
  memcpy(back, b, 12);
  memcpy(back + 12, b + 16, 48);
  unsigned char *frames[] = {a, b};
  int sizes[] = {60, 64};
  const char *names[] = {"untagged", "tagged  "};
  unsigned sums[2], checks[2];
  for (int i = 0; i < 2; i++) {
    int n = sizes[i];
    unsigned char *f = frames[i];
    unsigned c = crc(f, n);
    sums[i] = c;
    for (int j = 0; j < 4; j++)
      f[n + j] = (unsigned char)(c >> (8 * j));
    checks[i] = crc(f, n + 4);
    printf("%s: %d bytes + FCS = %d bytes, FCS ", names[i], n, n + 4);
    hex(f + n, 4);
    printf("\n  first 20 bytes: ");
    hex(f, 20);
    puts("");
  }
  unsigned t = (unsigned)b[14] << 8 | b[15];
  printf("tag read back: {'pcp': %u, 'dei': %u, 'vid': %u}\n", t >> 13,
         t >> 12 & 1, t & 4095);
  printf("stripped frame equals the original: %s\n",
         memcmp(back, a, 60) ? "False" : "True");
  printf("tagged and untagged FCS differ: %s\n",
         sums[0] != sums[1] ? "True" : "False");
  for (int i = 0; i < 2; i++)
    printf("receiver's check over %s frame + FCS: 0x%08x\n",
           i ? "tagged" : "untagged", checks[i]);
  return 0;
}

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

function hex(string $a): string
{
    return implode(' ', str_split(bin2hex($a), 2));
}
$a = hex2bin('00005e00530200005e00530188b5') . str_pad('hello, VLAN 10', 46, "\0");
$b = substr($a, 0, 12) . hex2bin('8100a00a') . substr($a, 12);
$back = substr($b, 0, 12) . substr($b, 16);
$t = ord($b[14]) << 8 | ord($b[15]);
$frames = [$a,$b];
$names = ['untagged','tagged'];
$sums = [];
$checks = [];
foreach ($frames as $i => $f) {
    $n = strlen($f);
    $c = crc32($f);
    $sums[] = $c;
    $tail = pack('V', $c);
    $checks[] = crc32($f . $tail);
    printf("%-8s: %d bytes + FCS = %d bytes, FCS %s\n", $names[$i], $n, $n + 4, hex($tail));
    echo '  first 20 bytes: ' . hex(substr($f, 0, 20)) . "\n";
}
printf("tag read back: {'pcp': %d, 'dei': %d, 'vid': %d}\n", $t >> 13, $t >> 12 & 1, $t & 4095);
echo 'stripped frame equals the original: ' . ($back === $a ? 'True' : 'False') . "\n";
echo 'tagged and untagged FCS differ: ' . ($sums[0] !== $sums[1] ? 'True' : 'False') . "\n";
foreach ($checks as $i => $c) {
    printf(
        "receiver's check over %s frame + FCS: 0x%s\n",
        $names[$i],
        bin2hex(pack('N', $c)),
    );
}

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

fn crc(a: &[u8]) -> u32 {
    let mut c = u32::MAX;
    for &b in a {
        c ^= u32::from(b);
        for _ in 0..8 {
            c = (c >> 1) ^ if c & 1 != 0 { 0xedb88320 } else { 0 }
        }
    }
    !c
}
fn hex(a: &[u8]) -> String {
    a.iter()
        .map(|b| format!("{b:02x}"))
        .collect::<Vec<_>>()
        .join(" ")
}
fn truth(v: bool) -> &'static str {
    if v { "True" } else { "False" }
}
fn main() {
    let mut a = vec![0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181];
    a.extend_from_slice(b"hello, VLAN 10");
    a.resize(60, 0);
    let mut b = a.clone();
    b.splice(12..12, [129, 0, 160, 10]);
    let back = [&b[..12], &b[16..]].concat();
    let t = u16::from_be_bytes([b[14], b[15]]);
    let frames = [a.clone(), b];
    let names = ["untagged", "tagged  "];
    let mut sums = [0; 2];
    let mut checks = [0; 2];
    for (i, f) in frames.iter().enumerate() {
        let n = f.len();
        let c = crc(f);
        sums[i] = c;
        let tail = c.to_le_bytes();
        println!(
            "{}: {n} bytes + FCS = {} bytes, FCS {}",
            names[i],
            n + 4,
            hex(&tail)
        );
        println!("  first 20 bytes: {}", hex(&f[..20]));
        checks[i] = crc(&[f.as_slice(), &tail].concat());
    }
    println!(
        "tag read back: {{'pcp': {}, 'dei': {}, 'vid': {}}}",
        t >> 13,
        t >> 12 & 1,
        t & 4095
    );
    println!("stripped frame equals the original: {}", truth(back == a));
    println!(
        "tagged and untagged FCS differ: {}",
        truth(sums[0] != sums[1])
    );
    for i in 0..2 {
        println!(
            "receiver's check over {} frame + FCS: 0x{:08x}",
            names[i].trim(),
            checks[i]
        );
    }
}

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

function crc(a: number[]): number {
  let c = 0xffffffff;
  for (const b of a) {
    c ^= b;
    for (let j = 0; j < 8; j++) c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0);
  }
  return (c ^ 0xffffffff) >>> 0;
}
const hex = (a: number[]) => a.map((b) => b.toString(16).padStart(2, "0")).join(" ");
const a = [0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181];
a.push(...Array.from("hello, VLAN 10", (c) => c.charCodeAt(0)), ...Array(32).fill(0));
const b = [...a.slice(0, 12), 129, 0, 160, 10, ...a.slice(12)];
const back = [...b.slice(0, 12), ...b.slice(16)],
  t = (b[14] << 8) | b[15];
const frames = [a, b],
  names = ["untagged", "tagged  "];
const sums = frames.map(crc),
  checks = frames.map((f, i) => {
    const n = f.length,
      c = sums[i];
    const tail = Array.from({ length: 4 }, (_, j) => (c >>> (8 * j)) & 255);
    console.log(`${names[i]}: ${n} bytes + FCS = ${n + 4} bytes, FCS ${hex(tail)}`);
    console.log(`  first 20 bytes: ${hex(f.slice(0, 20))}`);
    return crc([...f, ...tail]);
  });
console.log(`tag read back: {'pcp': ${t >>> 13}, 'dei': ${(t >>> 12) & 1}, 'vid': ${t & 4095}}`);
console.log(`stripped frame equals the original: ${hex(back) === hex(a) ? "True" : "False"}`);
console.log(`tagged and untagged FCS differ: ${sums[0] !== sums[1] ? "True" : "False"}`);
for (let i = 0; i < 2; i++) {
  const value = checks[i].toString(16).padStart(8, "0");
  console.log(`receiver's check over ${names[i].trim()} frame + FCS: 0x${value}`);
}

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"bytes"
	"fmt"
	"hash/crc32"
)

func truth(v bool) string {
	if v {
		return "True"
	}
	return "False"
}
func main() {
	a := make([]byte, 60)
	copy(a, []byte{0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181})
	copy(a[14:], "hello, VLAN 10")
	b := make([]byte, 64)
	copy(b, a[:12])
	copy(b[12:], []byte{129, 0, 160, 10})
	copy(b[16:], a[12:])
	back := append(append([]byte{}, b[:12]...), b[16:]...)
	t := uint16(b[14])<<8 | uint16(b[15])
	frames := [][]byte{a, b}
	names := []string{"untagged", "tagged"}
	var sums, checks [2]uint32
	for i, f := range frames {
		n := len(f)
		c := crc32.ChecksumIEEE(f)
		sums[i] = c
		tail := []byte{byte(c), byte(c >> 8), byte(c >> 16), byte(c >> 24)}
		checks[i] = crc32.ChecksumIEEE(append(f, tail...))
		fmt.Printf("%-8s: %d bytes + FCS = %d bytes, FCS % x\n", names[i], n, n+4, tail)
		fmt.Printf("  first 20 bytes: % x\n", f[:20])
	}
	fmt.Printf("tag read back: {'pcp': %d, 'dei': %d, 'vid': %d}\n", t>>13, t>>12&1, t&4095)
	fmt.Printf("stripped frame equals the original: %s\n", truth(bytes.Equal(back, a)))
	fmt.Printf("tagged and untagged FCS differ: %s\n", truth(sums[0] != sums[1]))
	for i, c := range checks {
		fmt.Printf("receiver's check over %s frame + FCS: 0x%08x\n", names[i], c)
	}
}

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

func crc(_ a: [UInt8]) -> UInt32 {
  var c = UInt32.max
  for b in a {
    c ^= UInt32(b)
    for _ in 0..<8 { c = (c >> 1) ^ (c & 1 != 0 ? 0xedb8_8320 : 0) }
  }
  return ~c
}
func hex(_ a: [UInt8]) -> String {
  a.map { String(Int($0) + 256, radix: 16).dropFirst() }.joined(separator: " ")
}
func truth(_ v: Bool) -> String { v ? "True" : "False" }
var a: [UInt8] = [0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181]
a += Array("hello, VLAN 10".utf8) + [UInt8](repeating: 0, count: 32)
let b = Array(a.prefix(12)) + [129, 0, 160, 10] + Array(a.dropFirst(12))
let back = Array(b.prefix(12)) + Array(b.dropFirst(16))
let t = UInt16(b[14]) << 8 | UInt16(b[15])
let frames = [a, b]
let names = ["untagged", "tagged  "]
var sums = [UInt32](repeating: 0, count: 2)
var checks = [UInt32](repeating: 0, count: 2)
for i in 0..<2 {
  let f = frames[i]
  let n = f.count
  let c = crc(f)
  sums[i] = c
  let tail = (0..<4).map { UInt8(truncatingIfNeeded: c >> ($0 * 8)) }
  print("\(names[i]): \(n) bytes + FCS = \(n+4) bytes, FCS \(hex(tail))")
  print("  first 20 bytes: \(hex(Array(f.prefix(20))))")
  checks[i] = crc(f + tail)
}
print("tag read back: {'pcp': \(t>>13), 'dei': \(t>>12&1), 'vid': \(t&4095)}")
print("stripped frame equals the original: \(truth(back==a))")
print("tagged and untagged FCS differ: \(truth(sums[0] != sums[1]))")
for i in 0..<2 {
  let s = String(checks[i], radix: 16)
  let value = String(repeating: "0", count: 8 - s.count) + s
  print("receiver's check over \(i==0 ? "untagged":"tagged") frame + FCS: 0x\(value)")
}

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

require 'zlib'

def hex(bytes)
  bytes.unpack1('H*').scan(/../).join(' ')
end

a = ['00005e00530200005e00530188b5'].pack('H*') + 'hello, VLAN 10'.ljust(46, "\0")
b = a[0, 12] + ['8100a00a'].pack('H*') + a[12..]
back = b[0, 12] + b[16..]
t = b[14, 2].unpack1('n')
frames = [a, b]
names = %w[untagged tagged]
sums = []
checks = []
frames.each_with_index do |frame, i|
  n = frame.bytesize
  sum = Zlib.crc32(frame)
  sums << sum
  tail = [sum].pack('V')
  checks << Zlib.crc32(frame + tail)
  puts format('%<s>-8s: %<n>d bytes + FCS = %<m>d bytes, FCS %<h>s',
              s: names[i], n: n, m: n + 4, h: hex(tail))
  puts "  first 20 bytes: #{hex(frame[0, 20])}"
end
puts format("tag read back: {'pcp': %<p>d, 'dei': %<d>d, 'vid': %<v>d}",
            p: t >> 13, d: (t >> 12) & 1, v: t & 4095)
puts "stripped frame equals the original: #{back == a ? 'True' : 'False'}"
puts "tagged and untagged FCS differ: #{sums[0] != sums[1] ? 'True' : 'False'}"
checks.each_with_index do |sum, i|
  puts format("receiver's check over %<s>s frame + FCS: 0x%<c>08x", s: names[i], c: sum)
end

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

fun crc(a: List<Int>): UInt {
    var c = UInt.MAX_VALUE
    for (b in a) {
        c = c xor b.toUInt()
        repeat(8) { c = (c shr 1) xor if (c and 1u != 0u) 0xedb88320u else 0u }
    }
    return c.inv()
}

fun hex(a: List<Int>) = a.joinToString(" ") { it.toString(16).padStart(2, '0') }

fun main() {
    val head = listOf(0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181)
    val a = head + "hello, VLAN 10".map { it.code } + List(32) { 0 }
    val b = a.take(12) + listOf(129, 0, 160, 10) + a.drop(12)
    val back = b.take(12) + b.drop(16)
    val t = b[14] shl 8 or b[15]
    val frames = listOf(a, b)
    val names = listOf("untagged", "tagged  ")
    val sums = frames.map { crc(it) }
    val checks =
        frames.mapIndexed { i, f ->
            val tail = List(4) { j -> ((sums[i] shr (8 * j)) and 255u).toInt() }
            println("${names[i]}: ${f.size} bytes + FCS = ${f.size + 4} bytes, FCS ${hex(tail)}")
            println("  first 20 bytes: ${hex(f.take(20))}")
            crc(f + tail)
        }
    println("tag read back: {'pcp': ${t shr 13}, 'dei': ${(t shr 12) and 1}, 'vid': ${t and 4095}}")
    println("stripped frame equals the original: ${if (back == a) "True" else "False"}")
    println("tagged and untagged FCS differ: ${if (sums[0] != sums[1]) "True" else "False"}")
    for (i in 0..1) {
        val value = checks[i].toString(16).padStart(8, '0')
        println("receiver's check over ${names[i].trim()} frame + FCS: 0x$value")
    }
}

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

int crc32(List<int> bytes) {
  var crc = 0xffffffff;
  for (final byte in bytes) {
    crc ^= byte;
    for (var bit = 0; bit < 8; bit++) {
      crc = (crc >> 1) ^ ((crc & 1) == 1 ? 0xedb88320 : 0);
    }
  }
  return crc ^ 0xffffffff;
}

List<int> fcs(List<int> bytes) {
  final crc = crc32(bytes);
  return [for (var index = 0; index < 4; index++) (crc >> (8 * index)) & 255];
}

String hex(List<int> bytes) =>
    bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(' ');
String truth(bool v) => v ? 'True' : 'False';
void main() {
  final plain = [0, 0, 94, 0, 83, 2, 0, 0, 94, 0, 83, 1, 136, 181];
  plain.addAll('hello, VLAN 10'.codeUnits);
  plain.addAll(List.filled(32, 0));
  final tag = [...plain.take(12), 129, 0, 160, 10, ...plain.skip(12)];
  final tci = (tag[14] << 8) | tag[15];
  final restored = [...tag.take(12), ...tag.skip(16)];
  for (final (name, f) in [('untagged', plain), ('tagged  ', tag)]) {
    final size = f.length;
    print('$name: $size bytes + FCS = ${size + 4} bytes, FCS ${hex(fcs(f))}');
    print('  first 20 bytes: ${hex(f.take(20).toList())}');
  }
  print(
    "tag read back: {'pcp': ${tci >> 13}, 'dei': ${(tci >> 12) & 1}, 'vid': ${tci & 4095}}",
  );
  print(
    'stripped frame equals the original: ${truth(hex(restored) == hex(plain))}',
  );
  print(
    'tagged and untagged FCS differ: ${truth(hex(fcs(tag)) != hex(fcs(plain)))}',
  );
  for (final (name, f) in [('untagged', plain), ('tagged', tag)]) {
    final residue = crc32([...f, ...fcs(f)]).toRadixString(16).padLeft(8, '0');
    print("receiver's check over $name frame + FCS: 0x$residue");
  }
}

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

untagged: 60 bytes + FCS = 64 bytes, FCS 3e 22 8d 0d
  first 20 bytes: 00 00 5e 00 53 02 00 00 5e 00 53 01 88 b5 68 65 6c 6c 6f 2c
tagged  : 64 bytes + FCS = 68 bytes, FCS a5 c5 10 db
  first 20 bytes: 00 00 5e 00 53 02 00 00 5e 00 53 01 81 00 a0 0a 88 b5 68 65
tag read back: {'pcp': 5, 'dei': 0, 'vid': 10}
stripped frame equals the original: True
tagged and untagged FCS differ: True
receiver's check over untagged frame + FCS: 0x2144df1c
receiver's check over tagged frame + FCS: 0x2144df1c

The four new bytes are 81 00 a0 0a. In binary, 0xA00A is 101 0 000000001010: priority 5, DEI 0 and VLAN 10. Tagging changed the frame check sequence, from 3e 22 8d 0d to a5 c5 10 db, and both frames still pass the receiver’s check, which ends at the same constant for every frame that arrives intact. A switch therefore has to compute a new frame check sequence each time it adds or removes a tag.

Number Base Converter Turn 0xA00A into binary and split it 3, 1 and 12 bits to read the priority, DEI and VLAN ID yourself.

Two larger schemes build on the same idea. A service provider can carry a customer’s tagged frames inside an outer tag of its own, the S-tag with EtherType 0x88A8, so customers keep their VLAN numbers. Data centres that need more than 4,094 separate networks use VXLAN, which carries Ethernet frames inside UDP packets and names each segment with a 24-bit number, enough for about 16 million.

Add four tag bytes to the original 1,518-byte frame and recompute the FCS over the changed byte sequence.

Interactive · stage by stage A tag adds four bytes to the original frame Add four tag bytes to the original 1,518-byte frame and recompute the FCS over the changed byte sequence.
A tag adds four bytes to the original frame, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original full-size 1,518 becomes 1,522 including FCS; recompute FCS over tagged bytes.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original full-size 1,518-byte frame gains a four-byte tag.

Checkpoint

Check: A tag adds four bytes to the original frame

  1. Question 1 of 2 What must change after a tag is inserted?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The FCS is recomputed over the changed frame bytes

    The tag changes the protected byte sequence. Unchanged addresses do not make the whole protected frame unchanged.

  2. Question 2 of 2 What is the length after adding one four-byte tag to the original 1,518-byte frame including FCS?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1522 bytes

    The original full-size 1,518-byte frame gains a four-byte tag.

Ingress classification and egress policy must agree

A switch port works in one of two ways:

  • An access port belongs to one VLAN. Frames from the device arrive untagged and are placed in that VLAN, and frames leave untagged, so the device needs no VLAN support and never sees a tag. Most ports that face computers, printers and cameras are access ports.
  • A trunk port carries several VLANs under its tagged and untagged membership policies. A trunk has an Trunk allowed list: A configured set of VLANs admitted by a trunk policy; delivery depends on the policies and state at the relevant boundaries. of the VLANs it carries, which on Cisco switches is every VLAN from 1 to 4094 unless it is narrowed. Trunks join switches to each other, and to routers and servers that understand tags.

One exception mixes the two. An IP phone with a computer plugged into it sends its own voice frames tagged with a voice VLAN and a priority, while the computer’s frames pass through the phone untagged and land in the port’s data VLAN, so one cable serves two VLANs.

Follow untagged access classification, then test the stated tag or native policy against both trunk allowed lists.

Interactive · stage by stage Ingress classification and egress policy must agree Follow untagged access classification, then test the stated tag or native policy against both trunk allowed lists.
Ingress classification and egress policy must agree, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Untagged access joins assigned VLAN; trunk tag/native policy plus both allowed lists determines delivery.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The stated cable crosses sending and receiving trunk allowed lists.

Checkpoint

Check: Ingress classification and egress policy must agree

  1. Question 1 of 2 What determines whether a tagged VLAN crosses the stated trunk?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both relevant egress and ingress allowed-list policies

    The model tests membership at each trunk boundary. Host IP does not supply the cable's VLAN admission policy.

  2. Question 2 of 2 How many trunk allowed-list boundaries are crossed across one cable with a trunk at each end?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2 allowed lists

    The stated cable crosses sending and receiving trunk allowed lists.

An untagged frame takes the receiving native VLAN

A trunk has one more setting, the Native VLAN classification: The configured association used to classify eligible untagged ingress frames, distinct from the port's tagged and untagged egress rules.. Under the stated untagged-native egress policy, its frames cross without a tag. An admitted untagged frame is classified into the receiving native VLAN; allowed lists and ingress policy still apply. It is VLAN 1 by default on Cisco switches, and Cisco’s configuration guide asks for the same native VLAN at both ends of every trunk. A Linux bridge calls the same idea the PVID: bridge vlan add dev eth1 vid 10 pvid untagged puts untagged frames arriving on eth1 into VLAN 10 and sends VLAN 10’s frames out of it untagged, as an access port does (bridge vlan del dev eth1 vid 1 then removes the default VLAN from the port). VLANs added without those two flags leave the port tagged, as on a trunk.

The next program floods four broadcasts through two switches joined by a trunk that allows VLANs 1, 10, 20 and 99, first with native VLAN 1 at both ends, then with SW2’s end changed to native VLAN 99:

Four broadcasts across a trunk, with and without a native VLAN mismatch Python · original-trunk_delivery/trunk_delivery.py
"""How a trunk carries broadcasts of several VLANs, and what a native VLAN mismatch does."""


def receive_vlan(port, tag):
    """The VLAN a frame belongs to when it enters a port (None: dropped)."""
    if port["mode"] == "access":
        return port["vlan"] if tag is None else None
    if tag is None:
        return port["native"]  # untagged frames on a trunk belong to its native VLAN
    return tag if tag in port["allowed"] else None


def send_tag(port, vlan):
    """How a frame of this VLAN leaves a port: 'untagged', a tag number, or None (not sent)."""
    if port["mode"] == "access":
        return "untagged" if port["vlan"] == vlan else None
    if vlan not in port["allowed"]:
        return None
    return "untagged" if vlan == port["native"] else vlan


def broadcast(switches, trunk_ends, sender):
    """Flood a broadcast from the host `sender` through two switches joined by one trunk."""
    (sw, p), = [(s, n) for s, ports in switches.items() for n, port in ports.items() if port.get("host") == sender]
    vlan = receive_vlan(switches[sw][p], None)
    got, on_trunk = [], "not sent"
    for name, port in switches[sw].items():
        if name == p:
            continue
        tag = send_tag(port, vlan)
        if tag is None:
            continue
        if port["mode"] == "access":
            got.append(port["host"])
            continue
        on_trunk = "untagged" if tag == "untagged" else f"tagged {tag}"
        other_sw, other_p = trunk_ends[(sw, name)]
        far_vlan = receive_vlan(switches[other_sw][other_p], None if tag == "untagged" else tag)
        for n2, port2 in switches[other_sw].items():
            if n2 != other_p and port2["mode"] == "access" and send_tag(port2, far_vlan):
                got.append(port2["host"])
    print(f"  {sender:<8} VLAN {vlan:<3} trunk carries it {on_trunk:<10} received by {', '.join(got) or 'nobody'}")


def network(sw2_native):
    trunk = {"mode": "trunk", "allowed": {1, 10, 20, 99}}
    return {
        "SW1": {1: {"mode": "access", "vlan": 10, "host": "pc-a"}, 2: {"mode": "access", "vlan": 20, "host": "pc-b"},
                3: {"mode": "access", "vlan": 1, "host": "printer"}, 24: trunk | {"native": 1}},
        "SW2": {1: {"mode": "access", "vlan": 10, "host": "pc-c"}, 2: {"mode": "access", "vlan": 20, "host": "pc-d"},
                3: {"mode": "access", "vlan": 99, "host": "camera"}, 24: trunk | {"native": sw2_native}},
    }


ENDS = {("SW1", 24): ("SW2", 24), ("SW2", 24): ("SW1", 24)}
for title, native in (("Native VLAN 1 at both ends:", 1), ("Native VLAN 1 on SW1 but 99 on SW2:", 99)):
    print(title)
    net = network(native)
    for host in ("pc-a", "pc-d", "printer", "camera"):
        broadcast(net, ENDS, host)

Output

Native VLAN 1 at both ends:
  pc-a     VLAN 10  trunk carries it tagged 10  received by pc-c
  pc-d     VLAN 20  trunk carries it tagged 20  received by pc-b
  printer  VLAN 1   trunk carries it untagged   received by nobody
  camera   VLAN 99  trunk carries it tagged 99  received by nobody
Native VLAN 1 on SW1 but 99 on SW2:
  pc-a     VLAN 10  trunk carries it tagged 10  received by pc-c
  pc-d     VLAN 20  trunk carries it tagged 20  received by pc-b
  printer  VLAN 1   trunk carries it untagged   received by camera
  camera   VLAN 99  trunk carries it untagged   received by printer

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trunk_delivery.py

One program · 14 languages Follow all original trunk broadcasts and native settings O(h) per broadcast for h hosts

Python 3.14 · main.py

names = ["pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"]
vlans = [10, 20, 1, 10, 20, 99]
allowed = {1, 10, 20, 99}


def send(v: int, n: int) -> int:
    return -1 if v not in allowed else 0 if v == n else v


def receive(t: int, n: int) -> int:
    return n if t == 0 else t if t in allowed else -1


for n in [1, 99]:
    native = [1, n]
    print(f"native=1,{n}")
    for h in [0, 4, 2, 5]:
        s, v = h // 3, vlans[h]
        t = send(v, native[s])
        far = receive(t, native[1 - s])
        got = [
            names[i]
            for i in range(6)
            if i != h and vlans[i] == (v if i // 3 == s else far)
        ]
        wire = "blocked" if t < 0 else "untagged" if t == 0 else f"tagged:{t}"
        print(f"{names[h]} vlan={v} wire={wire} got={','.join(got) or 'none'}")

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

const names = ["pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"],
  vlans = [10, 20, 1, 10, 20, 99];
const allowed = new Set([1, 10, 20, 99]);
const send = (v, n) => (allowed.has(v) ? (v === n ? 0 : v) : null);
const receive = (t, n) => (t === 0 ? n : allowed.has(t) ? t : null);
for (const n of [1, 99]) {
  const native = [1, n];
  console.log(`native=1,${n}`);
  for (const h of [0, 4, 2, 5]) {
    const s = Math.trunc(h / 3),
      v = vlans[h],
      t = send(v, native[s]);
    const far = t === null ? null : receive(t, native[1 - s]);
    const got = names.filter((_, i) => i !== h && vlans[i] === (Math.trunc(i / 3) === s ? v : far));
    const wire = t === null ? "blocked" : t === 0 ? "untagged" : `tagged:${t}`;
    console.log(`${names[h]} vlan=${v} wire=${wire} got=${got.join(",") || "none"}`);
  }
}

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

public class Main {
  static boolean allowed(int v) {
    return v == 1 || v == 10 || v == 20 || v == 99;
  }

  static int send(int v, int n) {
    return allowed(v) ? v == n ? 0 : v : -1;
  }

  static int receive(int t, int n) {
    return t == 0 ? n : allowed(t) ? t : -1;
  }

  public static void main(String[] args) {
    String[] names = {"pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"};
    int[] vlans = {10, 20, 1, 10, 20, 99};
    for (int n : new int[] {1, 99}) {
      int[] nativeVlan = {1, n};
      System.out.printf("native=1,%d\n", n);
      for (int h : new int[] {0, 4, 2, 5}) {
        int s = h / 3, v = vlans[h], t = send(v, nativeVlan[s]);
        int far = receive(t, nativeVlan[1 - s]);
        String got = "";
        for (int i = 0; i < 6; i++)
          if (i != h && vlans[i] == (i / 3 == s ? v : far))
            got += (got.isEmpty() ? "" : ",") + names[i];
        String wire = t < 0 ? "blocked" : t == 0 ? "untagged" : "tagged:" + t;
        System.out.printf(
            "%s vlan=%d wire=%s got=%s\n", names[h], v, wire, got.isEmpty() ? "none" : got);
      }
    }
  }
}

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <stdio.h>
int allowed(int v) { return v == 1 || v == 10 || v == 20 || v == 99; }
int send(int v, int n) { return allowed(v) ? v == n ? 0 : v : -1; }
int receive(int t, int n) { return t == 0 ? n : allowed(t) ? t : -1; }
int main(void) {
  const char *names[] = {"pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"};
  int vlans[] = {10, 20, 1, 10, 20, 99}, senders[] = {0, 4, 2, 5};
  for (int c = 0; c < 2; c++) {
    int n = c ? 99 : 1, native[] = {1, n};
    printf("native=1,%d\n", n);
    for (int k = 0; k < 4; k++) {
      int h = senders[k], s = h / 3, v = vlans[h], t = send(v, native[s]);
      int far = t < 0 ? -1 : receive(t, native[1 - s]), first = 1;
      char wire[20];
      if (t < 0)
        snprintf(wire, sizeof wire, "blocked");
      else if (t == 0)
        snprintf(wire, sizeof wire, "untagged");
      else
        snprintf(wire, sizeof wire, "tagged:%d", t);
      printf("%s vlan=%d wire=%s got=", names[h], v, wire);
      for (int i = 0; i < 6; i++)
        if (i != h && vlans[i] == (i / 3 == s ? v : far)) {
          printf("%s%s", first ? "" : ",", names[i]);
          first = 0;
        }
      puts(first ? "none" : "");
    }
  }
  return 0;
}

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

string[] names = ["pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"];
int[] vlans = [10, 20, 1, 10, 20, 99];
HashSet<int> allowed = [1, 10, 20, 99];
int? Send(int v, int n) => allowed.Contains(v) ? v == n ? 0 : v : null;
int? Receive(int t, int n) => t == 0 ? n : allowed.Contains(t) ? t : null;
foreach (int n in new[] { 1, 99 })
{
    int[] nativeVlan = [1, n]; Console.WriteLine($"native=1,{n}");
    foreach (int h in new[] { 0, 4, 2, 5 })
    {
        int s = h / 3, v = vlans[h]; int? t = Send(v, nativeVlan[s]);
        int? far = t is null ? null : Receive(t.Value, nativeVlan[1 - s]);
        var got = Enumerable.Range(0, 6).Where(i =>
            i != h && vlans[i] == (i / 3 == s ? v : far)).Select(i => names[i]);
        string wire = t is null ? "blocked" : t == 0 ? "untagged" : $"tagged:{t}";
        string text = string.Join(",", got);
        Console.WriteLine($"{names[h]} vlan={v} wire={wire} got={(text == "" ? "none" : text)}");
    }
}

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdio.h>
int allowed(int v) { return v == 1 || v == 10 || v == 20 || v == 99; }
int send(int v, int n) { return allowed(v) ? v == n ? 0 : v : -1; }
int receive(int t, int n) { return t == 0 ? n : allowed(t) ? t : -1; }
int main(void) {
  const char *names[] = {"pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"};
  int vlans[] = {10, 20, 1, 10, 20, 99}, senders[] = {0, 4, 2, 5};
  for (int c = 0; c < 2; c++) {
    int n = c ? 99 : 1, native[] = {1, n};
    printf("native=1,%d\n", n);
    for (int k = 0; k < 4; k++) {
      int h = senders[k], s = h / 3, v = vlans[h], t = send(v, native[s]);
      int far = t < 0 ? -1 : receive(t, native[1 - s]), first = 1;
      char wire[20];
      if (t < 0)
        snprintf(wire, sizeof wire, "blocked");
      else if (t == 0)
        snprintf(wire, sizeof wire, "untagged");
      else
        snprintf(wire, sizeof wire, "tagged:%d", t);
      printf("%s vlan=%d wire=%s got=", names[h], v, wire);
      for (int i = 0; i < 6; i++)
        if (i != h && vlans[i] == (i / 3 == s ? v : far)) {
          printf("%s%s", first ? "" : ",", names[i]);
          first = 0;
        }
      puts(first ? "none" : "");
    }
  }
  return 0;
}

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

$names = ['pc-a','pc-b','printer','pc-c','pc-d','camera'];
$vlans = [10,20,1,10,20,99];
function allowed(int $v): bool
{
    return in_array($v, [1,10,20,99], true);
}
function send(int $v, int $n): ?int
{
    return allowed($v) ? ($v === $n ? 0 : $v) : null;
}
function receive(int $t, int $n): ?int
{
    return $t === 0 ? $n : (allowed($t) ? $t : null);
}
foreach ([1,99] as $n) {
    $native = [1,$n];
    echo "native=1,$n\n";
    foreach ([0,4,2,5] as $h) {
        $s = intdiv($h, 3);
        $v = $vlans[$h];
        $t = send($v, $native[$s]);
        $far = $t === null ? null : receive($t, $native[1 - $s]);
        $got = [];
        for ($i = 0;$i < 6;++$i) {
            if ($i !== $h && $vlans[$i] === (intdiv($i, 3) === $s ? $v : $far)) {
                $got[] = $names[$i];
            }
        }
        $wire = $t === null ? 'blocked' : ($t === 0 ? 'untagged' : "tagged:$t");
        echo $names[$h] . " vlan=$v wire=$wire got=" . (implode(',', $got) ?: 'none') . "\n";
    }
}

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

fn allowed(v: i32) -> bool {
    [1, 10, 20, 99].contains(&v)
}
fn send(v: i32, n: i32) -> i32 {
    if !allowed(v) {
        -1
    } else if v == n {
        0
    } else {
        v
    }
}
fn receive(t: i32, n: i32) -> i32 {
    if t == 0 {
        n
    } else if allowed(t) {
        t
    } else {
        -1
    }
}
fn main() {
    let names = ["pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"];
    let vlans = [10, 20, 1, 10, 20, 99];
    for n in [1, 99] {
        let native = [1, n];
        println!("native=1,{n}");
        for h in [0, 4, 2, 5] {
            let s = h / 3;
            let v = vlans[h];
            let t = send(v, native[s]);
            let far = receive(t, native[1 - s]);
            let got = (0..6)
                .filter(|&i| i != h && vlans[i] == if i / 3 == s { v } else { far })
                .map(|i| names[i])
                .collect::<Vec<_>>()
                .join(",");
            let wire = match t {
                -1 => "blocked".into(),
                0 => "untagged".into(),
                _ => format!("tagged:{t}"),
            };
            println!(
                "{} vlan={v} wire={wire} got={}",
                names[h],
                if got.is_empty() { "none" } else { &got }
            );
        }
    }
}

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

const names = ["pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"],
  vlans = [10, 20, 1, 10, 20, 99];
const allowed = new Set([1, 10, 20, 99]);
const send = (v: number, n: number): number | null => (allowed.has(v) ? (v === n ? 0 : v) : null);
const receive = (t: number, n: number): number | null => (t === 0 ? n : allowed.has(t) ? t : null);
for (const n of [1, 99]) {
  const native = [1, n];
  console.log(`native=1,${n}`);
  for (const h of [0, 4, 2, 5]) {
    const s = Math.trunc(h / 3),
      v = vlans[h],
      t = send(v, native[s]);
    const far = t === null ? null : receive(t, native[1 - s]);
    const got = names.filter((_, i) => i !== h && vlans[i] === (Math.trunc(i / 3) === s ? v : far));
    const wire = t === null ? "blocked" : t === 0 ? "untagged" : `tagged:${t}`;
    console.log(`${names[h]} vlan=${v} wire=${wire} got=${got.join(",") || "none"}`);
  }
}

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"fmt"
	"strings"
)

func allowed(v int) bool { return v == 1 || v == 10 || v == 20 || v == 99 }
func send(v, n int) int {
	if !allowed(v) {
		return -1
	}
	if v == n {
		return 0
	}
	return v
}
func receive(t, n int) int {
	if t == 0 {
		return n
	}
	if !allowed(t) {
		return -1
	}
	return t
}
func main() {
	names := []string{"pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"}
	vlans := []int{10, 20, 1, 10, 20, 99}
	for _, n := range []int{1, 99} {
		native := []int{1, n}
		fmt.Printf("native=1,%d\n", n)
		for _, h := range []int{0, 4, 2, 5} {
			s, v := h/3, vlans[h]
			t := send(v, native[s])
			far := receive(t, native[1-s])
			got := []string{}
			for i, name := range names {
				target := far
				if i/3 == s {
					target = v
				}
				if i != h && vlans[i] == target {
					got = append(got, name)
				}
			}
			wire := "blocked"
			if t == 0 {
				wire = "untagged"
			} else if t > 0 {
				wire = fmt.Sprint("tagged:", t)
			}
			text := strings.Join(got, ",")
			if text == "" {
				text = "none"
			}
			fmt.Printf("%s vlan=%d wire=%s got=%s\n", names[h], v, wire, text)
		}
	}
}

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

let names = ["pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera"]
let vlans = [10, 20, 1, 10, 20, 99]
let allowed: Set<Int> = [1, 10, 20, 99]
func send(_ v: Int, _ n: Int) -> Int? { allowed.contains(v) ? v == n ? 0 : v : nil }
func receive(_ t: Int, _ n: Int) -> Int? { t == 0 ? n : allowed.contains(t) ? t : nil }
for n in [1, 99] {
  let native = [1, n]
  print("native=1,\(n)")
  for h in [0, 4, 2, 5] {
    let s = h / 3
    let v = vlans[h]
    let t = send(v, native[s])
    let far = t.flatMap { receive($0, native[1 - s]) }
    let got = (0..<6).filter { $0 != h && vlans[$0] == ($0 / 3 == s ? v : far) }
      .map { names[$0] }.joined(separator: ",")
    let wire = t.map { $0 == 0 ? "untagged" : "tagged:\($0)" } ?? "blocked"
    print("\(names[h]) vlan=\(v) wire=\(wire) got=\(got.isEmpty ? "none":got)")
  }
}

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

def send_tag(vlan, native)
  return unless [1, 10, 20, 99].include?(vlan)

  vlan == native ? 0 : vlan
end

def receive(tag, native)
  if tag.zero?
    native
  else
    ([1, 10, 20, 99].include?(tag) ? tag : nil)
  end
end

names = %w[pc-a pc-b printer pc-c pc-d camera]
vlans = [10, 20, 1, 10, 20, 99]
[1, 99].each do |n|
  native = [1, n]
  puts "native=1,#{n}"
  [0, 4, 2, 5].each do |h|
    s = h / 3
    v = vlans[h]
    t = send_tag(v, native[s])
    far = receive(t, native[1 - s]) unless t.nil?
    got = (0...6).select { |i| i != h && vlans[i] == (i / 3 == s ? v : far) }
                 .map { |i| names[i] }.join(',')
    wire = if t.nil?
             'blocked'
           else
             t.zero? ? 'untagged' : "tagged:#{t}"
           end
    puts "#{names[h]} vlan=#{v} wire=#{wire} got=#{got.empty? ? 'none' : got}"
  end
end

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

fun main() {
    val names = listOf("pc-a", "pc-b", "printer", "pc-c", "pc-d", "camera")
    val vlans = listOf(10, 20, 1, 10, 20, 99)
    val allowed = setOf(1, 10, 20, 99)

    fun send(
        v: Int,
        n: Int,
    ): Int =
        when {
            v !in allowed -> -1
            v == n -> 0
            else -> v
        }

    fun receive(
        t: Int,
        n: Int,
    ): Int =
        when {
            t == 0 -> n
            t in allowed -> t
            else -> -1
        }
    for (n in listOf(1, 99)) {
        val native = listOf(1, n)
        println("native=1,$n")
        for (h in listOf(0, 4, 2, 5)) {
            val s = h / 3
            val v = vlans[h]
            val t = send(v, native[s])
            val far = receive(t, native[1 - s])
            val got =
                (0..5)
                    .filter { it != h && vlans[it] == if (it / 3 == s) v else far }
                    .joinToString(",") { names[it] }
                    .ifEmpty { "none" }
            val wire =
                when (t) {
                    -1 -> "blocked"
                    0 -> "untagged"
                    else -> "tagged:$t"
                }
            println("${names[h]} vlan=$v wire=$wire got=$got")
        }
    }
}

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

void main() {
  final names = ['pc-a', 'pc-b', 'printer', 'pc-c', 'pc-d', 'camera'];
  final vlans = [10, 20, 1, 10, 20, 99], allowed = {1, 10, 20, 99};
  int send(int v, int n) {
    if (!allowed.contains(v)) return -1;
    return v == n ? 0 : v;
  }

  int receive(int t, int n) {
    if (t == 0) return n;
    return allowed.contains(t) ? t : -1;
  }

  for (final n in [1, 99]) {
    final native = [1, n];
    print('native=1,$n');
    for (final h in [0, 4, 2, 5]) {
      final s = h ~/ 3, v = vlans[h], t = send(v, native[s]);
      final far = receive(t, native[1 - s]);
      final got = [
        for (var i = 0; i < 6; i++)
          if (i != h && vlans[i] == (i ~/ 3 == s ? v : far)) names[i],
      ].join(',');
      final wire = t < 0
          ? 'blocked'
          : t == 0
          ? 'untagged'
          : 'tagged:$t';
      print('${names[h]} vlan=$v wire=$wire got=${got.isEmpty ? 'none' : got}');
    }
  }
}

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

native=1,1
pc-a vlan=10 wire=tagged:10 got=pc-c
pc-d vlan=20 wire=tagged:20 got=pc-b
printer vlan=1 wire=untagged got=none
camera vlan=99 wire=tagged:99 got=none
native=1,99
pc-a vlan=10 wire=tagged:10 got=pc-c
pc-d vlan=20 wire=tagged:20 got=pc-b
printer vlan=1 wire=untagged got=camera
camera vlan=99 wire=untagged got=printer

With matching ends, each VLAN keeps to itself: pc-a’s broadcast in VLAN 10 reaches only pc-c, and the camera’s in VLAN 99 reaches nobody, because no other host is in VLAN 99. With the mismatch, VLANs 10 and 20 still work, since their frames carry tags. But the printer’s VLAN 1 broadcast leaves SW1 untagged and SW2 puts it into VLAN 99, so the camera receives it, and the camera’s broadcasts leave SW2 untagged and arrive in VLAN 1 on SW1. Two networks meant to be separate have been joined without any router, and neither switch’s table looks wrong. A tag is the only record of a frame’s VLAN, and an untagged frame takes whatever VLAN the receiving port assigns.

For the same reason, careful networks treat the native VLAN as a weak spot. Cisco’s best-practice guide for its Catalyst 6500 and 4500 switches warns that frames can move from one VLAN to another without a router through the untagged native VLAN, and recommends a native VLAN that nothing else uses. Its switches can also tag the native VLAN on trunks with vlan dot1q tag native, after which they drop untagged frames on trunks. Three more settings close the remaining gaps:

  • Ports that face users are set to access mode. Catalyst ports start in a negotiating mode, dynamic auto, in which a port becomes a trunk if the device at the other end asks for one; access mode keeps a port a non-trunk whatever the other end asks for.
  • Each trunk allows only the VLANs it needs, so a VLAN reaches only the switches that serve it.
  • Ports that face users run BPDU guard from the previous lesson, which shuts down a port that receives a spanning tree BPDU.

Follow an untagged frame from SW1 native VLAN 1 into SW2 native VLAN 99, then compare correctly tagged VLANs 10 and 20.

Interactive · stage by stage An untagged frame takes the receiving native VLAN Follow an untagged frame from SW1 native VLAN 1 into SW2 native VLAN 99, then compare correctly tagged VLANs 10 and 20.
An untagged frame takes the receiving native VLAN, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original SW1 native 1 and SW2 native 99 move printer/camera broadcasts; tagged VLANs 10 and 20 remain distinct.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original receiving SW2 native VLAN is 99.

Checkpoint

Check: An untagged frame takes the receiving native VLAN

  1. Question 1 of 2 Where does an untagged trunk frame acquire its receiving VLAN?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: From the receiving trunk's configured native VLAN

    The untagged frame carries no VLAN tag. No tag exists from which to recover the sender's VLAN number.

  2. Question 2 of 2 An untagged frame reaches original SW2 with native VLAN 99. Which VLAN ID does it join?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 99 VLAN ID

    The original receiving SW2 native VLAN is 99.

Inter-VLAN routing adds both flows to each trunk direction

In the stated design, each VLAN has a separate IP subnet. A host uses its address and prefix to choose direct delivery or a gateway; VLAN membership alone does not make that IP decision. A router with interfaces in both VLANs forwards traffic between their subnets under the configured routing and filtering policy. There are two common ways to give a router an interface in every VLAN.

A router on a stick connects one router port to a switch with a trunk and gives it one subinterface per VLAN. On Linux, ip link add link eth0 name eth0.10 type vlan id 10 creates such a subinterface: everything sent through eth0.10 leaves eth0 tagged 10, and frames tagged 10 that arrive on eth0 appear on eth0.10. Follow a packet from PC A in VLAN 10 to printer D in VLAN 20 in the diagram above:

  1. PC A sees that 192.168.20.40 is outside 192.168.10.0/24, so it sends the packet to its gateway, 192.168.10.1: the frame is addressed to the router’s MAC address and leaves PC A untagged.
  2. SW1 tags the frame 10 and sends it up the trunk to the router, which receives it on eth0.10.
  3. The router routes the packet out of eth0.20. It builds a new frame from its own MAC address to the printer’s, tagged 20, and lowers the packet’s time to live by one.
  4. SW1 sends the frame on over the trunk to SW2, still tagged 20, and SW2 removes the tag on the printer’s access port.

Add the original 300 and 500 Mb/s flows to obtain 800 Mb/s in each router-trunk direction.

Interactive · stage by stage Inter-VLAN routing adds both flows to each trunk direction Add the original 300 and 500 Mb/s flows to obtain 800 Mb/s in each router-trunk direction.
Inter-VLAN routing adds both flows to each trunk direction, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original 300 and 500 Mb/s flows put 800 Mb/s on switch-to-router and 800 on the reverse direction.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original 300 and 500 Mb/s routed flows add to 800 in each trunk direction.

Every routed packet therefore crosses the router’s trunk twice, once in each direction. If VLAN 10 sends 400 Mb/s to VLAN 20 and VLAN 20 sends 400 Mb/s back, each direction of a 1 Gb/s full-duplex trunk carries 800 Mb/s, so the two directions of inter-VLAN traffic together can never exceed the speed of one direction of the trunk. A router on a stick is a good fit for a small office or a lab, where one cable and one router port are all there is.

A layer-3 switch routes between VLANs itself. It gets one virtual interface per VLAN, which Cisco calls a switch virtual interface (SVI), configured with the VLAN’s gateway address, and it routes between them inside the switch, so routed traffic crosses no trunk twice. It suits networks whose traffic between VLANs would not fit on one trunk.

Checkpoint

Check: Inter-VLAN routing adds both flows to each trunk direction

  1. Question 1 of 2 Why do both routed flows count in switch-to-router load?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Every stated inter-VLAN packet first goes up that trunk

    Each flow is sent to the router before returning to the switch. Application direction and trunk traversal direction are different.

  2. Question 2 of 2 Original routed flows are 300 and 500 Mb/s. How many Mb/s cross switch-to-router in total?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 800 Mb/s

    The original 300 and 500 Mb/s routed flows add to 800 in each trunk direction.

Snooping separates server messages by port trust

Every frame a device sends enters the network at an access port, so the access switch can check frames before any other device sees them. Three protections are common, and the third depends on records that the second keeps.

Port security limits how many source MAC addresses one port may use. On Cisco switches the limit is one address by default, and a frame from an address beyond the limit is a violation, handled in one of three modes: protect drops such frames silently, restrict drops them and counts and logs the violation, and shutdown, the default, error-disables the port until it is enabled again. It stops a port from filling the switch’s MAC table, the problem from the end of the first lesson in this module, and it notices a small switch or a second device added behind a desk socket. Addresses can be learned once and kept (“sticky”), but phones and laptops that use a random address on each network do not fit that well. Networks that need to know who is connecting use IEEE 802.1X instead, in which a device has to authenticate before the port forwards its traffic.

Compare the untrusted OFFER with the trusted ACK that follows the original client request.

Interactive · stage by stage Snooping separates server messages by port trust Compare the untrusted OFFER with the trusted ACK that follows the original client request.
Snooping separates server messages by port trust, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original untrusted OFFER is dropped; trusted ACK can create the stated binding after a client request.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original demonstration trusts only uplink port 1.

DHCP snooping protects how devices get their addresses. A device that joins a network broadcasts a DHCP request, and a server answers with an address, a gateway and DNS servers; the DHCP lesson later in the track covers the protocol. Any device can send those answers, so a home Wi-Fi router plugged into a desk socket by mistake starts handing out its own addresses, and every computer that takes one loses its connection to the real network. DHCP snooping divides ports into trusted ports, the uplinks towards the real servers, and untrusted ports, which are all ports by default:

  • A server’s messages (DHCPOFFER, DHCPACK, DHCPNAK) that arrive on an untrusted port are dropped.
  • A client message whose sender address differs from the client hardware address written inside it is dropped.
  • From every acknowledgement it passes, the switch records a DHCP snooping binding: A learned association of a leased IP address with client, VLAN and port information under a switch's DHCP snooping policy.: the client’s MAC address, its IP address, the lease time, the VLAN and the port.

Snooping is off until it is switched on, for the whole switch and for each VLAN, and the uplink must be marked trusted, or no device receives an address at all.

Checkpoint

Check: Snooping separates server messages by port trust

  1. Question 1 of 2 What does the original model do with an OFFER on an untrusted port?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Drop the server-only message

    Only the configured uplink is trusted in the model. Receiving an OFFER does not establish a safe trust boundary.

  2. Question 2 of 2 How many ports are trusted in the original AccessSwitch demonstration?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1 trusted port

    The original demonstration trusts only uplink port 1.

ARP checks need an applicable binding and explicit lifetime

Dynamic ARP inspection (DAI) protects the step after that. ARP finds the MAC address that belongs to an IPv4 address, and it has no way to check an answer, so hosts accept the answers they receive. A device that claims the gateway’s address, whether through a typing mistake in a static configuration or deliberately, draws other hosts’ traffic to itself. DAI intercepts every ARP packet on untrusted ports and compares its sender’s IP and MAC addresses with the ARP inspection binding: An applicable trusted address association used by an ARP validation policy to test whether a sender matches permitted information. (or with configured lists for hosts with static addresses), and it drops packets that disagree. It also limits ARP on each untrusted port, by default to 15 packets a second on Cisco switches, and error-disables a port that sends more. Like snooping, it is off for every VLAN by default.

A fourth protection, IP source guard, applies the same bindings to all IP traffic: an untrusted port may send only from the address DHCP gave it, and optionally only from the matching MAC address. The IETF specifies the same binding idea for DHCPv4 and DHCPv6 in RFC 7513.

Compare the valid PC binding with the unbound and mismatched senders, while keeping the model’s lease lifetime limits explicit.

Interactive · stage by stage ARP checks need an applicable binding and explicit lifetime Compare the valid PC binding with the unbound and mismatched senders, while keeping the model's lease lifetime limits explicit.
ARP checks need an applicable binding and explicit lifetime, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original PC binding versus unbound or mismatched sender; explicitly bound demonstration lease/time limitations.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original stored binding has MAC, port and lease duration; this alone is not an expiry clock.

Checkpoint

Check: ARP checks need an applicable binding and explicit lifetime

  1. Question 1 of 2 Does storing lease seconds alone implement binding expiry?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, expiry needs a time reference and an enforced lifetime rule

    The original model stores a duration but has no expiry clock. A duration value alone does not schedule or enforce expiration.

  2. Question 2 of 2 How many fields are stored in the original (MAC, port, lease duration) binding value?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 3 binding fields

    The original stored binding has MAC, port and lease duration; this alone is not an expiry clock.

Address and rate limits depend on the configured model

Here are the three protections at work on one switch in VLAN 10, with the uplink on port 1, a PC on port 2, a home router plugged in by mistake on port 3 and a visitor on port 4:

DHCP snooping, ARP inspection and port security, event by event Python · original-snooping/snooping.py
"""DHCP snooping, dynamic ARP inspection and port security on one access switch, frame by frame."""

UPLINK = 1  # the only trusted port: it leads to the network's DHCP server and router
TRUSTED = {UPLINK}
SERVER_ONLY = {"OFFER", "ACK", "NAK"}  # DHCP messages that only a server sends
ARP_LIMIT = 15  # ARP packets a second allowed on an untrusted port
MAX_MACS = 1  # source addresses allowed on an untrusted port

PC, GATEWAY, HOME_ROUTER = "00:00:5e:00:53:02", "00:00:5e:00:53:01", "00:00:5e:00:53:66"
LAPTOP, PHONE = "00:00:5e:00:53:77", "00:00:5e:00:53:78"


class AccessSwitch:
    def __init__(self, vlan):
        self.vlan = vlan
        self.bindings = {}  # IP address -> (MAC address, port, lease in seconds), learned from DHCP ACKs
        self.asked_on = {}  # client MAC address -> port its DHCP request came from
        self.macs = {}  # untrusted port -> source addresses seen there
        self.arp_seen = {}  # (port, second) -> ARP packets
        self.disabled = set()

    def receive(self, t, port, src, kind, **msg):
        if port in self.disabled:
            return "ignored: the port is error-disabled"
        if port not in TRUSTED:
            seen = self.macs.setdefault(port, set())
            if src not in seen and len(seen) >= MAX_MACS:
                self.disabled.add(port)
                return f"port security: a new address on a port allowed {MAX_MACS}; port {port} shut down"
            seen.add(src)
        if kind == "DHCP":
            return self.dhcp(port, src, **msg)
        return self.arp(t, port, src, **msg)

    def dhcp(self, port, src, op, client, ip=None, lease=None):
        if port not in TRUSTED:
            if op in SERVER_ONLY:
                return f"dropped: a DHCP {op} from an untrusted port"
            if client != src:
                return "dropped: the client address inside is not the frame's source"
            self.asked_on[client] = port
            return f"forwarded to the uplink (port {UPLINK})"
        if op == "ACK" and client in self.asked_on:
            self.bindings[ip] = (client, self.asked_on[client], lease)
            return f"forwarded to port {self.asked_on[client]}; binding {ip} = {client} on port {self.asked_on[client]}"
        return f"forwarded to port {self.asked_on.get(client, 'every untrusted port')}"

    def arp(self, t, port, src, sender_ip):
        if port in TRUSTED:
            return "forwarded without checks (trusted port)"
        second = (port, int(t))
        self.arp_seen[second] = self.arp_seen.get(second, 0) + 1
        if self.arp_seen[second] > ARP_LIMIT:
            self.disabled.add(port)
            return f"over {ARP_LIMIT} ARP packets in a second: port {port} error-disabled"
        binding = self.bindings.get(sender_ip)
        if binding is None:
            return f"dropped: no binding for {sender_ip}"
        if binding[:2] != (src, port):
            return f"dropped: {sender_ip} is bound to {binding[0]} on port {binding[1]}"
        return "forwarded: the sender matches its binding"


def show(t, port, what, result):
    print(f"t={t:<4} port {port}  {what:<40} -> {result}")


sw = AccessSwitch(vlan=10)
events = [
    (0.0, 2, PC, "DHCP", {"op": "DISCOVER", "client": PC}, "PC asks for an address"),
    (0.1, 3, HOME_ROUTER, "DHCP", {"op": "OFFER", "client": PC, "ip": "192.168.0.10"}, "home router answers it"),
    (0.1, 1, GATEWAY, "DHCP", {"op": "OFFER", "client": PC, "ip": "192.168.10.25"}, "the network's server offers"),
    (0.2, 2, PC, "DHCP", {"op": "REQUEST", "client": PC}, "PC requests the offered address"),
    (0.3, 1, GATEWAY, "DHCP", {"op": "ACK", "client": PC, "ip": "192.168.10.25", "lease": 86400}, "the server acknowledges"),
    (1.0, 2, PC, "ARP", {"sender_ip": "192.168.10.25"}, "PC: who has 192.168.10.1?"),
    (1.0, 1, GATEWAY, "ARP", {"sender_ip": "192.168.10.1"}, "gateway: 192.168.10.1 is at my MAC"),
    (2.0, 3, HOME_ROUTER, "ARP", {"sender_ip": "192.168.10.1"}, "port 3: 192.168.10.1 is at my MAC"),
    (2.5, 3, HOME_ROUTER, "ARP", {"sender_ip": "192.168.10.25"}, "port 3: 192.168.10.25 is at my MAC"),
]
for t, port, src, kind, msg, what in events:
    show(t, port, what, sw.receive(t, port, src, kind, **msg))

burst = [sw.receive(4 + i / 20, 3, HOME_ROUTER, "ARP", sender_ip=f"192.168.10.{100 + i}") for i in range(20)]
dropped = sum(r.startswith("dropped") for r in burst)
show(4.0, 3, "20 ARP packets within one second", f"{dropped} dropped, then: {burst[dropped]}")
show(6.0, 3, "one more ARP packet", sw.receive(6.0, 3, HOME_ROUTER, "ARP", sender_ip="192.168.10.66"))
show(7.0, 4, "a laptop asks for an address", sw.receive(7.0, 4, LAPTOP, "DHCP", op="DISCOVER", client=LAPTOP))
show(7.5, 4, "a second device on the same port", sw.receive(7.5, 4, PHONE, "DHCP", op="DISCOVER", client=PHONE))
print("binding table:")
for ip, (mac, port, lease) in sw.bindings.items():
    print(f"  {ip}  {mac}  VLAN {sw.vlan}  port {port}  lease {lease:,} s")

Output

t=0.0  port 2  PC asks for an address                   -> forwarded to the uplink (port 1)
t=0.1  port 3  home router answers it                   -> dropped: a DHCP OFFER from an untrusted port
t=0.1  port 1  the network's server offers              -> forwarded to port 2
t=0.2  port 2  PC requests the offered address          -> forwarded to the uplink (port 1)
t=0.3  port 1  the server acknowledges                  -> forwarded to port 2; binding 192.168.10.25 = 00:00:5e:00:53:02 on port 2
t=1.0  port 2  PC: who has 192.168.10.1?                -> forwarded: the sender matches its binding
t=1.0  port 1  gateway: 192.168.10.1 is at my MAC       -> forwarded without checks (trusted port)
t=2.0  port 3  port 3: 192.168.10.1 is at my MAC        -> dropped: no binding for 192.168.10.1
t=2.5  port 3  port 3: 192.168.10.25 is at my MAC       -> dropped: 192.168.10.25 is bound to 00:00:5e:00:53:02 on port 2
t=4.0  port 3  20 ARP packets within one second         -> 15 dropped, then: over 15 ARP packets in a second: port 3 error-disabled
t=6.0  port 3  one more ARP packet                      -> ignored: the port is error-disabled
t=7.0  port 4  a laptop asks for an address             -> forwarded to the uplink (port 1)
t=7.5  port 4  a second device on the same port         -> port security: a new address on a port allowed 1; port 4 shut down
binding table:
  192.168.10.25  00:00:5e:00:53:02  VLAN 10  port 2  lease 86,400 s

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 snooping.py

One program · 14 languages Check all original owned access-switch events O(n) for n received events

Python 3.14 · main.py

tab, ask, mac, cnt, off = {}, {}, {}, {}, set()


def step(event):
    t, p, src, msg, id, ip, ttl = event
    if p in off:
        return 0
    if p != 1:
        if p in mac and mac[p] != src:
            off.add(p)
            return 1
        mac[p] = src
    if msg:
        if p != 1:
            if msg >= 3:
                return 2
            if id != src:
                return 3
            ask[id] = p
            return 4
        if msg == 4 and id in ask:
            tab[ip] = (id, ask[id], ttl)
            return 5
        return 6
    if p == 1:
        return 7
    key = (p, t // 100)
    cnt[key] = cnt.get(key, 0) + 1
    if cnt[key] > 15:
        off.add(p)
        return 8
    entry = tab.get(ip)
    return 9 if entry is None else 10 if entry[:2] == (src, p) else 11


rowset = [
    [0, 2, 2, 1, 2, 0, 0],
    [10, 3, 102, 3, 2, -10, 0],
    [10, 1, 1, 3, 2, 25, 0],
    [20, 2, 2, 2, 2, 0, 0],
    [30, 1, 1, 4, 2, 25, 86400],
    [100, 2, 2, 0, 0, 25, 0],
    [100, 1, 1, 0, 0, 1, 0],
    [200, 3, 102, 0, 0, 1, 0],
    [250, 3, 102, 0, 0, 25, 0],
]
rowset += [[400 + 5 * i, 3, 102, 0, 0, 100 + i, 0] for i in range(20)]
rowset += [
    [600, 3, 102, 0, 0, 66, 0],
    [700, 4, 119, 1, 119, 0, 0],
    [750, 4, 120, 1, 120, 0, 0],
]
for event in rowset:
    print(step(event))
for ip, (src, p, ttl) in sorted(tab.items()):
    print(f"binding=192.168.10.{ip},00:00:5e:00:53:{src:02x},10,{p},{ttl}")

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

const ask = Array(256).fill(0),
  mac = Array(6).fill(0),
  off = Array(6).fill(false);
const cnt = Array.from({ length: 6 }, () => Array(10).fill(0)),
  tab = Array(256).fill(null);
function step([t, p, src, msg, id, ip, ttl]) {
  if (off[p]) return 0;
  if (p !== 1) {
    if (mac[p] && mac[p] !== src + 1) {
      off[p] = true;
      return 1;
    }
    mac[p] = src + 1;
  }
  if (msg) {
    if (p !== 1) {
      if (msg >= 3) return 2;
      if (id !== src) return 3;
      ask[id] = p;
      return 4;
    }
    if (msg === 4 && ask[id]) {
      tab[ip] = [id, ask[id], ttl];
      return 5;
    }
    return 6;
  }
  if (p === 1) return 7;
  if (++cnt[p][Math.trunc(t / 100)] > 15) {
    off[p] = true;
    return 8;
  }
  const b = tab[ip];
  return b === null || b === undefined ? 9 : b[0] === src && b[1] === p ? 10 : 11;
}
const rows = [
  [0, 2, 2, 1, 2, 0, 0],
  [10, 3, 102, 3, 2, -10, 0],
  [10, 1, 1, 3, 2, 25, 0],
  [20, 2, 2, 2, 2, 0, 0],
  [30, 1, 1, 4, 2, 25, 86400],
  [100, 2, 2, 0, 0, 25, 0],
  [100, 1, 1, 0, 0, 1, 0],
  [200, 3, 102, 0, 0, 1, 0],
  [250, 3, 102, 0, 0, 25, 0],
];
for (let i = 0; i < 20; i++) rows.push([400 + 5 * i, 3, 102, 0, 0, 100 + i, 0]);
rows.push([600, 3, 102, 0, 0, 66, 0], [700, 4, 119, 1, 119, 0, 0], [750, 4, 120, 1, 120, 0, 0]);
for (const e of rows) console.log(step(e));
for (let ip = 0; ip < 256; ip++)
  if (tab[ip]) {
    const [src, p, ttl] = tab[ip];
    console.log(
      `binding=192.168.10.${ip},00:00:5e:00:53:${src.toString(16).padStart(2, "0")},10,${p},${ttl}`,
    );
  }

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

public class Main {
  static final int[] ask = new int[256], mac = new int[6];
  static final int[][] cnt = new int[6][10], tab = new int[256][];
  static final boolean[] off = new boolean[6];

  static int step(int[] e) {
    int t = e[0], p = e[1], src = e[2], msg = e[3], id = e[4], ip = e[5], ttl = e[6];
    if (off[p]) return 0;
    if (p != 1) {
      if (mac[p] != 0 && mac[p] != src + 1) {
        off[p] = true;
        return 1;
      }
      mac[p] = src + 1;
    }
    if (msg != 0) {
      if (p != 1) {
        if (msg >= 3) return 2;
        if (id != src) return 3;
        ask[id] = p;
        return 4;
      }
      if (msg == 4 && ask[id] != 0) {
        tab[ip] = new int[] {id, ask[id], ttl};
        return 5;
      }
      return 6;
    }
    if (p == 1) return 7;
    if (++cnt[p][t / 100] > 15) {
      off[p] = true;
      return 8;
    }
    int[] b = tab[ip];
    return b == null ? 9 : b[0] == src && b[1] == p ? 10 : 11;
  }

  public static void main(String[] args) {
    String text =
        "0 2 2 1 2 0 0;10 3 102 3 2 -10 0;10 1 1 3 2 25 0;20 2 2 2 2 0 0;"
            + "30 1 1 4 2 25 86400;100 2 2 0 0 25 0;100 1 1 0 0 1 0;200 3 102 0 0 1 0;"
            + "250 3 102 0 0 25 0;600 3 102 0 0 66 0;700 4 119 1 119 0 0;750 4 120 1 120 0 0";
    var rows = new java.util.ArrayList<int[]>();
    for (String row : text.split(";"))
      rows.add(java.util.Arrays.stream(row.split(" ")).mapToInt(Integer::parseInt).toArray());
    for (int i = 0; i < 20; i++) rows.add(9 + i, new int[] {400 + 5 * i, 3, 102, 0, 0, 100 + i, 0});
    rows.forEach(e -> System.out.println(step(e)));
    for (int ip = 0; ip < 256; ip++)
      if (tab[ip] != null) {
        int[] b = tab[ip];
        System.out.printf(
            "binding=192.168.10.%d,00:00:5e:00:53:%02x,10,%d,%d%n", ip, b[0], b[1], b[2]);
      }
  }
}

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <stdio.h>
static int tab[256][3], ask[256], mac[6], off[6], cnt[6][10];
static int step(const int *e) {
  int t = e[0], p = e[1], src = e[2], msg = e[3], id = e[4], ip = e[5],
      ttl = e[6];
  if (off[p])
    return 0;
  if (p != 1) {
    if (mac[p] && mac[p] != src + 1) {
      off[p] = 1;
      return 1;
    }
    mac[p] = src + 1;
  }
  if (msg) {
    if (p != 1) {
      if (msg >= 3)
        return 2;
      if (id != src)
        return 3;
      ask[id] = p;
      return 4;
    }
    if (msg == 4 && ask[id]) {
      tab[ip][0] = id;
      tab[ip][1] = ask[id];
      tab[ip][2] = ttl;
      return 5;
    }
    return 6;
  }
  if (p == 1)
    return 7;
  if (++cnt[p][t / 100] > 15) {
    off[p] = 1;
    return 8;
  }
  if (tab[ip][1] == 0)
    return 9;
  return tab[ip][0] == src && tab[ip][1] == p ? 10 : 11;
}
int main(void) {
  const int head[][7] = {
      {0, 2, 2, 1, 2, 0, 0},       {10, 3, 102, 3, 2, -10, 0},
      {10, 1, 1, 3, 2, 25, 0},     {20, 2, 2, 2, 2, 0, 0},
      {30, 1, 1, 4, 2, 25, 86400}, {100, 2, 2, 0, 0, 25, 0},
      {100, 1, 1, 0, 0, 1, 0},     {200, 3, 102, 0, 0, 1, 0},
      {250, 3, 102, 0, 0, 25, 0}};
  for (int i = 0; i < 9; i++)
    printf("%d\n", step(head[i]));
  for (int i = 0; i < 20; i++) {
    int e[] = {400 + 5 * i, 3, 102, 0, 0, 100 + i, 0};
    printf("%d\n", step(e));
  }
  const int tail[][7] = {{600, 3, 102, 0, 0, 66, 0},
                         {700, 4, 119, 1, 119, 0, 0},
                         {750, 4, 120, 1, 120, 0, 0}};
  for (int i = 0; i < 3; i++)
    printf("%d\n", step(tail[i]));
  for (int ip = 0; ip < 256; ip++)
    if (tab[ip][1] != 0)
      printf("binding=192.168.10.%d,00:00:5e:00:53:%02x,10,%d,%d\n", ip,
             tab[ip][0], tab[ip][1], tab[ip][2]);
  return 0;
}

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

int[] ask = new int[256], mac = new int[6];
int[,] cnt = new int[6, 10];
int[]?[] tab = new int[256][];
bool[] off = new bool[6];
int Step(int[] e)
{
    int t = e[0], p = e[1], src = e[2], msg = e[3], id = e[4], ip = e[5], ttl = e[6];
    if (off[p]) return 0;
    if (p != 1)
    {
        if (mac[p] != 0 && mac[p] != src + 1) { off[p] = true; return 1; }
        mac[p] = src + 1;
    }
    if (msg != 0)
    {
        if (p != 1) { if (msg >= 3) return 2; if (id != src) return 3; ask[id] = p; return 4; }
        if (msg == 4 && ask[id] != 0) { tab[ip] = [id, ask[id], ttl]; return 5; }
        return 6;
    }
    if (p == 1) return 7;
    if (++cnt[p, t / 100] > 15) { off[p] = true; return 8; }
    var b = tab[ip]; return b is null ? 9 : b[0] == src && b[1] == p ? 10 : 11;
}
int[][] rows = [[0,2,2,1,2,0,0],[10,3,102,3,2,-10,0],[10,1,1,3,2,25,0],
    [20,2,2,2,2,0,0],[30,1,1,4,2,25,86400],[100,2,2,0,0,25,0],
    [100,1,1,0,0,1,0],[200,3,102,0,0,1,0],[250,3,102,0,0,25,0]];
foreach (var e in rows) Console.WriteLine(Step(e));
for (int i = 0; i < 20; i++) Console.WriteLine(Step([400 + 5 * i, 3, 102, 0, 0, 100 + i, 0]));
int[][] tail = [[600, 3, 102, 0, 0, 66, 0],
    [700, 4, 119, 1, 119, 0, 0], [750, 4, 120, 1, 120, 0, 0]];
foreach (var e in tail)
    Console.WriteLine(Step(e));
for (int ip = 0; ip < 256; ip++) if (tab[ip] is { } b)
    Console.WriteLine($"binding=192.168.10.{ip},00:00:5e:00:53:{b[0]:x2},10,{b[1]},{b[2]}");

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stdio.h>
static int tab[256][3], ask[256], mac[6], off[6], cnt[6][10];
static int step(const int *e) {
  int t = e[0], p = e[1], src = e[2], msg = e[3], id = e[4], ip = e[5],
      ttl = e[6];
  if (off[p])
    return 0;
  if (p != 1) {
    if (mac[p] && mac[p] != src + 1) {
      off[p] = 1;
      return 1;
    }
    mac[p] = src + 1;
  }
  if (msg) {
    if (p != 1) {
      if (msg >= 3)
        return 2;
      if (id != src)
        return 3;
      ask[id] = p;
      return 4;
    }
    if (msg == 4 && ask[id]) {
      tab[ip][0] = id;
      tab[ip][1] = ask[id];
      tab[ip][2] = ttl;
      return 5;
    }
    return 6;
  }
  if (p == 1)
    return 7;
  if (++cnt[p][t / 100] > 15) {
    off[p] = 1;
    return 8;
  }
  if (tab[ip][1] == 0)
    return 9;
  return tab[ip][0] == src && tab[ip][1] == p ? 10 : 11;
}
int main(void) {
  const int head[][7] = {
      {0, 2, 2, 1, 2, 0, 0},       {10, 3, 102, 3, 2, -10, 0},
      {10, 1, 1, 3, 2, 25, 0},     {20, 2, 2, 2, 2, 0, 0},
      {30, 1, 1, 4, 2, 25, 86400}, {100, 2, 2, 0, 0, 25, 0},
      {100, 1, 1, 0, 0, 1, 0},     {200, 3, 102, 0, 0, 1, 0},
      {250, 3, 102, 0, 0, 25, 0}};
  for (int i = 0; i < 9; i++)
    printf("%d\n", step(head[i]));
  for (int i = 0; i < 20; i++) {
    int e[] = {400 + 5 * i, 3, 102, 0, 0, 100 + i, 0};
    printf("%d\n", step(e));
  }
  const int tail[][7] = {{600, 3, 102, 0, 0, 66, 0},
                         {700, 4, 119, 1, 119, 0, 0},
                         {750, 4, 120, 1, 120, 0, 0}};
  for (int i = 0; i < 3; i++)
    printf("%d\n", step(tail[i]));
  for (int ip = 0; ip < 256; ip++)
    if (tab[ip][1] != 0)
      printf("binding=192.168.10.%d,00:00:5e:00:53:%02x,10,%d,%d\n", ip,
             tab[ip][0], tab[ip][1], tab[ip][2]);
  return 0;
}

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

$ask = array_fill(0, 256, 0);
$mac = array_fill(0, 6, 0);
$off = array_fill(0, 6, false);
$cnt = array_fill(0, 6, array_fill(0, 10, 0));
$tab = array_fill(0, 256, [0,0,0]);
function step(array $e): int
{
    global $ask,$mac,$off,$cnt,$tab;
    [$t,$p,$src,$msg,$id,$ip,$ttl] = $e;
    if ($off[$p]) {
        return 0;
    }
    if ($p !== 1) {
        if ($mac[$p] !== 0 && $mac[$p] !== $src + 1) {
            $off[$p] = true;
            return 1;
        }$mac[$p] = $src + 1;
    }
    if ($msg !== 0) {
        if ($p !== 1) {
            if ($msg >= 3) {
                return 2;
            }if ($id !== $src) {
                return 3;
            }$ask[$id] = $p;
            return 4;
        }
        if ($msg === 4 && $ask[$id] !== 0) {
            $tab[$ip] = [$id,$ask[$id],$ttl];
            return 5;
        }return 6;
    }
    if ($p === 1) {
        return 7;
    }
    if (++$cnt[$p][intdiv($t, 100)] > 15) {
        $off[$p] = true;
        return 8;
    }
    $b = $tab[$ip];
    return $b[1] === 0 ? 9 : ($b[0] === $src && $b[1] === $p ? 10 : 11);
}
$rows = [[0,2,2,1,2,0,0],[10,3,102,3,2,-10,0],[10,1,1,3,2,25,0],
    [20,2,2,2,2,0,0],[30,1,1,4,2,25,86400],[100,2,2,0,0,25,0],
    [100,1,1,0,0,1,0],[200,3,102,0,0,1,0],[250,3,102,0,0,25,0]];
foreach ($rows as $e) {
    echo step($e) . "\n";
}
for ($i = 0;$i < 20;++$i) {
    echo step([400 + 5 * $i,3,102,0,0,100 + $i,0]) . "\n";
}
foreach ([[600,3,102,0,0,66,0],[700,4,119,1,119,0,0],[750,4,120,1,120,0,0]] as $e) {
    echo step($e) . "\n";
}
foreach ($tab as $ip => $b) {
    if ($b[1] !== 0) {
        printf("binding=192.168.10.%d,00:00:5e:00:53:%02x,10,%d,%d\n", $ip, $b[0], $b[1], $b[2]);
    }
}

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

fn main() {
    let mut ask = [0; 256];
    let mut mac = [0; 6];
    let mut off = [false; 6];
    let mut cnt = [[0; 10]; 6];
    let mut tab = [[0; 3]; 256];
    let mut rows = vec![
        [0, 2, 2, 1, 2, 0, 0],
        [10, 3, 102, 3, 2, -10, 0],
        [10, 1, 1, 3, 2, 25, 0],
        [20, 2, 2, 2, 2, 0, 0],
        [30, 1, 1, 4, 2, 25, 86400],
        [100, 2, 2, 0, 0, 25, 0],
        [100, 1, 1, 0, 0, 1, 0],
        [200, 3, 102, 0, 0, 1, 0],
        [250, 3, 102, 0, 0, 25, 0],
    ];
    rows.extend((0..20).map(|i| [400 + 5 * i, 3, 102, 0, 0, 100 + i, 0]));
    rows.extend([
        [600, 3, 102, 0, 0, 66, 0],
        [700, 4, 119, 1, 119, 0, 0],
        [750, 4, 120, 1, 120, 0, 0],
    ]);
    let mut step = |[t, p, src, msg, id, ip, ttl]: [i32; 7]| {
        let p = p as usize;
        let id = id as usize;
        if off[p] {
            return 0;
        }
        if p != 1 {
            if mac[p] != 0 && mac[p] != src + 1 {
                off[p] = true;
                return 1;
            }
            mac[p] = src + 1;
        }
        if msg != 0 {
            if p != 1 {
                if msg >= 3 {
                    return 2;
                }
                if id as i32 != src {
                    return 3;
                }
                ask[id] = p;
                return 4;
            }
            if msg == 4 && ask[id] != 0 {
                tab[ip as usize] = [id as i32, ask[id] as i32, ttl];
                return 5;
            }
            return 6;
        }
        if p == 1 {
            return 7;
        }
        cnt[p][(t / 100) as usize] += 1;
        if cnt[p][(t / 100) as usize] > 15 {
            off[p] = true;
            return 8;
        }
        let b = tab[ip as usize];
        if b[1] == 0 {
            9
        } else if b[0] == src && b[1] == p as i32 {
            10
        } else {
            11
        }
    };
    for e in rows {
        println!("{}", step(e));
    }
    for (ip, b) in tab.iter().enumerate().filter(|(_, b)| b[1] != 0) {
        println!(
            "binding=192.168.10.{ip},00:00:5e:00:53:{:02x},10,{},{}",
            b[0], b[1], b[2]
        );
    }
}

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

const ask = Array(256).fill(0),
  mac = Array(6).fill(0),
  off = Array(6).fill(false);
const cnt = Array.from({ length: 6 }, () => Array(10).fill(0)),
  tab: (number[] | null)[] = Array(256).fill(null);
function step([t, p, src, msg, id, ip, ttl]: number[]): number {
  if (off[p]) return 0;
  if (p !== 1) {
    if (mac[p] && mac[p] !== src + 1) {
      off[p] = true;
      return 1;
    }
    mac[p] = src + 1;
  }
  if (msg) {
    if (p !== 1) {
      if (msg >= 3) return 2;
      if (id !== src) return 3;
      ask[id] = p;
      return 4;
    }
    if (msg === 4 && ask[id]) {
      tab[ip] = [id, ask[id], ttl];
      return 5;
    }
    return 6;
  }
  if (p === 1) return 7;
  if (++cnt[p][Math.trunc(t / 100)] > 15) {
    off[p] = true;
    return 8;
  }
  const b = tab[ip];
  return b === null || b === undefined ? 9 : b[0] === src && b[1] === p ? 10 : 11;
}
const rows = [
  [0, 2, 2, 1, 2, 0, 0],
  [10, 3, 102, 3, 2, -10, 0],
  [10, 1, 1, 3, 2, 25, 0],
  [20, 2, 2, 2, 2, 0, 0],
  [30, 1, 1, 4, 2, 25, 86400],
  [100, 2, 2, 0, 0, 25, 0],
  [100, 1, 1, 0, 0, 1, 0],
  [200, 3, 102, 0, 0, 1, 0],
  [250, 3, 102, 0, 0, 25, 0],
];
for (let i = 0; i < 20; i++) rows.push([400 + 5 * i, 3, 102, 0, 0, 100 + i, 0]);
rows.push([600, 3, 102, 0, 0, 66, 0], [700, 4, 119, 1, 119, 0, 0], [750, 4, 120, 1, 120, 0, 0]);
for (const e of rows) console.log(step(e));
for (let ip = 0; ip < 256; ip++)
  if (tab[ip]) {
    const b = tab[ip];
    if (b === null) continue;
    const [src, p, ttl] = b;
    console.log(
      `binding=192.168.10.${ip},00:00:5e:00:53:${src.toString(16).padStart(2, "0")},10,${p},${ttl}`,
    );
  }

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import "fmt"

var ask [256]int
var mac [6]int
var off [6]bool
var cnt [6][10]int
var tab [256][3]int

func step(e [7]int) int {
	t, p, src, msg, id, ip, ttl := e[0], e[1], e[2], e[3], e[4], e[5], e[6]
	if off[p] {
		return 0
	}
	if p != 1 {
		if mac[p] != 0 && mac[p] != src+1 {
			off[p] = true
			return 1
		}
		mac[p] = src + 1
	}
	if msg != 0 {
		if p != 1 {
			if msg >= 3 {
				return 2
			}
			if id != src {
				return 3
			}
			ask[id] = p
			return 4
		}
		if msg == 4 && ask[id] != 0 {
			tab[ip] = [3]int{id, ask[id], ttl}
			return 5
		}
		return 6
	}
	if p == 1 {
		return 7
	}
	cnt[p][t/100]++
	if cnt[p][t/100] > 15 {
		off[p] = true
		return 8
	}
	b := tab[ip]
	if b[1] == 0 {
		return 9
	}
	if b[0] == src && b[1] == p {
		return 10
	}
	return 11
}
func main() {
	rows := [][7]int{{0, 2, 2, 1, 2, 0, 0}, {10, 3, 102, 3, 2, -10, 0}, {10, 1, 1, 3, 2, 25, 0},
		{20, 2, 2, 2, 2, 0, 0}, {30, 1, 1, 4, 2, 25, 86400}, {100, 2, 2, 0, 0, 25, 0},
		{100, 1, 1, 0, 0, 1, 0}, {200, 3, 102, 0, 0, 1, 0}, {250, 3, 102, 0, 0, 25, 0}}
	for _, e := range rows {
		fmt.Println(step(e))
	}
	for i := 0; i < 20; i++ {
		fmt.Println(step([7]int{400 + 5*i, 3, 102, 0, 0, 100 + i, 0}))
	}
	for _, e := range [][7]int{{600, 3, 102, 0, 0, 66, 0},
		{700, 4, 119, 1, 119, 0, 0}, {750, 4, 120, 1, 120, 0, 0}} {
		fmt.Println(step(e))
	}
	for ip, b := range tab {
		if b[1] != 0 {
			fmt.Printf("binding=192.168.10.%d,00:00:5e:00:53:%02x,10,%d,%d\n", ip, b[0], b[1], b[2])
		}
	}
}

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

var ask = [Int](repeating: 0, count: 256)
var mac = [Int](repeating: 0, count: 6)
var off = [Bool](repeating: false, count: 6)
var cnt = [[Int]](repeating: [Int](repeating: 0, count: 10), count: 6)
var tab = [[Int]](repeating: [0, 0, 0], count: 256)
func step(_ e: [Int]) -> Int {
  let t = e[0]
  let p = e[1]
  let src = e[2]
  let msg = e[3]
  let id = e[4]
  let ip = e[5]
  let ttl = e[6]
  if off[p] { return 0 }
  if p != 1 {
    if mac[p] != 0 && mac[p] != src + 1 {
      off[p] = true
      return 1
    }
    mac[p] = src + 1
  }
  if msg != 0 {
    if p != 1 {
      if msg >= 3 { return 2 }
      if id != src { return 3 }
      ask[id] = p
      return 4
    }
    if msg == 4 && ask[id] != 0 {
      tab[ip] = [id, ask[id], ttl]
      return 5
    }
    return 6
  }
  if p == 1 { return 7 }
  cnt[p][t / 100] += 1
  if cnt[p][t / 100] > 15 {
    off[p] = true
    return 8
  }
  let b = tab[ip]
  return b[1] == 0 ? 9 : b[0] == src && b[1] == p ? 10 : 11
}
let rows = [
  [0, 2, 2, 1, 2, 0, 0], [10, 3, 102, 3, 2, -10, 0], [10, 1, 1, 3, 2, 25, 0],
  [20, 2, 2, 2, 2, 0, 0], [30, 1, 1, 4, 2, 25, 86400], [100, 2, 2, 0, 0, 25, 0],
  [100, 1, 1, 0, 0, 1, 0], [200, 3, 102, 0, 0, 1, 0], [250, 3, 102, 0, 0, 25, 0],
]
for e in rows { print(step(e)) }
for i in 0..<20 { print(step([400 + 5 * i, 3, 102, 0, 0, 100 + i, 0])) }
for e in [[600, 3, 102, 0, 0, 66, 0], [700, 4, 119, 1, 119, 0, 0], [750, 4, 120, 1, 120, 0, 0]] {
  print(step(e))
}
for ip in 0..<256 where tab[ip][1] != 0 {
  let b = tab[ip]
  let s = String(b[0], radix: 16)
  let hex = s.count == 1 ? "0" + s : s
  print("binding=192.168.10.\(ip),00:00:5e:00:53:\(hex),10,\(b[1]),\(b[2])")
}

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

def request(ask, row)
  _, port, src, msg, who, = row
  return 2 if msg >= 3
  return 3 if who != src

  ask[who] = port
  4
end

def dhcp(state, row)
  tab, ask = state
  _, port, _, msg, who, ip, ttl = row
  return request(ask, row) if port != 1
  return 6 unless msg == 4 && ask.key?(who)

  tab[ip] = [who, ask[who], ttl]
  5
end

def arp(state, row)
  tab, _, _, cnt, off = state
  ticks, port, src, _, _, ip = row
  return 7 if port == 1

  key = [port, ticks / 100]
  cnt[key] = cnt.fetch(key, 0) + 1
  return (off[port] = true) && 8 if cnt[key] > 15
  return 9 unless tab.key?(ip)

  tab[ip].first(2) == [src, port] ? 10 : 11
end

def security(state, port, src)
  _, _, mac, _, off = state
  return false if port == 1

  old = mac[port]
  return off[port] = true if old && old != src

  mac[port] = src
  false
end

def step(state, row)
  _, port, src, msg = row
  return 0 if state[4][port]
  return 1 if security(state, port, src)

  msg.zero? ? arp(state, row) : dhcp(state, row)
end

rows = [[0, 2, 2, 1, 2, 0, 0], [10, 3, 102, 3, 2, -10, 0], [10, 1, 1, 3, 2, 25, 0],
        [20, 2, 2, 2, 2, 0, 0], [30, 1, 1, 4, 2, 25, 86_400], [100, 2, 2, 0, 0, 25, 0],
        [100, 1, 1, 0, 0, 1, 0], [200, 3, 102, 0, 0, 1, 0], [250, 3, 102, 0, 0, 25, 0]]
rows += (0...20).map { |i| [400 + 5 * i, 3, 102, 0, 0, 100 + i, 0] }
rows += [[600, 3, 102, 0, 0, 66, 0], [700, 4, 119, 1, 119, 0, 0], [750, 4, 120, 1, 120, 0, 0]]
state = [{}, {}, {}, {}, {}]
rows.each { |row| puts step(state, row) }
state[0].keys.sort.each do |ip|
  mac, port, ttl = state[0][ip]
  suffix = mac.to_s(16).rjust(2, '0')
  puts "binding=192.168.10.#{ip},00:00:5e:00:53:#{suffix},10,#{port},#{ttl}"
end

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

val ask = IntArray(256)
val mac = IntArray(6)
val off = BooleanArray(6)
val cnt = Array(6) { IntArray(10) }
val tab = Array(256) { IntArray(3) }

fun step(e: List<Int>): Int {
    val(t, p, src, msg, id) = e
    val ip = e[5]
    val ttl = e[6]
    if (off[p]) return 0
    if (p != 1) {
        if (mac[p] != 0 && mac[p] != src + 1) {
            off[p] = true
            return 1
        }
        mac[p] = src + 1
    }
    if (msg != 0) {
        if (p != 1) {
            if (msg >= 3) return 2
            if (id != src) return 3
            ask[id] = p
            return 4
        }
        if (msg == 4 && ask[id] != 0) {
            tab[ip] = intArrayOf(id, ask[id], ttl)
            return 5
        }
        return 6
    }
    if (p == 1) return 7
    if (++cnt[p][t / 100] > 15) {
        off[p] = true
        return 8
    }
    val b = tab[ip]
    return if (b[1] == 0) {
        9
    } else if (b[0] == src && b[1] == p) {
        10
    } else {
        11
    }
}

fun main() {
    val text =
        "0 2 2 1 2 0 0;10 3 102 3 2 -10 0;10 1 1 3 2 25 0;20 2 2 2 2 0 0;" +
            "30 1 1 4 2 25 86400;100 2 2 0 0 25 0;100 1 1 0 0 1 0;" +
            "200 3 102 0 0 1 0;250 3 102 0 0 25 0;600 3 102 0 0 66 0;" +
            "700 4 119 1 119 0 0;750 4 120 1 120 0 0"
    val rows = text.split(';').map { it.split(' ').map(String::toInt) }.toMutableList()
    rows.addAll(9, (0..19).map { i -> listOf(400 + 5 * i, 3, 102, 0, 0, 100 + i, 0) })
    for (e in rows)println(step(e))
    for (ip in tab.indices) {
        if (tab[ip][1] != 0) {
            val b = tab[ip]
            val suffix = b[0].toString(16).padStart(2, '0')
            println("binding=192.168.10.$ip,00:00:5e:00:53:$suffix,10,${b[1]},${b[2]}")
        }
    }
}

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

void main() {
  final ask = List.filled(256, 0),
      mac = List.filled(6, 0),
      off = List.filled(6, false);
  final cnt = List.generate(6, (_) => List.filled(10, 0)),
      tab = List.generate(256, (_) => List.filled(3, 0));
  int step(List<int> e) {
    final [t, p, src, msg, id, ip, ttl] = e;
    if (off[p]) return 0;
    if (p != 1) {
      if (mac[p] != 0 && mac[p] != src + 1) {
        off[p] = true;
        return 1;
      }
      mac[p] = src + 1;
    }
    if (msg != 0) {
      if (p != 1) {
        if (msg >= 3) return 2;
        if (id != src) return 3;
        ask[id] = p;
        return 4;
      }
      if (msg == 4 && ask[id] != 0) {
        tab[ip] = [id, ask[id], ttl];
        return 5;
      }
      return 6;
    }
    if (p == 1) return 7;
    if (++cnt[p][t ~/ 100] > 15) {
      off[p] = true;
      return 8;
    }
    final b = tab[ip];
    return b[1] == 0
        ? 9
        : b[0] == src && b[1] == p
        ? 10
        : 11;
  }

  final rows = [
    [0, 2, 2, 1, 2, 0, 0],
    [10, 3, 102, 3, 2, -10, 0],
    [10, 1, 1, 3, 2, 25, 0],
    [20, 2, 2, 2, 2, 0, 0],
    [30, 1, 1, 4, 2, 25, 86400],
    [100, 2, 2, 0, 0, 25, 0],
    [100, 1, 1, 0, 0, 1, 0],
    [200, 3, 102, 0, 0, 1, 0],
    [250, 3, 102, 0, 0, 25, 0],
  ];
  for (final e in rows) print(step(e));
  for (var i = 0; i < 20; i++)
    print(step([400 + 5 * i, 3, 102, 0, 0, 100 + i, 0]));
  for (final e in [
    [600, 3, 102, 0, 0, 66, 0],
    [700, 4, 119, 1, 119, 0, 0],
    [750, 4, 120, 1, 120, 0, 0],
  ])
    print(step(e));
  for (var ip = 0; ip < 256; ip++)
    if (tab[ip][1] != 0) {
      final b = tab[ip], suffix = tab[ip][0].toRadixString(16).padLeft(2, '0');
      print('binding=192.168.10.$ip,00:00:5e:00:53:$suffix,10,${b[1]},${b[2]}');
    }
}

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

4
2
6
4
5
10
7
9
11
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
8
0
0
0
0
0
4
1
binding=192.168.10.25,00:00:5e:00:53:02,10,2,86400

The home router’s offer never reaches the PC, so the PC takes the real server’s address, and the switch records the binding 192.168.10.25 on port 2. The PC’s ARP request matches that binding and passes; the gateway’s reply passes unchecked on the trusted uplink. Port 3’s claims to own 192.168.10.1 and 192.168.10.25 both fail the check, the first for want of any binding and the second because the binding names another MAC address on another port. When port 3 then sends 20 ARP packets in one second, the sixteenth exceeds the limit and the port is error-disabled, and a second device behind port 4 shuts that port down too. The ARP decisions all rest on the binding, which only an acknowledgement from the server behind the trusted port could create.

Apply MAX_MACS = 1 and ARP_LIMIT = 15 to the original twenty-message burst in the declared integer-second buckets.

Interactive · stage by stage Address and rate limits depend on the configured model Apply MAX_MACS = 1 and ARP_LIMIT = 15 to the original twenty-message burst in the declared integer-second buckets.
Address and rate limits depend on the configured model, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original MAX_MACS = 1, ARP_LIMIT = 15 and twenty-message burst; original integer-second buckets remain declared.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. The original integer-second bucket permits fifteen before its next packet disables the port.

Checkpoint

Check: Address and rate limits depend on the configured model

  1. Question 1 of 2 What time window does the original ARP counter use?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Integer-second buckets

    The original key includes int(t). Messages across a bucket boundary are counted separately in this model.

  2. Question 2 of 2 How many ARP packets does the original configured integer-second bucket permit before the next disables the port?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 15 ARP packets

    The original integer-second bucket permits fifteen before its next packet disables the port.

Two switches joined by two cables would form a loop, and spanning tree would block one of them. Link aggregation instead bundles parallel links between the same two devices into one logical link, which spanning tree treats as a single port, so all of its links carry traffic. LACP, the Link Aggregation Control Protocol of IEEE 802.1AX, lets the two ends agree on which links form the bundle, and a link that fails leaves the bundle while the others carry on. On Cisco switches a port in active mode starts LACP and a port in passive mode only answers, so at least one end must be active; up to eight links are active in a bundle, and members need the same speed, duplex and VLAN settings. LACP’s messages go out every 30 seconds by default, or every second at the fast rate.

The stated per-flow distribution policy keeps a connection on one Link aggregation member: One physical link in a logical link aggregation, selected for eligible traffic by the implementation's distribution policy. to preserve ordering. A switch can hash selected header fields to choose that member; LACP negotiates aggregation membership rather than prescribing this particular field hash. Which fields the configured implementation uses affects how evenly these flows are spread:

Sharing four links between connections, three ways Python · original-lacp_hash/lacp_hash.py
"""How a bundle of four links shares connections, for three choices of the header fields it hashes."""
import zlib

LINKS = 4
LINK_GBPS = 10
SERVER_MAC, ROUTER_MAC = "00:00:5e:00:53:10", "00:00:5e:00:53:01"
SERVER_IP = "192.0.2.10"


def member(fields):
    """The member link for a frame: a hash of the chosen header fields, modulo the number of links.

    This program uses CRC-32 as the hash. Switches and operating systems each have their own formula, but every
    one of them sends all the frames of one connection over the same link, so that they stay in order.
    """
    return zlib.crc32("|".join(map(str, fields)).encode()) % LINKS


POLICIES = {
    "MAC addresses": lambda ip, port: (SERVER_MAC, ROUTER_MAC),
    "IP addresses": lambda ip, port: (SERVER_IP, ip),
    "IP addresses and ports": lambda ip, port: (SERVER_IP, ip, 443, port),
}


def spread(title, connections):
    print(f"{title}: {len(connections):,} connections over {LINKS} links of {LINK_GBPS} Gb/s")
    for name, fields in POLICIES.items():
        count = [0] * LINKS
        for ip, port in connections:
            count[member(fields(ip, port))] += 1
        print(f"  hash of {name:<22} connections per link {count}, busiest link {max(count) / len(connections):.0%}")


# Every destination is on another network, so every frame leaves through the router: the destination MAC
# address of every frame is the router's.
clients = [f"198.51.100.{n}" for n in range(1, 101)] + [f"203.0.113.{n}" for n in range(1, 101)]
spread("200 clients, 5 connections each", [(ip, 49152 + 97 * k + i) for i, ip in enumerate(clients) for k in range(5)])
spread("2 storage systems, 8 connections each", [(ip, 50000 + k) for ip in ("198.51.100.200", "203.0.113.200") for k in range(8)])
print(f"a single connection never gets more than one link, {LINK_GBPS} Gb/s, whatever the hash")

Output

200 clients, 5 connections each: 1,000 connections over 4 links of 10 Gb/s
  hash of MAC addresses          connections per link [0, 1000, 0, 0], busiest link 100%
  hash of IP addresses           connections per link [245, 255, 250, 250], busiest link 26%
  hash of IP addresses and ports connections per link [258, 259, 237, 246], busiest link 26%
2 storage systems, 8 connections each: 16 connections over 4 links of 10 Gb/s
  hash of MAC addresses          connections per link [0, 16, 0, 0], busiest link 100%
  hash of IP addresses           connections per link [16, 0, 0, 0], busiest link 100%
  hash of IP addresses and ports connections per link [4, 4, 4, 4], busiest link 25%
a single connection never gets more than one link, 10 Gb/s, whatever the hash

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lacp_hash.py

When only MAC addresses are hashed, every frame from the server to clients on other networks carries the same pair of addresses, its own and the router’s, so all 1,000 connections land on one link and the other three stay idle. The default on Cisco switches is to hash the source MAC address, and Linux’s bonding driver hashes MAC addresses by default too; its documentation warns that traffic through a gateway then generally uses one link. Hashing IP addresses spreads the 200 clients evenly, but the two storage systems in the second test both happened to land on the same link. Adding the port numbers spread their 16 connections 4, 4, 4 and 4. In every case one connection gets at most one link’s speed: a backup that could run at 25 Gb/s gets 10 Gb/s at most over a bundle of four 10 Gb/s links.

Compare the original four 10 Gb/s members using MAC, IP and port hash fields under the stated distribution policies.

Interactive · stage by stage A member-selection policy maps a flow to one link Compare the original four 10 Gb/s members using MAC, IP and port hash fields under the stated distribution policies.
A member-selection policy maps a flow to one link, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original four 10 Gb/s links and MAC/IP/port CRC policies; do not claim every platform uses the same fields.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. A stated flow pinned to one of the original 10 Gb/s members has that member ceiling.

Checkpoint

Check: A member-selection policy maps a flow to one link

  1. Question 1 of 2 Does LACP itself prescribe the exact CRC field hash used in this program?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, member-selection policy is an implementation choice

    The program explicitly models selected hash policies. LACP negotiation does not establish this universal field-hash algorithm.

  2. Question 2 of 2 A flow is pinned to one original 10 Gb/s member. What is its member-link ceiling in Gb/s?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 10 Gb/s

    A stated flow pinned to one of the original 10 Gb/s members has that member ceiling.

LLDP describes the directly attached neighbour

LLDP, the Link Layer Discovery Protocol of IEEE 802.1AB, lets every device announce itself on each port at regular intervals: its chassis ID, the ID of the port the message left from and how long to keep the information, usually followed by its system name, its capabilities (bridge, router, phone), a management address and the port’s VLAN. LLDP frames have EtherType 0x88CC and go to 01:80:c2:00:00:0e, an address that bridges do not forward, so each device learns only about its direct neighbour. The lldpd daemon on Linux sends every 30 seconds and asks neighbours to keep its information for 4 times that, 120 seconds; Catalyst switches use the same timers but run LLDP only after lldp run. Cisco’s own Cisco Discovery Protocol (CDP) does the same job between Cisco devices.

LLDP’s neighbour tables are a map of the cabling: lldpcli show neighbors on Linux or show lldp neighbors on a Catalyst switch tells you which device and port sit at the far end of every cable, which quickly shows a cable plugged into the wrong port. An extension, LLDP-MED, lets a switch tell an IP phone which voice VLAN to use. The same announcements describe the device’s model, software and management address to anything plugged into the port, so it is worth turning LLDP off on ports that face users or other organisations unless phones there need it.

Commands for a real network

On Linux, ip link add link eth0 name eth0.10 type vlan id 10 creates a VLAN subinterface; ip link set dev br0 type bridge vlan_filtering 1 turns on VLANs in a bridge, and bridge vlan show lists each port’s VLANs. On a Cisco Catalyst switch, switchport mode access and switchport access vlan 10 make an access port; switchport mode trunk, switchport trunk allowed vlan 10,20 and switchport trunk native vlan 999 set up a trunk.

VLANs in GATE CS

The GATE 2027 Computer Science syllabus lists Ethernet under the data link layer but does not name VLANs, so in that exam the tag appears mainly in frame-size questions. Remember that a tag adds 4 bytes to the frame, taking the largest one from 1,518 to 1,522 bytes, while the largest packet inside stays at 1,500 bytes.

The CCNA 200-301 exam topics cover most of this lesson: access ports for data and voice, the default VLAN and inter-VLAN connectivity, trunk ports, 802.1Q and the native VLAN, CDP and LLDP, EtherChannel with LACP, and DHCP snooping, dynamic ARP inspection and port security.

Read chassis, port and TTL fields as information about the directly attached link neighbour.

Interactive · stage by stage LLDP describes the directly attached neighbour Read chassis, port and TTL fields as information about the directly attached link neighbour.
LLDP describes the directly attached neighbour, step 3 of 3. Showing Original model, Scope, Decision. In focus: Decision.Original modelStated inputsScopeKeep the assumptionsDecisionApply the stated rule

The drawing shows the whole scene; its 3 steps are listed below.

Every step as text
  1. Original chassis, port and TTL fields describe one link neighbour rather than a routed discovery graph.
  2. This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
  3. LLDP describes the direct link neighbour at the port.

Checkpoint

Check: LLDP describes the directly attached neighbour

  1. Question 1 of 2 How far does the stated LLDP neighbour advertisement describe?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The directly attached link neighbour

    The advertisement is link local. It is not a routed path discovery mechanism.

  2. Question 2 of 2 How many directly attached neighbours does one stated LLDP port advertisement describe?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1 neighbour

    LLDP describes the direct link neighbour at the port.

Exercise

Exercise · Medium · Python

Which hosts can reach each other without a router?

Complete l2_reachable and needs_router in vlans.py. Both take a network and the names of two hosts.

network maps each switch's name to its ports, {port number: port}, and each port is a dictionary:

  • "mode" is "access" or "trunk". An access port has "vlan", its VLAN ID. A trunk has "native", its native VLAN, and "allowed", the list of VLANs it carries; the native VLAN passes only if it is in that list too.
  • A port with a host plugged in has "host", the host's name; such a port is always an access port. find_host, already written, returns the (switch, port) of a host.
  • A port cabled to another switch has "to", the (switch, port) at the far end, and that port has a "to" back.

A frame of VLAN v crosses a cable like this:

  1. Leaving an access port, it is sent untagged if the port's VLAN is v, and not sent otherwise.
  2. Leaving a trunk, it is not sent if v is not allowed, sent untagged if v is the native VLAN, and tagged with v otherwise.
  3. Entering an access port, an untagged frame joins the port's VLAN and a tagged frame is dropped.
  4. Entering a trunk, an untagged frame joins the native VLAN and a tagged frame the VLAN of its tag, and it is dropped if that VLAN is not allowed.

Inside a switch a broadcast of VLAN v goes out of every port, so it reaches every host on an access port of v and continues through every cable.

  • l2_reachable(network, host_a, host_b) returns True when a broadcast that host_a sends reaches host_b. Look in that direction only: with some mistakes it is true one way and false the other.
  • needs_router(network, host_a, host_b) returns True when the two hosts' ports are in different VLANs and host_a's broadcast does not reach host_b, so a router with an interface in each VLAN would connect them. It returns False when the broadcast already arrives, and also when both ports are in the same VLAN but cut apart: a router does not join two halves of one VLAN, the trunk has to be fixed.

The sample tests check one switch, a trunk, a VLAN missing from one end's allowed list, a native VLAN mismatch, and four switches in a chain with a transit switch and a cable between two access ports.

Starter code · vlans.py

def find_host(network, name):
    """Return (switch, port) of the access port where the host `name` is plugged in."""
    for switch, ports in network.items():
        for number, port in ports.items():
            if port.get("host") == name:
                return switch, number
    raise KeyError(name)


def l2_reachable(network, host_a, host_b):
    """True when a broadcast that host_a sends reaches host_b without passing through a router."""
    # Replace this line with your code.
    return False


def needs_router(network, host_a, host_b):
    """True when the hosts are in different VLANs and cannot reach each other at layer 2."""
    # Replace this line with your code.
    return False
The sample tests · test_vlans.py
from vlans import l2_reachable, needs_router


def host(vlan, name):
    """An access port with a host plugged in."""
    return {"mode": "access", "vlan": vlan, "host": name}


def trunk(native, allowed, to):
    """A trunk port that leads to another switch's port."""
    return {"mode": "trunk", "native": native, "allowed": allowed, "to": to}


def link(vlan, to):
    """An access port that leads to another switch's port."""
    return {"mode": "access", "vlan": vlan, "to": to}


def test_one_switch():
    """keeps two VLANs of one switch apart"""
    net = {"SW1": {1: host(10, "pc-a"), 2: host(10, "pc-b"), 3: host(20, "pc-c")}}
    assert l2_reachable(net, "pc-a", "pc-b") and l2_reachable(net, "pc-b", "pc-a")
    assert not l2_reachable(net, "pc-a", "pc-c") and not l2_reachable(net, "pc-c", "pc-b")
    assert needs_router(net, "pc-a", "pc-c")
    assert not needs_router(net, "pc-a", "pc-b")


def test_trunk_carries_both_vlans():
    """carries each VLAN across a trunk, and keeps them apart there too"""
    net = {
        "SW1": {1: host(10, "pc-a"), 2: host(20, "pc-b"), 24: trunk(1, [1, 10, 20], ("SW2", 24))},
        "SW2": {1: host(10, "pc-c"), 2: host(20, "pc-d"), 24: trunk(1, [1, 10, 20], ("SW1", 24))},
    }
    assert l2_reachable(net, "pc-a", "pc-c") and l2_reachable(net, "pc-d", "pc-b")
    assert not l2_reachable(net, "pc-a", "pc-d")
    assert needs_router(net, "pc-a", "pc-d")
    assert not needs_router(net, "pc-b", "pc-d")


def test_vlan_missing_from_allowed_list():
    """cuts a VLAN in two when one end of the trunk does not allow it"""
    net = {
        "SW1": {1: host(10, "pc-a"), 2: host(20, "pc-b"), 24: trunk(1, [1, 10, 20], ("SW2", 24))},
        "SW2": {1: host(10, "pc-c"), 2: host(20, "pc-d"), 24: trunk(1, [1, 10], ("SW1", 24))},
    }
    assert l2_reachable(net, "pc-a", "pc-c")
    assert not l2_reachable(net, "pc-b", "pc-d") and not l2_reachable(net, "pc-d", "pc-b")
    assert not needs_router(net, "pc-b", "pc-d")  # one VLAN cut in two: fix the trunk, a router does not help
    assert needs_router(net, "pc-a", "pc-d")


def test_native_vlan_mismatch():
    """joins two VLANs, one way or both, when the ends of a trunk disagree about the native VLAN"""
    net = {
        "SW1": {1: host(1, "printer"), 2: host(10, "pc-a"), 24: trunk(1, [1, 10, 99], ("SW2", 24))},
        "SW2": {1: host(99, "camera"), 2: host(1, "pc-x"), 3: host(10, "pc-c"), 24: trunk(99, [1, 10, 99], ("SW1", 24))},
    }
    assert l2_reachable(net, "printer", "camera") and l2_reachable(net, "camera", "printer")
    assert not needs_router(net, "printer", "camera")
    assert l2_reachable(net, "pc-x", "printer") and not l2_reachable(net, "printer", "pc-x")
    assert l2_reachable(net, "pc-a", "pc-c")


def test_transit_switch_and_access_link():
    """passes VLANs through a switch with no hosts in them, and only one VLAN over a link between access ports"""
    net = {
        "SW1": {1: host(30, "cam-1"), 2: host(40, "pc-1"), 24: trunk(1, [30, 40], ("SW2", 23))},
        "SW2": {23: trunk(1, [30, 40], ("SW1", 24)), 24: trunk(1, [30, 40], ("SW3", 24))},
        "SW3": {1: host(30, "cam-3"), 2: host(40, "pc-3"), 24: trunk(1, [30, 40], ("SW2", 24)), 23: link(30, ("SW4", 24))},
        "SW4": {1: host(30, "cam-4"), 2: host(40, "pc-4"), 24: trunk(30, [30, 40], ("SW3", 23))},
    }
    assert l2_reachable(net, "cam-1", "cam-3") and l2_reachable(net, "pc-3", "pc-1")
    assert l2_reachable(net, "cam-1", "cam-4") and l2_reachable(net, "cam-4", "cam-1")
    assert not l2_reachable(net, "pc-1", "pc-4") and not l2_reachable(net, "pc-4", "pc-3")
    assert not needs_router(net, "pc-4", "pc-1")
    assert needs_router(net, "cam-4", "pc-3")
A hint

Search over pairs (switch, vlan): "a frame of this VLAN is inside this switch". Start from host A's switch and the VLAN of its port. For every port of that switch that has a "to", work out how the frame leaves (not at all, untagged, or with a tag), then which VLAN the far port puts it in (or whether it drops it), and add the new pair. Host B hears the broadcast when the pair (B's switch, the VLAN of B's port) has been reached.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Practise the stated switching contracts

Practice that adapts to you

Practise this lesson

17 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 How many forwarding contexts are in a stated set of VLANs 10, 20 and 30?

    Type a number.

    Show the answer: Question 1 of 17

    Answer: 3 VLANs

    The set contains three VLANs.

  2. Question 2 of 17 What VID is in the original PCP 5, DEI 0, VLAN 10 tag?

    Type a number.

    Show the answer: Question 2 of 17

    Answer: 10 VLAN ID

    The twelve-bit VID is ten.

  3. Question 3 of 17 Add a four-byte tag to a 64-byte frame including FCS. What is the new size?

    Type a number.

    Show the answer: Question 3 of 17

    Answer: 68 bytes

    Sixty-four plus four is sixty-eight.

  4. Question 4 of 17 How many bytes does one ordinary 802.1Q tag contribute?

    Type a number.

    Show the answer: Question 4 of 17

    Answer: 4 bytes

    TPID and TCI occupy two bytes each.

  5. Question 5 of 17 A correctly tagged and allowed VLAN 10 frame crosses the original trunk. Which VLAN ID remains identified?

    Type a number.

    Show the answer: Question 5 of 17

    Answer: 10 VLAN ID

    The explicit allowed tag identifies VLAN ten.

  6. Question 6 of 17 Two routed flows of 200 and 500 Mb/s use the stated router-on-a-stick trunk. What is switch-to-router load?

    Type a number.

    Show the answer: Question 6 of 17

    Answer: 700 Mb/s

    Two hundred plus five hundred is seven hundred.

  7. Question 7 of 17 What access-port number carries the original valid client request?

    Type a number.

    Show the answer: Question 7 of 17

    Answer: 2 port

    The original PC sends its request on port two.

  8. Question 8 of 17 The original valid PC sender has one applicable matching binding. How many matching bindings does that stated check use?

    Type a number.

    Show the answer: Question 8 of 17

    Answer: 1 binding

    It looks up the one binding for the sender IP.

  9. Question 9 of 17 What is the original MAX_MACS setting on an untrusted port?

    Type a number.

    Show the answer: Question 9 of 17

    Answer: 1 MAC address

    The original setting is one.

  10. Question 10 of 17 Four stated 10 Gb/s members are ideally balanced. What aggregate member capacity do they sum to?

    Type a number.

    Show the answer: Question 10 of 17

    Answer: 40 Gb/s

    Four times ten gives forty before other constraints.

  11. Question 11 of 17 How many mandatory data TLVs are chassis ID, port ID and TTL, before the end marker?

    Type a number.

    Show the answer: Question 11 of 17

    Answer: 3 TLVs

    The list has three mandatory data TLVs.

  12. Question 12 of 17 The VLAN ID of an 802.1Q tag has 12 bits. How many values can name an ordinary VLAN?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: 4,094, because 0 and 4095 are reserved

    Twelve bits give 4,096 values. VLAN ID 0 marks a tag that carries only a priority, and 4095 is reserved, so VLANs 1 to 4094 remain. That limit is one reason large data centres use VXLAN's 24-bit segment numbers.

  13. Question 13 of 17 A full-size Ethernet frame of 1,518 bytes, frame check sequence included, is tagged for a trunk. How many bytes long is the tagged frame?

    Type a number.

    Show the answer: Question 13 of 17

    Answer: 1522 bytes

    The tag adds 4 bytes between the source address and the EtherType and takes nothing away from the data, so the frame grows from 1,518 to 1,522 bytes, while the packet inside can still be at most 1,500 bytes. The frame check sequence is recomputed, because it covers the tag.

  14. Question 14 of 17 An untagged frame arrives on a trunk port whose native VLAN is 99 and whose allowed list includes 1, 10 and 99. Which VLAN does it join?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: VLAN 99, the native VLAN of the receiving port

    An untagged frame carries no record of its VLAN, so the receiving port puts it into its own native VLAN. That is why both ends must agree: if the sender's native VLAN were 1, the frame would move from VLAN 1 into VLAN 99.

  15. Question 15 of 17 DHCP snooping is on, and port 7 is an untrusted access port. Which of these messages arriving on port 7 does the switch drop?

    Choose every answer that is right.

    Show the answer: Question 15 of 17

    Answer:

    • A DHCPACK from a device on port 7
    • A DHCPOFFER from a device on port 7
    • A DHCPDISCOVER whose client hardware address differs from the frame's source address

    Offers, acknowledgements and refusals come only from servers, which sit behind trusted ports, so on an untrusted port they are dropped. A client's request passes, unless the address written inside it differs from the frame's source address, a check that Cisco switches make by default.

  16. Question 16 of 17 Through a router on a stick with a 1 Gb/s full-duplex trunk, VLAN 10 sends 300 Mb/s to VLAN 20 and VLAN 20 sends 500 Mb/s back to VLAN 10. How many megabits a second cross the trunk from the switch to the router?

    Type a number.

    Show the answer: Question 16 of 17

    Answer: 800 Mb/s

    Every routed packet goes up the trunk to the router and comes back down, so both flows travel from the switch to the router: 300 + 500 = 800 Mb/s, and the same 800 Mb/s come back the other way. All the traffic between VLANs, in both directions together, has to fit in one direction of the trunk.

  17. Question 17 of 17 Under this lesson's per-flow member-selection policy, a server uses an LACP bundle of four 10 Gb/s links. How fast can one TCP connection go?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: At most 10 Gb/s, because all frames of one connection use the same member link

    This policy maps the connection's selected header fields to one member, preserving order. Adding port numbers spreads distinct connections rather than splitting this flow; LACP negotiates membership and does not prescribe that hash policy.

Interview questions

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    After a new access switch is installed, a printer in VLAN 1 on the old switch starts receiving broadcasts from cameras in VLAN 99 on the new one, while VLANs 10 and 20 work normally. What is the most likely mistake on the trunk, how do you confirm it, and how do you make the trunk safe?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    In a college computer lab, some PCs suddenly get addresses in 192.168.0.0/24 instead of the lab's 10.20.0.0/22 and lose all access, while others are fine. A home Wi-Fi router had been plugged into a desk socket. How do you find it from the switches, and what do you configure so this cannot happen again?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    An office routes between four VLANs with a router on a stick over one 1 Gb/s trunk. Every night, servers in VLAN 30 back up to storage in VLAN 40 at about 700 Mb/s, and staff then find the internet very slow. Explain the bottleneck with numbers and compare two ways to fix it.

    Model answer, Pro. First step and follow-ups

What is the native VLAN of a trunk, and why do many networks move it off VLAN 1? The native VLAN classifies admitted untagged ingress. Under the stated trunk egress policy, frames of that VLAN leave untagged. On many switches it is VLAN 1 by default. The trunk cannot tell which VLAN an untagged frame came from, so both ends must agree on the native VLAN; if they do not, frames quietly move from one VLAN into another. Networks therefore choose a native VLAN that no device uses, or tag the native VLAN and configure untagged admission separately, so untagged traffic cannot reach unintended hosts. Ports facing users are kept in access mode for the same reason.

Key takeaways

  • A VLAN is a separate broadcast domain on shared switches; a VLAN usually gets one IPv4 subnet, and VLAN IDs run from 1 to 4094.
  • An 802.1Q tag is 4 bytes after the source address: TPID 0x8100, a 3-bit priority, a 1-bit DEI and a 12-bit VLAN ID. It takes the largest frame to 1,522 bytes, and adding or removing it changes the frame check sequence.
  • Access ports carry one VLAN untagged; trunks carry several VLANs tagged, plus one untagged native VLAN, which must match at both ends.
  • Traffic between VLANs is routed: by a router on a stick, which crosses its trunk twice for every routed packet, or by a layer-3 switch with one virtual interface per VLAN.
  • Port security limits addresses per port; DHCP snooping lets only trusted ports answer DHCP and records bindings; dynamic ARP inspection and IP source guard check traffic against those bindings.
  • LACP bundles parallel links into one logical port, but each connection uses one link; LLDP tells each device who its neighbours are.

Terms in this lesson

ARP inspection binding
An applicable trusted address association used by an ARP validation policy to test whether a sender matches permitted information.
DHCP snooping binding
A learned association of a leased IP address with client, VLAN and port information under a switch's DHCP snooping policy.
Link aggregation member
One physical link in a logical link aggregation, selected for eligible traffic by the implementation's distribution policy.
Native VLAN classification
The configured association used to classify eligible untagged ingress frames, distinct from the port's tagged and untagged egress rules.
Trunk allowed list
A configured set of VLANs admitted by a trunk policy; delivery depends on the policies and state at the relevant boundaries.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.