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.
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
What you’ll learn · Before you start
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
On this page
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 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.
- 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.
- 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.
- Access ports send and receive untagged frames, so the hosts never see a tag.
- 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.
- 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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original VLAN 10 and VLAN 20 hosts share hardware but retain separate forwarding contexts.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- The original comparison separates VLAN 10 and VLAN 20.
Checkpoint
Check: A VLAN bounds layer-two broadcast delivery
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original PCP 5/DEI 0/VID 10 produces TCI 0xA00A; 12 bits leave IDs 1–4094 ordinary.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- The twelve-bit VLAN ID field leaves ordinary IDs 1 through 4094.
Checkpoint
Check: An 802.1Q tag carries priority and a VLAN ID
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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 67 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 24 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 719 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 184 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 16 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
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.
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original full-size 1,518 becomes 1,522 including FCS; recompute FCS over tagged bytes.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- The original full-size 1,518-byte frame gains a four-byte tag.
Checkpoint
Check: A tag adds four bytes to the original frame
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Untagged access joins assigned VLAN; trunk tag/native policy plus both allowed lists determines delivery.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- The stated cable crosses sending and receiving trunk allowed lists.
Checkpoint
Check: Ingress classification and egress policy must agree
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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 66 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 9 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 21 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 719 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 191 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 11 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original SW1 native 1 and SW2 native 99 move printer/camera broadcasts; tagged VLANs 10 and 20 remain distinct.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- The original receiving SW2 native VLAN is 99.
Checkpoint
Check: An untagged frame takes the receiving native VLAN
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:
- 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.
- SW1 tags the frame 10 and sends it up the trunk to the router, which receives it on eth0.10.
- 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.
- 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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original 300 and 500 Mb/s flows put 800 Mb/s on switch-to-router and 800 on the reverse direction.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- 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
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original untrusted OFFER is dropped; trusted ACK can create the stated binding after a client request.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- 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
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original PC binding versus unbound or mismatched sender; explicitly bound demonstration lease/time limitations.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- 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
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, 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 102 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 8 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 4 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 8 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 21 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 716 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 187 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 13 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original MAX_MACS = 1, ARP_LIMIT = 15 and twenty-message burst; original integer-second buckets remain declared.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- 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
A member-selection policy maps a flow to one link
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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original four 10 Gb/s links and MAC/IP/port CRC policies; do not claim every platform uses the same fields.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- 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
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Original chassis, port and TTL fields describe one link neighbour rather than a routed discovery graph.
- This scene follows the stated original model and keeps its program, data and workload. Device policy and topology assumptions remain explicit.
- LLDP describes the direct link neighbour at the port.
Checkpoint
Check: LLDP describes the directly attached neighbour
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:
- Leaving an access port, it is sent untagged if the port's VLAN is
v, and not sent otherwise. - Leaving a trunk, it is not sent if
vis not allowed, sent untagged ifvis the native VLAN, and tagged withvotherwise. - Entering an access port, an untagged frame joins the port's VLAN and a tagged frame is dropped.
- 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)returnsTruewhen a broadcast thathost_asends reacheshost_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)returnsTruewhen the two hosts' ports are in different VLANs andhost_a's broadcast does not reachhost_b, so a router with an interface in each VLAN would connect them. It returnsFalsewhen 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
Interview questions
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
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?
-
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?
-
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.
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
- IEEE 802.1Q-2022: Bridges and Bridged Networks (IEEE Standards Association)
- IEEE 802.1AX-2020: Link Aggregation (IEEE Standards Association)
- IEEE 802.1AB-2026: Station and Media Access Control Connectivity Discovery (IEEE Standards Association)
- IEEE 802 Numbers: EtherTypes (Internet Assigned Numbers Authority (IANA))
- RFC 9542: IANA Considerations and IETF Protocol and Documentation Usage for IEEE 802 Parameters (RFC Editor (IETF))
- RFC 7348: Virtual eXtensible Local Area Network (VXLAN) (RFC Editor (IETF))
- RFC 826: An Ethernet Address Resolution Protocol (RFC Editor (IETF))
- RFC 7513: Source Address Validation Improvement (SAVI) Solution for DHCP (RFC Editor (IETF))
- Linux source: include/linux/if_vlan.h (the tag fields and frame sizes) (The Linux kernel project)
- ip-link(8): network device configuration, VLAN devices and bridge options (iproute2 7.2.0) (iproute2 project (Debian manpages))
- bridge(8): show and manipulate bridge addresses and devices (iproute2 7.2.0) (iproute2 project (Debian manpages))
- Ethernet Bridging (Linux kernel documentation) (The Linux Kernel documentation)
- Linux Ethernet Bonding Driver HOWTO (Linux kernel documentation) (The Linux Kernel documentation)
- lldpcli(8): control the LLDP daemon (lldpd project (Debian manpages))
- Catalyst 9300 VLAN Configuration Guide: Configuring VLANs (Cisco)
- Catalyst 9300 VLAN Configuration Guide: Configuring VLAN Trunks (Cisco)
- Catalyst 9300 VLAN Configuration Guide: Configuring Voice VLANs (Cisco)
- Catalyst 9300 Layer 2 Configuration Guide: Configuring IEEE 802.1Q Tunneling (native VLAN tagging) (Cisco)
- Best Practices for Catalyst 6500/6000 Series and Catalyst 4500/4000 Series Switches Running Cisco IOS Software (Cisco)
- Catalyst 9300 IP Addressing Services Configuration Guide: Configuring DHCP (DHCP snooping) (Cisco)
- Catalyst 9300 Security Configuration Guide: Configuring Dynamic ARP Inspection (Cisco)
- Catalyst 9300 Security Configuration Guide: Configuring IP Source Guard (Cisco)
- Catalyst 9300 Security Configuration Guide: Port Security (Cisco)
- Catalyst 9300 Layer 2 Configuration Guide: Configuring EtherChannels (Cisco)
- Catalyst 9300 Interface and Hardware Configuration Guide: Configuring LLDP, LLDP-MED, and Wired Location Service (Cisco)
- CCNA 200-301 v1.1 exam topics (Cisco)
- GATE 2027 syllabus: Computer Science and Information Technology (IIT Madras)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress