Your country

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

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

Computer Networks  Module 5 – Switching, spanning tree and VLANs

How switches learn, flood and forward

How an Ethernet switch builds its MAC address table, when it forwards, floods or filters a frame, why entries age out, and how hubs and routers differ.

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

What you will learn

  • Explain how a learning switch builds its MAC address table and forwards, floods or filters frames
  • Distinguish hubs, switches and routers by collision and broadcast domains
  • Trace a frame through a multi-switch network given each switch's MAC table
  • Choose an ageing time by weighing flooding against stale entries

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how a learning switch builds its MAC address table and forwards, floods or filters frames
  • Distinguish hubs, switches and routers by collision and broadcast domains
  • Trace a frame through a multi-switch network given each switch's MAC table
  • Choose an ageing time by weighing flooding against stale entries

Before you start

On this page

An Ethernet switch decides where each frame goes by its destination MAC address, and it learns where addresses are by reading valid source addresses on learning-enabled ports. For an admitted frame, the switch first records the source address against that port in its MAC address table (IEEE 802.1Q calls it the filtering database). Then it looks up the destination. If the table places the destination on another port, the switch forwards the frame out of that port alone; if the destination sits on the very port the frame came in on, it filters the frame, which means it drops it; and if the destination is unknown, a broadcast, or a multicast group it has no information about under the stated flood policy, it floods to eligible ports in that VLAN, excluding ingress. Dynamic entries expire after an ageing time, 300 seconds by default on a Linux bridge and on Cisco’s Catalyst 9300 switches, so the table keeps up with hosts that move or fall silent.

A source address teaches the ingress port

The valid frames in this model follow the same three steps, in this order:

  1. Source address learning: The update that records the port and time where a valid source address was most recently observed in a bridge forwarding context. Record “the frame’s source address is reachable through the port it arrived on, as of now”. A new address gets an entry, a known address has its timer reset, and an address that shows up on a different port is moved to that port.
  2. Look up the destination address. Only an entry that has not aged out counts.
  3. Decide. Forward to the port in the entry, filter if that port is the arrival port, and flood if there is no usable entry or the destination is a group address.

The program below is that algorithm as a short class, fed with eight frames on a five-port switch. The hosts use MAC addresses from the block reserved for documentation, 00:00:5e:00:53:00 to 00:00:5e:00:53:ff (RFC 9542), and each gets a letter so the output stays readable.

A learning switch deciding eight frames Python · original-learning_switch/learning_switch.py
"""A learning switch: learn the source address, then forward, flood or filter by the destination."""

BROADCAST = "ff:ff:ff:ff:ff:ff"
AGEING = 300  # seconds an entry lives after its address was last seen as a source

# Documentation addresses (00:00:5e:00:53:xx), one letter per host
NAMES = {f"00:00:5e:00:53:0{c.lower()}": c for c in "ABCDE"} | {BROADCAST: "all"}
MAC = {name: mac for mac, name in NAMES.items()}


class LearningSwitch:
    def __init__(self, ports, ageing=AGEING):
        self.ports = ports
        self.ageing = ageing
        self.table = {}  # MAC address -> (port, time it was last seen as a source)

    def lookup(self, mac, now):
        """The port of a MAC address, or None if it is unknown or its entry has aged out."""
        entry = self.table.get(mac)
        if entry is not None and now - entry[1] < self.ageing:
            return entry[0]
        return None

    def receive(self, port, src, dst, now):
        """Learn from the frame's source, then decide what to do with it."""
        before = self.lookup(src, now)
        self.table[src] = (port, now)
        if before is None:
            learned = f"learn {NAMES[src]} on {port}"
        elif before != port:
            learned = f"move {NAMES[src]} {before}->{port}"
        else:
            learned = f"refresh {NAMES[src]}"
        out = None if dst == BROADCAST else self.lookup(dst, now)
        if out == port:
            return learned, "filter: already on that side", []
        if out is not None:
            return learned, f"forward to {out}", [out]
        flood = [p for p in self.ports if p != port]
        reason = "broadcast" if dst == BROADCAST else "unknown"
        return learned, f"{reason}: flood to {','.join(map(str, flood))}", flood


switch = LearningSwitch(ports=[1, 2, 3, 4, 5])
events = [  # (time in seconds, ingress port, source, destination)
    (0, 1, "A", "B"),
    (1, 2, "B", "A"),
    (2, 1, "A", "B"),
    (3, 3, "C", "all"),
    (4, 4, "D", "C"),
    (5, 4, "E", "D"),  # D and E sit behind a small switch on port 4
    (20, 5, "B", "A"),  # B's laptop is now plugged into port 5
    (400, 3, "C", "A"),  # A has said nothing since t=2
]
print(f"{'t (s)':>5}  {'port':>4}  {'frame':<8}  {'learning':<14}  decision")
for now, port, src, dst in events:
    learned, decision, _ = switch.receive(port, MAC[src], MAC[dst], now)
    print(f"{now:>5}  {port:>4}  {src + ' -> ' + dst:<8}  {learned:<14}  {decision}")

print("\nMAC table at t=400 s:")
for mac, (port, seen) in sorted(switch.table.items(), key=lambda item: NAMES[item[0]]):
    state = "aged out" if 400 - seen >= AGEING else f"age {400 - seen} s"
    print(f"  {mac} ({NAMES[mac]})  port {port}  {state}")

Output

t (s)  port  frame     learning        decision
    0     1  A -> B    learn A on 1    unknown: flood to 2,3,4,5
    1     2  B -> A    learn B on 2    forward to 1
    2     1  A -> B    refresh A       forward to 2
    3     3  C -> all  learn C on 3    broadcast: flood to 1,2,4,5
    4     4  D -> C    learn D on 4    forward to 3
    5     4  E -> D    learn E on 4    filter: already on that side
   20     5  B -> A    move B 2->5     forward to 1
  400     3  C -> A    learn C on 3    unknown: flood to 1,2,4,5

MAC table at t=400 s:
  00:00:5e:00:53:0a (A)  port 1  aged out
  00:00:5e:00:53:0b (B)  port 5  aged out
  00:00:5e:00:53:0c (C)  port 3  age 0 s
  00:00:5e:00:53:0d (D)  port 4  aged out
  00:00:5e:00:53:0e (E)  port 4  aged out

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

One program · 14 languages Learn and age the original eight switch frames O(p) per flood for p eligible ports

Python 3.14 · main.py

table: dict[str, tuple[int, int]] = {}


def lookup(address: str, now: int) -> int | None:
    entry = table.get(address)
    return entry[0] if entry is not None and now - entry[1] < 300 else None


def receive(
    now: int, port: int, source: str, destination: str
) -> tuple[str, list[int]]:
    table[source] = (port, now)
    out = None if destination == "all" else lookup(destination, now)
    if out == port:
        return "filter", []
    if out is not None:
        return "forward", [out]
    return "broadcast" if destination == "all" else "unknown", [
        p for p in range(1, 6) if p != port
    ]


events = [
    (0, 1, "A", "B"),
    (1, 2, "B", "A"),
    (2, 1, "A", "B"),
    (3, 3, "C", "all"),
    (4, 4, "D", "C"),
    (5, 4, "E", "D"),
    (20, 5, "B", "A"),
    (400, 3, "C", "A"),
]
for now, port, source, destination in events:
    decision, ports = receive(now, port, source, destination)
    shown = ",".join(map(str, ports)) or "none"
    print(f"t={now} {source}->{destination} {decision} ports={shown}")
for source, (port, seen) in sorted(table.items()):
    usable = str(lookup(source, 400) is not None).lower()
    print(f"{source} port={port} age={400 - seen} usable={usable}")

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 table = new Map();
function lookup(address, now) {
  const entry = table.get(address);
  return entry !== undefined && now - entry[1] < 300 ? entry[0] : null;
}
const events = [
  [0, 1, "A", "B"],
  [1, 2, "B", "A"],
  [2, 1, "A", "B"],
  [3, 3, "C", "all"],
  [4, 4, "D", "C"],
  [5, 4, "E", "D"],
  [20, 5, "B", "A"],
  [400, 3, "C", "A"],
];
for (const [now, port, source, destination] of events) {
  table.set(source, [port, now]);
  const out = destination === "all" ? null : lookup(destination, now);
  const decision =
    out === port
      ? "filter"
      : out !== null
        ? "forward"
        : destination === "all"
          ? "broadcast"
          : "unknown";
  const ports =
    out === port ? [] : out !== null ? [out] : [1, 2, 3, 4, 5].filter((p) => p !== port);
  console.log(`t=${now} ${source}->${destination} ${decision} ports=${ports.join(",") || "none"}`);
}
for (const source of [...table.keys()].sort()) {
  const [port, seen] = table.get(source);
  console.log(`${source} port=${port} age=${400 - seen} usable=${lookup(source, 400) !== null}`);
}

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.HashMap;
import java.util.TreeSet;

public class Main {
  record Entry(int port, int seen) {}

  record Event(int now, int port, String source, String destination) {}

  static final HashMap<String, Entry> table = new HashMap<>();

  static Integer lookup(String source, int now) {
    Entry e = table.get(source);
    return e != null && now - e.seen() < 300 ? e.port() : null;
  }

  public static void main(String[] args) {
    Event[] events = {
      new Event(0, 1, "A", "B"),
      new Event(1, 2, "B", "A"),
      new Event(2, 1, "A", "B"),
      new Event(3, 3, "C", "all"),
      new Event(4, 4, "D", "C"),
      new Event(5, 4, "E", "D"),
      new Event(20, 5, "B", "A"),
      new Event(400, 3, "C", "A")
    };
    for (Event e : events) {
      table.put(e.source(), new Entry(e.port(), e.now()));
      Integer out = e.destination().equals("all") ? null : lookup(e.destination(), e.now());
      String decision =
          out != null && out == e.port()
              ? "filter"
              : out != null ? "forward" : e.destination().equals("all") ? "broadcast" : "unknown";
      var ports = new StringBuilder();
      for (int p = 1; p <= 5; p++) {
        if (out != null ? p == out && out != e.port() : p != e.port()) {
          if (ports.length() > 0) ports.append(',');
          ports.append(p);
        }
      }
      String shown = ports.length() == 0 ? "none" : ports.toString();
      System.out.println(
          "t="
              + e.now()
              + " "
              + e.source()
              + "->"
              + e.destination()
              + " "
              + decision
              + " ports="
              + shown);
    }
    for (String source : new TreeSet<>(table.keySet())) {
      Entry e = table.get(source);
      System.out.println(
          source
              + " port="
              + e.port()
              + " age="
              + (400 - e.seen())
              + " usable="
              + (lookup(source, 400) != null));
    }
  }
}

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 <map>
#include <optional>
#include <print>
#include <string>
#include <tuple>
#include <vector>
using Table = std::map<std::string, std::pair<int, int>>;
std::optional<int> lookup(const Table &table, const std::string &source,
                          int now) {
  const auto e = table.find(source);
  if (e == table.end() || now - e->second.second >= 300)
    return std::nullopt;
  return e->second.first;
}
int main() {
  Table table;
  const std::vector<std::tuple<int, int, std::string, std::string>> events = {
      {0, 1, "A", "B"},   {1, 2, "B", "A"},  {2, 1, "A", "B"},
      {3, 3, "C", "all"}, {4, 4, "D", "C"},  {5, 4, "E", "D"},
      {20, 5, "B", "A"},  {400, 3, "C", "A"}};
  for (const auto &[now, port, source, destination] : events) {
    table[source] = {port, now};
    const auto out =
        destination == "all" ? std::nullopt : lookup(table, destination, now);
    const auto decision = out && *out == port    ? "filter"
                          : out                  ? "forward"
                          : destination == "all" ? "broadcast"
                                                 : "unknown";
    std::string ports;
    for (int p = 1; p <= 5; p++)
      if (out ? p == *out && *out != port : p != port) {
        if (!ports.empty())
          ports += ",";
        ports += std::to_string(p);
      }
    std::println("t={} {}->{} {} ports={}", now, source, destination, decision,
                 ports.empty() ? "none" : ports);
  }
  for (const auto &[source, e] : table) {
    std::println("{} port={} age={} usable={}", source, e.first, 400 - e.second,
                 lookup(table, source, 400).has_value());
  }
}

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

using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Dictionary<string, (int Port, int Seen)> table = new();
int? Lookup(string source, int now) =>
    table.TryGetValue(source, out var entry) && now - entry.Seen < 300 ? entry.Port : null;
(int Now, int Port, string Source, string Destination)[] events = [(0,1,"A","B"),
    (1,2,"B","A"),(2,1,"A","B"),(3,3,"C","all"),(4,4,"D","C"),(5,4,"E","D"),
    (20,5,"B","A"),(400,3,"C","A")];
foreach (var (now, port, source, destination) in events)
{
    table[source] = (port, now);
    int? output = destination == "all" ? null : Lookup(destination, now);
    string decision = output == port ? "filter" : output is not null ? "forward"
        : destination == "all" ? "broadcast" : "unknown";
    var ports = Enumerable.Range(1, 5).Where(p =>
        output is not null ? p == output && output != port : p != port);
    string shown = string.Join(",", ports);
    string result = shown == "" ? "none" : shown;
    Console.WriteLine($"t={now} {source}->{destination} {decision} ports={result}");
}
foreach (string source in table.Keys.Order(StringComparer.Ordinal))
{
    var (port, seen) = table[source];
    string usable = (Lookup(source, 400) is not null).ToString().ToLowerInvariant();
    Console.WriteLine($"{source} port={port} age={400 - seen} usable={usable}");
}

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 ports[5], seen[5];
static int lookup(int source, int now) {
  return source >= 0 && ports[source] && now - seen[source] < 300
             ? ports[source]
             : 0;
}
int main(void) {
  const int times[] = {0, 1, 2, 3, 4, 5, 20, 400},
            ingress[] = {1, 2, 1, 3, 4, 4, 5, 3};
  const int sources[] = {0, 1, 0, 2, 3, 4, 1, 2},
            destinations[] = {1, 0, 1, -1, 2, 3, 0, 0};
  for (int i = 0; i < 8; i++) {
    const int now = times[i], port = ingress[i], src = sources[i],
              dst = destinations[i];
    ports[src] = port;
    seen[src] = now;
    const int out = lookup(dst, now);
    const char *decision = out == port ? "filter"
                           : out       ? "forward"
                           : dst < 0   ? "broadcast"
                                       : "unknown";
    if (dst < 0)
      printf("t=%d %c->all %s ports=", now, 'A' + src, decision);
    else
      printf("t=%d %c->%c %s ports=", now, 'A' + src, 'A' + dst, decision);
    int count = 0;
    for (int p = 1; p <= 5; p++)
      if (out ? p == out && out != port : p != port) {
        printf("%s%d", count++ ? "," : "", p);
      }
    if (!count)
      printf("none");
    putchar('\n');
  }
  for (int i = 0; i < 5; i++)
    printf("%c port=%d age=%d usable=%s\n", 'A' + i, ports[i], 400 - seen[i],
           lookup(i, 400) ? "true" : "false");
  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);

/** @param array<string, array{int, int}> $table */
function lookup(array $table, string $source, int $now): ?int
{
    $entry = $table[$source] ?? null;
    return $entry !== null && $now - $entry[1] < 300 ? $entry[0] : null;
}
$table = [];
$events = [[0,1,'A','B'],[1,2,'B','A'],[2,1,'A','B'],[3,3,'C','all'],
    [4,4,'D','C'],[5,4,'E','D'],[20,5,'B','A'],[400,3,'C','A']];
foreach ($events as [$now, $port, $source, $destination]) {
    $table[$source] = [$port, $now];
    $out = $destination === 'all' ? null : lookup($table, $destination, $now);
    $decision = $out === $port ? 'filter' : ($out !== null ? 'forward'
        : ($destination === 'all' ? 'broadcast' : 'unknown'));
    $ports = array_filter(range(1, 5), static fn(int $p): bool
        => $out !== null ? $p === $out && $out !== $port : $p !== $port);
    $shown = implode(',', $ports) ?: 'none';
    echo "t={$now} {$source}->{$destination} {$decision} ports={$shown}\n";
}
ksort($table);
foreach ($table as $source => [$port, $seen]) {
    $age = 400 - $seen;
    $usable = lookup($table, $source, 400) !== null ? 'true' : 'false';
    echo "{$source} port={$port} age={$age} usable={$usable}\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

use std::collections::BTreeMap;
fn lookup(table: &BTreeMap<&str, (i32, i32)>, source: &str, now: i32) -> Option<i32> {
    table
        .get(source)
        .and_then(|&(port, seen)| (now - seen < 300).then_some(port))
}
fn main() {
    let mut table = BTreeMap::new();
    let events = [
        (0, 1, "A", "B"),
        (1, 2, "B", "A"),
        (2, 1, "A", "B"),
        (3, 3, "C", "all"),
        (4, 4, "D", "C"),
        (5, 4, "E", "D"),
        (20, 5, "B", "A"),
        (400, 3, "C", "A"),
    ];
    for (now, port, source, destination) in events {
        table.insert(source, (port, now));
        let out = if destination == "all" {
            None
        } else {
            lookup(&table, destination, now)
        };
        let decision = match out {
            Some(p) if p == port => "filter",
            Some(_) => "forward",
            _ if destination == "all" => "broadcast",
            _ => "unknown",
        };
        let ports = (1..=5)
            .filter(|&p| match out {
                Some(value) => p == value && value != port,
                None => p != port,
            })
            .map(|p| p.to_string())
            .collect::<Vec<_>>()
            .join(",");
        let shown = if ports.is_empty() { "none" } else { &ports };
        println!("t={now} {source}->{destination} {decision} ports={shown}");
    }
    for (source, (port, seen)) in &table {
        let age = 400 - seen;
        let usable = lookup(&table, source, 400).is_some();
        println!("{source} port={port} age={age} usable={usable}");
    }
}

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 table = new Map<string, [number, number]>();
function lookup(address: string, now: number): number | null {
  const entry = table.get(address);
  return entry !== undefined && now - entry[1] < 300 ? entry[0] : null;
}
const events: [number, number, string, string][] = [
  [0, 1, "A", "B"],
  [1, 2, "B", "A"],
  [2, 1, "A", "B"],
  [3, 3, "C", "all"],
  [4, 4, "D", "C"],
  [5, 4, "E", "D"],
  [20, 5, "B", "A"],
  [400, 3, "C", "A"],
];
for (const [now, port, source, destination] of events) {
  table.set(source, [port, now]);
  const out = destination === "all" ? null : lookup(destination, now);
  const decision =
    out === port
      ? "filter"
      : out !== null
        ? "forward"
        : destination === "all"
          ? "broadcast"
          : "unknown";
  const ports =
    out === port ? [] : out !== null ? [out] : [1, 2, 3, 4, 5].filter((p) => p !== port);
  console.log(`t=${now} ${source}->${destination} ${decision} ports=${ports.join(",") || "none"}`);
}
for (const source of [...table.keys()].sort()) {
  const entry = table.get(source);
  if (entry === undefined) continue;
  const [port, seen] = entry;
  console.log(`${source} port=${port} age=${400 - seen} usable=${lookup(source, 400) !== null}`);
}

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"
	"sort"
	"strings"
)

type entry struct{ port, seen int }
type event struct {
	now, port           int
	source, destination string
}

func lookup(table map[string]entry, source string, now int) (int, bool) {
	e, ok := table[source]
	return e.port, ok && now-e.seen < 300
}
func main() {
	table := map[string]entry{}
	events := []event{{0, 1, "A", "B"}, {1, 2, "B", "A"}, {2, 1, "A", "B"}, {3, 3, "C", "all"},
		{4, 4, "D", "C"}, {5, 4, "E", "D"}, {20, 5, "B", "A"}, {400, 3, "C", "A"}}
	for _, e := range events {
		table[e.source] = entry{e.port, e.now}
		out, known := lookup(table, e.destination, e.now)
		if e.destination == "all" {
			known = false
		}
		decision := "unknown"
		switch {
		case known && out == e.port:
			decision = "filter"
		case known:
			decision = "forward"
		case e.destination == "all":
			decision = "broadcast"
		}
		ports := []string{}
		for p := 1; p <= 5; p++ {
			if (known && p == out && out != e.port) || (!known && p != e.port) {
				ports = append(ports, fmt.Sprint(p))
			}
		}
		shown := strings.Join(ports, ",")
		if shown == "" {
			shown = "none"
		}
		fmt.Printf("t=%d %s->%s %s ports=%s\n", e.now, e.source, e.destination, decision, shown)
	}
	keys := []string{}
	for source := range table {
		keys = append(keys, source)
	}
	sort.Strings(keys)
	for _, source := range keys {
		e := table[source]
		_, usable := lookup(table, source, 400)
		fmt.Printf("%s port=%d age=%d usable=%t\n", source, e.port, 400-e.seen, usable)
	}
}

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 table = [String: (Int, Int)]()
func lookup(_ source: String, now: Int) -> Int? {
  guard let entry = table[source], now - entry.1 < 300 else { return nil }
  return entry.0
}
let events = [
  (0, 1, "A", "B"), (1, 2, "B", "A"), (2, 1, "A", "B"), (3, 3, "C", "all"),
  (4, 4, "D", "C"), (5, 4, "E", "D"), (20, 5, "B", "A"), (400, 3, "C", "A"),
]
for (now, port, source, destination) in events {
  table[source] = (port, now)
  let out = destination == "all" ? nil : lookup(destination, now: now)
  let decision =
    out == port
    ? "filter"
    : out != nil
      ? "forward"
      : destination == "all" ? "broadcast" : "unknown"
  let ports = (1...5).filter { p in out.map { p == $0 && $0 != port } ?? (p != port) }
    .map { String($0) }.joined(separator: ",")
  print("t=\(now) \(source)->\(destination) \(decision) ports=\(ports.isEmpty ? "none":ports)")
}
for source in table.keys.sorted() {
  guard let entry = table[source] else { continue }
  print("\(source) port=\(entry.0) age=\(400-entry.1) usable=\(lookup(source,now:400) != nil)")
}

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 lookup(table, source, now)
  entry = table[source]
  entry && now - entry[1] < 300 ? entry[0] : nil
end

table = {}
events = [[0, 1, 'A', 'B'], [1, 2, 'B', 'A'], [2, 1, 'A', 'B'], [3, 3, 'C', 'all'],
          [4, 4, 'D', 'C'], [5, 4, 'E', 'D'], [20, 5, 'B', 'A'], [400, 3, 'C', 'A']]
events.each do |now, port, source, destination|
  table[source] = [port, now]
  output = destination == 'all' ? nil : lookup(table, destination, now)
  decision = if output == port
               'filter'
             elsif output
               'forward'
             else
               destination == 'all' ? 'broadcast' : 'unknown'
             end
  ports = (1..5).select do |value|
    output ? value == output && output != port : value != port
  end.join(',')
  puts "t=#{now} #{source}->#{destination} #{decision} ports=#{ports.empty? ? 'none' : ports}"
end
table.keys.sort.each do |source|
  port, seen = table[source]
  puts "#{source} port=#{port} age=#{400 - seen} usable=#{!lookup(table, source, 400).nil?}"
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 table = mutableMapOf<String, Pair<Int, Int>>()

fun lookup(
    source: String,
    now: Int,
): Int? = table[source]?.takeIf { now - it.second < 300 }?.first

data class Event(
    val now: Int,
    val port: Int,
    val source: String,
    val destination: String,
)

fun main() {
    val events =
        listOf(
            Event(0, 1, "A", "B"),
            Event(1, 2, "B", "A"),
            Event(2, 1, "A", "B"),
            Event(3, 3, "C", "all"),
            Event(4, 4, "D", "C"),
            Event(5, 4, "E", "D"),
            Event(20, 5, "B", "A"),
            Event(400, 3, "C", "A"),
        )
    for ((now, port, source, destination) in events) {
        table[source] = port to now
        val output = if (destination == "all") null else lookup(destination, now)
        val decision =
            when {
                output == port -> "filter"
                output != null -> "forward"
                destination == "all" -> "broadcast"
                else -> "unknown"
            }
        val ports =
            (1..5)
                .filter {
                    if (output != null) it == output && output != port else it != port
                }.joinToString(",")
                .ifEmpty { "none" }
        println("t=$now $source->$destination $decision ports=$ports")
    }
    for (source in table.keys.sorted()) {
        val (port, seen) = table.getValue(source)
        println("$source port=$port age=${400 - seen} usable=${lookup(source,400) != null}")
    }
}

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

final table = <String, (int, int)>{};
int? lookup(String source, int now) {
  final entry = table[source];
  return entry != null && now - entry.$2 < 300 ? entry.$1 : null;
}

void main() {
  final events = [
    (0, 1, 'A', 'B'),
    (1, 2, 'B', 'A'),
    (2, 1, 'A', 'B'),
    (3, 3, 'C', 'all'),
    (4, 4, 'D', 'C'),
    (5, 4, 'E', 'D'),
    (20, 5, 'B', 'A'),
    (400, 3, 'C', 'A'),
  ];
  for (final (now, port, source, destination) in events) {
    table[source] = (port, now);
    final output = destination == 'all' ? null : lookup(destination, now);
    final decision = output == port
        ? 'filter'
        : output != null
        ? 'forward'
        : destination == 'all'
        ? 'broadcast'
        : 'unknown';
    final ports = [1, 2, 3, 4, 5]
        .where(
          (p) => output != null ? p == output && output != port : p != port,
        )
        .join(',');
    print(
      't=$now $source->$destination $decision ports=${ports.isEmpty ? 'none' : ports}',
    );
  }
  final keys = table.keys.toList()..sort();
  for (final source in keys) {
    final entry = table[source];
    if (entry == null) continue;
    final (port, seen) = entry;
    print(
      '$source port=$port age=${400 - seen} usable=${lookup(source, 400) != null}',
    );
  }
}

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

t=0 A->B unknown ports=2,3,4,5
t=1 B->A forward ports=1
t=2 A->B forward ports=2
t=3 C->all broadcast ports=1,2,4,5
t=4 D->C forward ports=3
t=5 E->D filter ports=none
t=20 B->A forward ports=1
t=400 C->A unknown ports=1,2,4,5
A port=1 age=398 usable=false
B port=5 age=380 usable=false
C port=3 age=0 usable=true
D port=4 age=396 usable=false
E port=4 age=395 usable=false

Follow A entering port 1, then learn its source location before selecting ports 2, 3, 4 and 5 for the unknown B destination.

Interactive · stage by stage A source address teaches the ingress port Follow A entering port 1, then learn its source location before selecting ports 2, 3, 4 and 5 for the unknown B destination.
A source address teaches the ingress port, step 3 of 3. Showing Arrival, Learn source, Unknown B. In focus: Unknown B.ArrivalA on port 1Learn sourceA maps to port 1Unknown BPorts 2, 3, 4 and 5

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

Every step as text
  1. The original first frame enters port 1 with source A and destination B.
  2. The table records the arrival port from A, before deciding where the frame leaves.
  3. B has no usable entry, so this stated learning model floods to every other eligible port.

Checkpoint

Check: A source address teaches the ingress port

  1. Question 1 of 2 Which address teaches the ingress port?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The source address of a valid received frame

    Source learning records where the sender was seen. A destination field does not reveal the destination's port.

  2. Question 2 of 2 Five ports exist and the frame arrives on port 1. How many output ports receive the original unknown unicast?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 4 ports

    Exclude the one ingress port from the five stated ports.

A destination lookup selects forward, filter or flood

Read the decisions line by line:

  • t = 0. The table is empty, so the switch learns A on port 1 and, knowing nothing about B, floods the frame to ports 2 to 5. C, D and E receive a frame that is not for them, see a destination address that is not theirs, and discard it.
  • t = 1. B answers. The switch learns B on port 2 and already knows A, so the reply leaves by port 1 only. One exchange taught the switch both ends of the conversation.
  • t = 2. A’s entry is refreshed, and the frame for B goes out of port 2 alone.
  • t = 3. A broadcast from C, such as an ARP request asking who has an IP address, is always flooded, however much the switch knows.
  • t = 4 and 5. D and E are both on port 4, through a small unmanaged switch under a desk. When E sends to D, the switch finds D on port 4, the port the frame arrived on, and Switch filter decision: A forwarding decision that selects no outgoing port when the usable destination entry points to the frame arrival port.: the small switch has already delivered it, and sending it back would only add a duplicate.
  • t = 20. B has been unplugged from port 2 and plugged into port 5. Its first frame from the new port moves the entry at once.
  • t = 400. A has sent nothing since t = 2, 398 seconds earlier, so its entry has aged out and the frame for A is flooded, as if A were new. C’s own entry had aged out too (C last spoke at t = 3), so the switch learns C afresh.
A frame from A to unknown host B is flooded to B and C; the reply teaches the switch where B is, and later frames go only to B.A (port 1)SwitchB (port 2)C (port 3)frame for Blearn A on port 1;B unknownflooded copyflooded copy:C drops itreply for Alearn B on port 2;A is on port 1forwardednext frame for Bforwarded: C sees nothing

Learn from the source, flood the unknown, forward the known

Text description of the diagram

A sequence diagram with four columns: host A on port 1, the switch, host B on port 2 and host C on port 3. Time runs downwards.

  1. A sends a frame for B. The switch learns that A is on port 1, but it has no entry for B.
  2. The switch floods the frame: one copy goes to B and one to C, which drops it because it is not for C.
  3. B replies to A. The switch learns that B is on port 2 and already knows that A is on port 1, so it forwards the reply to port 1 only.
  4. A sends its next frame for B. The switch now knows B's port and forwards the frame to port 2 only; C sees nothing.

Flooding is not a failure. It is how a switch stays correct while it does not know something: every host still receives the frames meant for it, and the cost is copies on links that did not need them. As soon as the destination sends anything, it is learned, and the flooding for it stops.

Compare B’s reply on port 2 with E sending to D on port 4, where the destination is on the arrival side.

Interactive · stage by stage A destination lookup selects forward, filter or flood Compare B's reply on port 2 with E sending to D on port 4, where the destination is on the arrival side.
A destination lookup selects forward, filter or flood, step 3 of 3. Showing Reply, Next A frame, Shared ingress. In focus: Shared ingress.ReplyB on port 2Next A frameForward to port 2Shared ingressE to D on port 4

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

Every step as text
  1. The reply learns B on port 2 and uses the known A entry on port 1.
  2. The new A-to-B frame can use one known egress port rather than flood.
  3. The stated D entry is on the same arrival port, so the E-to-D frame is filtered.

Checkpoint

Check: A destination lookup selects forward, filter or flood

  1. Question 1 of 2 A known destination is on the arrival port. Which decision fits the model?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Filter the frame

    No egress is needed when the destination is on the same side. Flooding excludes the arrival port and is not the known same-port decision.

  2. Question 2 of 2 In the original reply B-to-A, how many egress ports carry the frame?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1 port

    A is known on one different port.

The newest source location replaces the old entry

Each entry ties a MAC address, within one VLAN, to a port, with the time the address was last seen as a source. Entries learned from traffic are dynamic and age out. Static entries are configured by an administrator and stay; the Linux bridge command, for example, adds them with bridge fdb add and lists the table with bridge fdb show.

A few rules follow from learning only from source addresses:

  • One port can lead to many addresses. A port connected to another switch or to a virtualisation server learns every address behind it. The table says which way to send, not what is plugged in.
  • A host that only receives is never learned. If a server receives a one-way stream and sends nothing back on that network, every switch keeps flooding the stream. Cisco’s notes on unicast flooding name asymmetric routing as one of the two most common causes in campus networks: replies come back through a different router, so the switch on the outbound path never sees the server’s own address as a source.
  • A group address is never a valid source. A frame can be addressed to a group, but it is always sent by one interface, so a frame whose source address has the group bit set is malformed. The Linux bridge drops such frames before it learns anything from them.
  • A move takes effect at once. The next frame from the host’s new port updates the entry. An entry marked sticky is the exception: on Linux, such an entry does not change its port through learning.

The table is also per VLAN: the same MAC address may appear in two VLANs with two different ports, and flooding reaches only the ports of the frame’s VLAN. The VLAN lesson of this module explains how a switch knows a frame’s VLAN.

Follow B from port 2 to port 5 at t = 20, then read the one updated location and timestamp.

Interactive · stage by stage The newest source location replaces the old entry Follow B from port 2 to port 5 at t = 20, then read the one updated location and timestamp.
The newest source location replaces the old entry, step 3 of 3. Showing Before move, Source appears, Next lookup. In focus: Next lookup.Before moveB on port 2Source appearsB on port 5 at t=20Next lookupOne current B entry

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

Every step as text
  1. The original entry describes B at port 2.
  2. Seeing B as a source on port 5 replaces its prior location immediately in this learning model.
  3. The replacement has port 5 and the new source timestamp rather than keeping two live locations.

Checkpoint

Check: The newest source location replaces the old entry

  1. Question 1 of 2 A source moves and transmits on a new port. What changes first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its table entry is replaced with the new source port

    The newest valid source observation updates the location. Ageing handles silence, not a new source observation on another port.

  2. Question 2 of 2 B moves from original port 2 to port 5 and transmits. What port does its next lookup return?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 5 port

    Learning replaces the former source location with port 5.

An entry is usable only before its ageing boundary

An entry lives for the Forwarding entry age: The elapsed time since the source observation that last refreshed a dynamic forwarding entry, compared with its configured ageing rule. after the address was last seen as a source, and every frame from the host resets its timer. Linux bridges and Cisco Catalyst 9300 switches both default to 300 seconds; Cisco accepts 10 to 1,000,000 seconds. Linux treats an entry as gone at the exact moment its age reaches the ageing time, the rule the exercise below uses too.

Why expire anything? A host can disappear from one port and reappear on another without the first port going down: a virtual machine that migrates to another server, or a laptop moved from a desk switch to a wall socket. Until the host sends a frame from its new place, the old entry keeps sending its traffic the wrong way. When a port’s link goes down, the problem is smaller: a Linux bridge deletes every entry learned on a port it disables, so frames for those hosts are flooded and reach them wherever they now are.

The ageing time is therefore a trade-off. Short ageing makes the switch forget quiet hosts and flood frames for them; long ageing leaves a host that moved silently cut off for longer. Here is the trade-off with numbers, for a sensor that sends one frame every 400 seconds while a client sends it a frame every 10 seconds:

Flooding against waiting, for four ageing times Python · original-ageing_tradeoff/ageing_tradeoff.py
"""Choosing an ageing time: flooding for a quiet host against the wait after a host moves silently."""

SENSOR_EVERY = 400  # the sensor sends one frame every 400 s, at t = 0, 400, 800, ...
CLIENT_EVERY = 10  # a client sends the sensor a frame every 10 s, at t = 5, 15, 25, ...
HOUR = 3600

print(f"{'ageing':>7}  {'flooded per hour':>16}  {'worst wait after a silent move':>30}")
for ageing in (15, 60, 300, 1800):
    flooded = 0
    for t in range(5, HOUR, CLIENT_EVERY):
        last_seen = t - t % SENSOR_EVERY  # the sensor's latest frame before t
        if t - last_seen >= ageing:  # its entry has aged out: the switch floods
            flooded += 1
    print(f"{ageing:>5} s  {flooded:>9} of {HOUR // CLIENT_EVERY}  {ageing:>28} s")

Output

 ageing  flooded per hour  worst wait after a silent move
   15 s        351 of 360                            15 s
   60 s        306 of 360                            60 s
  300 s         90 of 360                           300 s
 1800 s          0 of 360                          1800 s

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

With the default 300 seconds, the sensor’s entry is valid for the first 300 seconds of each 400-second cycle, so the client’s frames in the remaining 100 seconds are flooded: 90 of 360 an hour. An ageing time longer than the sensor’s 400-second interval would stop the flooding completely, at the price of a much longer worst case after a silent move. Spanning tree, the subject of the next lesson, uses this trade-off on purpose: while the network’s topology is changing, a Linux bridge ages its entries after the forward delay, 15 seconds by default, instead of 300.

Start from A’s last frame at t = 2 and find the 300-second boundary at t = 302 before reading the t = 400 decision.

Interactive · stage by stage An entry is usable only before its ageing boundary Start from A's last frame at t = 2 and find the 300-second boundary at t = 302 before reading the t = 400 decision.
An entry is usable only before its ageing boundary, step 3 of 3. Showing Last source frame, Boundary, Original t=400. In focus: Original t=400.Last source frameA at t=2Boundary300 seconds laterOriginal t=400A is unknown

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

Every step as text
  1. A last refreshed its original entry at time 2 seconds.
  2. With ageing 300, the original entry is usable only while elapsed time is less than 300.
  3. The silent A entry is too old, so the stated C-to-A frame floods to other eligible ports.

Checkpoint

Check: An entry is usable only before its ageing boundary

  1. Question 1 of 2 An entry age equals its configured ageing interval. Is it usable in this model?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, equality is the expired boundary

    The original lookup uses elapsed time strictly less than ageing. An inclusive comparison changes the original boundary.

  2. Question 2 of 2 A was last refreshed at t=2 with ageing 300. At what time is the exact expiry boundary?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 302 seconds

    Add 300 to the stored time 2; equality is expired.

Each switch learns only where a source arrives

In a network of several switches, each one runs the same algorithm on its own table and knows nothing about the others. To trace a frame, start at the switch where it enters and apply the three steps; whenever a switch forwards or floods out of a port that leads to another switch, continue at that switch, with that link as the arrival port.

The next program wires three switches in a row. S1 has host A on port 1, host B on port 2 and its port 3 linked to port 1 of S2. S2 has host C on port 2 and its port 3 linked to port 1 of S3, which has host D on port 2 and host E on port 3. Every table starts empty, and five frames are sent:

Five frames, three switches, three tables Python · original-three_switches/three_switches.py
"""Five frames through three learning switches in a row: which links carry each one, and what each switch learns."""
from collections import deque

# Who is at the other end of each switch port: a host, or (switch, port) of the next switch.
WIRING = {
    "S1": {1: "A", 2: "B", 3: ("S2", 1)},
    "S2": {1: ("S1", 3), 2: "C", 3: ("S3", 1)},
    "S3": {1: ("S2", 3), 2: "D", 3: "E"},
}
HOME = {"A": ("S1", 1), "B": ("S1", 2), "C": ("S2", 2), "D": ("S3", 2), "E": ("S3", 3)}
tables = {s: {} for s in WIRING}  # switch -> {host: port}


def send(src, dst):
    """Deliver one frame; return the hosts that got a copy and the number of links it crossed."""
    got, links = [], 1  # the sender's own cable to its switch
    queue = deque([HOME[src]])  # (switch, ingress port) still to process
    while queue:
        switch, port = queue.popleft()
        tables[switch][src] = port  # learn the source
        known = tables[switch].get(dst)
        if dst == "all" or known is None:
            out = [p for p in WIRING[switch] if p != port]  # flood
        else:
            out = [] if known == port else [known]  # forward or filter
        for p in out:
            links += 1
            peer = WIRING[switch][p]
            if isinstance(peer, tuple):
                queue.append(peer)
            else:
                got.append(peer)
    return got, links


for src, dst in [("A", "E"), ("E", "A"), ("C", "B"), ("D", "all"), ("A", "E")]:
    got, links = send(src, dst)
    extra = [h for h in got if h != dst] if dst != "all" else []
    print(f"{src} -> {dst:<3}  links crossed: {links}  copies to {','.join(sorted(got)):<9}  unwanted: {len(extra)}")

print()
for switch, table in tables.items():
    print(switch, "  ".join(f"{h}:{p}" for h, p in sorted(table.items())))

Output

A -> E    links crossed: 7  copies to B,C,D,E    unwanted: 3
E -> A    links crossed: 4  copies to A          unwanted: 0
C -> B    links crossed: 7  copies to A,B,D,E    unwanted: 3
D -> all  links crossed: 7  copies to A,B,C,E    unwanted: 0
A -> E    links crossed: 4  copies to E          unwanted: 0

S1 A:1  C:3  D:3  E:3
S2 A:1  C:2  D:3  E:3
S3 A:1  C:1  D:2  E:3

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

One program · 14 languages Forward all five original frames through three switches O(s times p) for s visited switches and p ports

Python 3.14 · main.py

from collections import deque

net = [["A", "B", (1, 0)], [(0, 2), "C", (2, 0)], [(1, 2), "D", "E"]]
host = {"A": (0, 0), "B": (0, 1), "C": (1, 1), "D": (2, 1), "E": (2, 2)}
fdb: list[dict[str, int]] = [{}, {}, {}]


def send(src: str, dst: str) -> tuple[list[str], int]:
    got: list[str] = []
    links = 1
    queue = deque([host[src]])
    while queue:
        switch, ingress = queue.popleft()
        fdb[switch][src] = ingress
        known = fdb[switch].get(dst)
        out = (
            [p for p in range(3) if p != ingress]
            if known is None
            else []
            if known == ingress
            else [known]
        )
        for port in out:
            links += 1
            peer = net[switch][port]
            if isinstance(peer, tuple):
                queue.append(peer)
            else:
                got.append(peer)
    return got, links


for src, dst in [
    ("A", "E"),
    ("E", "A"),
    ("C", "B"),
    ("D", "all"),
    ("A", "E"),
]:
    got, links = send(src, dst)
    extra = sum(host != dst for host in got) if dst != "all" else 0
    print(f"{src}->{dst} links={links} copies={','.join(sorted(got))} unwanted={extra}")
for switch, table in enumerate(fdb, 1):
    print(
        f"S{switch} "
        + ",".join(f"{host}:{port + 1}" for host, port in sorted(table.items()))
    )

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 net = [
  ["A", "B", [1, 0]],
  [[0, 2], "C", [2, 0]],
  [[1, 2], "D", "E"],
];
const host = new Map([
  ["A", [0, 0]],
  ["B", [0, 1]],
  ["C", [1, 1]],
  ["D", [2, 1]],
  ["E", [2, 2]],
]);
const fdb = [new Map(), new Map(), new Map()];
function send(src, dst) {
  const got = [],
    queue = [host.get(src)];
  let links = 1;
  for (let at = 0; at < queue.length; at++) {
    const [sw, ingress] = queue[at];
    fdb[sw].set(src, ingress);
    const known = fdb[sw].get(dst);
    const out =
      known === undefined
        ? [0, 1, 2].filter((p) => p !== ingress)
        : known === ingress
          ? []
          : [known];
    for (const port of out) {
      links++;
      const peer = net[sw][port];
      if (Array.isArray(peer)) queue.push(peer);
      else got.push(peer);
    }
  }
  return [got, links];
}
const frames = [
  ["A", "E"],
  ["E", "A"],
  ["C", "B"],
  ["D", "all"],
  ["A", "E"],
];
for (const [src, dst] of frames) {
  const [got, links] = send(src, dst);
  const extra = dst === "all" ? 0 : got.filter((h) => h !== dst).length;
  const copies = got.sort().join(",");
  console.log(`${src}->${dst} links=${links} copies=${copies} unwanted=${extra}`);
}
for (let sw = 0; sw < 3; sw++) {
  const row = [...fdb[sw].keys()].sort().map((h) => `${h}:${fdb[sw].get(h) + 1}`);
  console.log(`S${sw + 1} ${row.join(",")}`);
}

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.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

public class Main {
  record Port(int sw, int ingress) {}

  record Peer(String host, Port port) {}

  static Peer host(String name) {
    return new Peer(name, null);
  }

  static Peer link(int sw, int ingress) {
    return new Peer(null, new Port(sw, ingress));
  }

  public static void main(String[] args) {
    Peer[][] net = {
      {host("A"), host("B"), link(1, 0)},
      {link(0, 2), host("C"), link(2, 0)},
      {link(1, 2), host("D"), host("E")}
    };
    var host =
        Map.of(
            "A",
            new Port(0, 0),
            "B",
            new Port(0, 1),
            "C",
            new Port(1, 1),
            "D",
            new Port(2, 1),
            "E",
            new Port(2, 2));
    List<HashMap<String, Integer>> fdb = List.of(new HashMap<>(), new HashMap<>(), new HashMap<>());
    String[][] frames = {{"A", "E"}, {"E", "A"}, {"C", "B"}, {"D", "all"}, {"A", "E"}};
    for (String[] frame : frames) {
      String src = frame[0], dst = frame[1];
      var got = new ArrayList<String>();
      var queue = new ArrayDeque<Port>();
      queue.add(host.get(src));
      int links = 1;
      while (!queue.isEmpty()) {
        Port here = queue.remove();
        fdb.get(here.sw()).put(src, here.ingress());
        Integer known = fdb.get(here.sw()).get(dst);
        for (int port = 0; port < 3; port++) {
          boolean skip =
              known != null ? port != known || known == here.ingress() : port == here.ingress();
          if (skip) continue;
          links++;
          Peer peer = net[here.sw()][port];
          if (peer.port() != null) queue.add(peer.port());
          else got.add(peer.host());
        }
      }
      int extra = dst.equals("all") ? 0 : (int) got.stream().filter(h -> !h.equals(dst)).count();
      got.sort(String::compareTo);
      System.out.printf(
          "%s->%s links=%d copies=%s unwanted=%d%n", src, dst, links, String.join(",", got), extra);
    }
    for (int sw = 0; sw < 3; sw++) {
      var row = new ArrayList<String>();
      for (String hostName : new TreeSet<>(fdb.get(sw).keySet()))
        row.add(hostName + ":" + (fdb.get(sw).get(hostName) + 1));
      System.out.println("S" + (sw + 1) + " " + String.join(",", row));
    }
  }
}

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 <algorithm>
#include <map>
#include <print>
#include <string>
#include <utility>
#include <vector>
struct Port {
  int sw;
  int ingress;
};
struct Peer {
  std::string host;
  Port port;
};
int main() {
  const Peer net[3][3] = {{{"A", {}}, {"B", {}}, {"", {1, 0}}},
                          {{"", {0, 2}}, {"C", {}}, {"", {2, 0}}},
                          {{"", {1, 2}}, {"D", {}}, {"E", {}}}};
  const std::map<std::string, Port> host = {{"A", {0, 0}},
                                            {"B", {0, 1}},
                                            {"C", {1, 1}},
                                            {"D", {2, 1}},
                                            {"E", {2, 2}}};
  std::map<std::string, int> fdb[3];
  const std::vector<std::pair<std::string, std::string>> frames = {
      {"A", "E"}, {"E", "A"}, {"C", "B"}, {"D", "all"}, {"A", "E"}};
  for (const auto &[src, dst] : frames) {
    std::vector<std::string> got;
    std::vector<Port> queue = {host.at(src)};
    int links = 1;
    for (std::size_t at = 0; at < queue.size(); at++) {
      const auto here = queue[at];
      auto &table = fdb[here.sw];
      table[src] = here.ingress;
      const auto known = table.find(dst);
      for (int p = 0; p < 3; p++) {
        const bool skip =
            known != table.end()
                ? p != known->second || known->second == here.ingress
                : p == here.ingress;
        if (skip)
          continue;
        links++;
        const auto &peer = net[here.sw][p];
        if (peer.host.empty())
          queue.push_back(peer.port);
        else
          got.push_back(peer.host);
      }
    }
    const auto extra =
        dst == "all"
            ? 0
            : std::count_if(got.begin(), got.end(),
                            [&](const auto &host) { return host != dst; });
    std::sort(got.begin(), got.end());
    std::string copies;
    for (const auto &host : got) {
      if (!copies.empty())
        copies += ",";
      copies += host;
    }
    std::println("{}->{} links={} copies={} unwanted={}", src, dst, links,
                 copies, extra);
  }
  for (int sw = 0; sw < 3; sw++) {
    std::string row;
    for (const auto &[host, p] : fdb[sw]) {
      if (!row.empty())
        row += ",";
      row += host + ":" + std::to_string(p + 1);
    }
    std::println("S{} {}", sw + 1, row);
  }
}

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? Host, int Switch, int Port)[][] net = [
    [("A",0,0),("B",0,0),(null,1,0)],
    [(null,0,2),("C",0,0),(null,2,0)],
    [(null,1,2),("D",0,0),("E",0,0)]];
Dictionary<string, (int Switch, int Port)> host = new()
{
    ["A"] = (0, 0),
    ["B"] = (0, 1),
    ["C"] = (1, 1),
    ["D"] = (2, 1),
    ["E"] = (2, 2)
};
Dictionary<string, int>[] fdb = [new(), new(), new()];
var frames = new[] { ("A", "E"), ("E", "A"), ("C", "B"), ("D", "all"), ("A", "E") };
foreach (var (src, dst) in frames)
{
    List<string> got = new(); Queue<(int Switch, int Port)> queue = new();
    queue.Enqueue(host[src]); int links = 1;
    while (queue.TryDequeue(out var here))
    {
        fdb[here.Switch][src] = here.Port;
        bool known = fdb[here.Switch].TryGetValue(dst, out int output);
        for (int port = 0; port < 3; port++)
        {
            if (known ? port != output || output == here.Port : port == here.Port) continue;
            links++; var peer = net[here.Switch][port];
            if (peer.Host is null) queue.Enqueue((peer.Switch, peer.Port)); else got.Add(peer.Host);
        }
    }
    int extra = dst == "all" ? 0 : got.Count(h => h != dst);
    got.Sort(StringComparer.Ordinal);
    string copies = string.Join(",", got);
    Console.WriteLine($"{src}->{dst} links={links} copies={copies} unwanted={extra}");
}
for (int sw = 0; sw < 3; sw++)
{
    var row = fdb[sw].Keys.Order(StringComparer.Ordinal).Select(h => $"{h}:{fdb[sw][h] + 1}");
    Console.WriteLine($"S{sw + 1} {string.Join(",", row)}");
}

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>
#include <string.h>

typedef struct {
  int host, sw, port;
} Peer;
typedef struct {
  int sw, port;
} Port;
int main(void) {
  const Peer net[3][3] = {{{0, 0, 0}, {1, 0, 0}, {-1, 1, 0}},
                          {{-1, 0, 2}, {2, 0, 0}, {-1, 2, 0}},
                          {{-1, 1, 2}, {3, 0, 0}, {4, 0, 0}}};
  const Port host[] = {{0, 0}, {0, 1}, {1, 1}, {2, 1}, {2, 2}};
  const int frames[5][2] = {{0, 4}, {4, 0}, {2, 1}, {3, -1}, {0, 4}};
  int fdb[3][5];
  for (int s = 0; s < 3; s++)
    for (int h = 0; h < 5; h++)
      fdb[s][h] = -1;
  for (int f = 0; f < 5; f++) {
    const int src = frames[f][0], dst = frames[f][1];
    Port queue[12] = {host[src]};
    int end = 1, links = 1, got[5] = {0};
    for (int at = 0; at < end; at++) {
      const Port here = queue[at];
      fdb[here.sw][src] = here.port;
      const int known = dst < 0 ? -1 : fdb[here.sw][dst];
      for (int p = 0; p < 3; p++) {
        if (known >= 0 ? p != known || known == here.port : p == here.port)
          continue;
        links++;
        const Peer next = net[here.sw][p];
        if (next.host < 0)
          queue[end++] = (Port){next.sw, next.port};
        else
          got[next.host]++;
      }
    }
    int extra = 0;
    char copies[16] = "";
    int length = 0;
    for (int h = 0; h < 5; h++)
      if (got[h]) {
        if (length)
          copies[length++] = ',';
        copies[length++] = (char)('A' + h);
        if (dst >= 0 && h != dst)
          extra += got[h];
      }
    if (dst < 0)
      printf("%c->all", 'A' + src);
    else
      printf("%c->%c", 'A' + src, 'A' + dst);
    printf(" links=%d copies=%s unwanted=%d\n", links, copies, extra);
  }
  for (int sw = 0; sw < 3; sw++) {
    printf("S%d ", sw + 1);
    int count = 0;
    for (int h = 0; h < 5; h++)
      if (fdb[sw][h] >= 0)
        printf("%s%c:%d", count++ ? "," : "", 'A' + h, fdb[sw][h] + 1);
    putchar('\n');
  }
  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);

$net = [['A','B',[1,0]],[[0,2],'C',[2,0]],[[1,2],'D','E']];
$host = ['A' => [0,0],'B' => [0,1],'C' => [1,1],'D' => [2,1],'E' => [2,2]];
$fdb = [[],[],[]];
foreach ([['A','E'],['E','A'],['C','B'],['D','all'],['A','E']] as [$src, $dst]) {
    $got = [];
    $queue = [$host[$src]];
    $links = 1;
    for ($at = 0; $at < count($queue); ++$at) {
        [$sw, $ingress] = $queue[$at];
        $fdb[$sw][$src] = $ingress;
        $known = $fdb[$sw][$dst] ?? null;
        for ($port = 0; $port < 3; ++$port) {
            $skip = $known !== null ? $port !== $known || $known === $ingress : $port === $ingress;
            if ($skip) {
                continue;
            }
            ++$links;
            $peer = $net[$sw][$port];
            if (is_array($peer)) {
                $queue[] = $peer;
            } else {
                $got[] = $peer;
            }
        }
    }
    $extra = $dst === 'all' ? 0 : count(array_filter(
        $got,
        static fn(string $host): bool => $host !== $dst,
    ));
    sort($got);
    echo $src . '->' . $dst . ' links=' . $links . ' copies=' . implode(',', $got)
        . ' unwanted=' . $extra . "\n";
}
foreach ($fdb as $sw => $table) {
    ksort($table);
    $row = [];
    foreach ($table as $host => $port) {
        $row[] = $host . ':' . ($port + 1);
    }
    echo 'S' . ($sw + 1) . ' ' . implode(',', $row) . "\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

use std::collections::{BTreeMap, VecDeque};
#[derive(Clone, Copy)]
enum Peer {
    Host(char),
    Link(usize, usize),
}
fn main() {
    use Peer::{Host as H, Link as L};
    let net = [
        [H('A'), H('B'), L(1, 0)],
        [L(0, 2), H('C'), L(2, 0)],
        [L(1, 2), H('D'), H('E')],
    ];
    let host = BTreeMap::from([
        ('A', (0, 0)),
        ('B', (0, 1)),
        ('C', (1, 1)),
        ('D', (2, 1)),
        ('E', (2, 2)),
    ]);
    let mut fdb: [BTreeMap<char, usize>; 3] = std::array::from_fn(|_| BTreeMap::new());
    for (src, dst) in [('A', 'E'), ('E', 'A'), ('C', 'B'), ('D', '*'), ('A', 'E')] {
        let mut got = Vec::new();
        let mut links = 1;
        let mut queue = VecDeque::new();
        if let Some(&origin) = host.get(&src) {
            queue.push_back(origin);
        }
        while let Some((sw, ingress)) = queue.pop_front() {
            fdb[sw].insert(src, ingress);
            let known = fdb[sw].get(&dst).copied();
            for port in 0..3 {
                if known.map_or(port == ingress, |p| port != p || p == ingress) {
                    continue;
                }
                links += 1;
                match net[sw][port] {
                    H(host) => got.push(host),
                    L(s, p) => queue.push_back((s, p)),
                }
            }
        }
        let extra = if dst == '*' {
            0
        } else {
            got.iter().filter(|&&h| h != dst).count()
        };
        got.sort();
        let copies = got
            .iter()
            .map(char::to_string)
            .collect::<Vec<_>>()
            .join(",");
        let dst = if dst == '*' {
            "all".to_string()
        } else {
            dst.to_string()
        };
        println!("{src}->{dst} links={links} copies={copies} unwanted={extra}");
    }
    for (sw, table) in fdb.iter().enumerate() {
        let row = table
            .iter()
            .map(|(h, p)| format!("{h}:{}", p + 1))
            .collect::<Vec<_>>()
            .join(",");
        println!("S{} {row}", sw + 1);
    }
}

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

type Peer = string | [number, number];
const net: Peer[][] = [
  ["A", "B", [1, 0]],
  [[0, 2], "C", [2, 0]],
  [[1, 2], "D", "E"],
];
const host = new Map<string, [number, number]>([
  ["A", [0, 0]],
  ["B", [0, 1]],
  ["C", [1, 1]],
  ["D", [2, 1]],
  ["E", [2, 2]],
]);
const fdb = [new Map<string, number>(), new Map<string, number>(), new Map<string, number>()];
function send(src: string, dst: string): [string[], number] {
  const origin = host.get(src);
  if (origin === undefined) throw new Error("unknown source");
  const got: string[] = [],
    queue: [number, number][] = [origin];
  let links = 1;
  for (let at = 0; at < queue.length; at++) {
    const [sw, ingress] = queue[at];
    fdb[sw].set(src, ingress);
    const known = fdb[sw].get(dst);
    const out =
      known === undefined
        ? [0, 1, 2].filter((p) => p !== ingress)
        : known === ingress
          ? []
          : [known];
    for (const port of out) {
      links++;
      const peer = net[sw][port];
      if (typeof peer === "string") got.push(peer);
      else queue.push(peer);
    }
  }
  return [got, links];
}
const frames = [
  ["A", "E"],
  ["E", "A"],
  ["C", "B"],
  ["D", "all"],
  ["A", "E"],
];
for (const [src, dst] of frames) {
  const [got, links] = send(src, dst);
  const extra = dst === "all" ? 0 : got.filter((h) => h !== dst).length;
  const copies = got.sort().join(",");
  console.log(`${src}->${dst} links=${links} copies=${copies} unwanted=${extra}`);
}
for (let sw = 0; sw < 3; sw++) {
  const row = [...fdb[sw].keys()].sort().map((h) => {
    const port = fdb[sw].get(h);
    if (port === undefined) throw new Error("missing entry");
    return `${h}:${port + 1}`;
  });
  console.log(`S${sw + 1} ${row.join(",")}`);
}

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"
	"sort"
	"strings"
)

type port struct{ sw, ingress int }
type peer struct {
	host string
	port port
}

func main() {
	net := [][]peer{{{"A", port{}}, {"B", port{}}, {"", port{1, 0}}},
		{{"", port{0, 2}}, {"C", port{}}, {"", port{2, 0}}},
		{{"", port{1, 2}}, {"D", port{}}, {"E", port{}}}}
	host := map[string]port{"A": {0, 0}, "B": {0, 1}, "C": {1, 1}, "D": {2, 1}, "E": {2, 2}}
	fdb := []map[string]int{{}, {}, {}}
	frames := [][2]string{{"A", "E"}, {"E", "A"}, {"C", "B"}, {"D", "all"}, {"A", "E"}}
	for _, frame := range frames {
		src, dst := frame[0], frame[1]
		got := []string{}
		queue := []port{host[src]}
		links := 1
		for at := 0; at < len(queue); at++ {
			here := queue[at]
			fdb[here.sw][src] = here.ingress
			output, known := fdb[here.sw][dst]
			for p := 0; p < 3; p++ {
				if known && (p != output || output == here.ingress) || !known && p == here.ingress {
					continue
				}
				links++
				next := net[here.sw][p]
				if next.host == "" {
					queue = append(queue, next.port)
				} else {
					got = append(got, next.host)
				}
			}
		}
		extra := 0
		for _, host := range got {
			if dst != "all" && host != dst {
				extra++
			}
		}
		sort.Strings(got)
		fmt.Printf("%s->%s links=%d copies=%s unwanted=%d\n",
			src, dst, links, strings.Join(got, ","), extra)
	}
	for sw, table := range fdb {
		keys := []string{}
		for host := range table {
			keys = append(keys, host)
		}
		sort.Strings(keys)
		row := []string{}
		for _, host := range keys {
			row = append(row, fmt.Sprintf("%s:%d", host, table[host]+1))
		}
		fmt.Printf("S%d %s\n", sw+1, strings.Join(row, ","))
	}
}

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

enum Peer {
  case host(String)
  case link(Int, Int)
}
let net: [[Peer]] = [
  [.host("A"), .host("B"), .link(1, 0)],
  [.link(0, 2), .host("C"), .link(2, 0)], [.link(1, 2), .host("D"), .host("E")],
]
let host = ["A": (0, 0), "B": (0, 1), "C": (1, 1), "D": (2, 1), "E": (2, 2)]
var fdb = [[String: Int](), [String: Int](), [String: Int]()]
for (src, dst) in [("A", "E"), ("E", "A"), ("C", "B"), ("D", "all"), ("A", "E")] {
  guard let origin = host[src] else { continue }
  var queue = [origin]
  var got = [String]()
  var links = 1
  var at = 0
  while at < queue.count {
    let (sw, ingress) = queue[at]
    at += 1
    fdb[sw][src] = ingress
    let known = fdb[sw][dst]
    for port in 0..<3 {
      if known.map({ port != $0 || $0 == ingress }) ?? (port == ingress) { continue }
      links += 1
      switch net[sw][port] {
      case .host(let host): got.append(host)
      case .link(let sw, let ingress): queue.append((sw, ingress))
      }
    }
  }
  let extra = dst == "all" ? 0 : got.filter { $0 != dst }.count
  let copies = got.sorted().joined(separator: ",")
  print("\(src)->\(dst) links=\(links) copies=\(copies) unwanted=\(extra)")
}
for (sw, table) in fdb.enumerated() {
  let row = table.keys.sorted().compactMap { host -> String? in
    guard let port = table[host] else { return nil }
    return "\(host):\(port+1)"
  }.joined(separator: ",")
  print("S\(sw+1) \(row)")
}

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

net = [['A', 'B', [1, 0]], [[0, 2], 'C', [2, 0]], [[1, 2], 'D', 'E']]
host = { 'A' => [0, 0], 'B' => [0, 1], 'C' => [1, 1], 'D' => [2, 1], 'E' => [2, 2] }
fdb = [{}, {}, {}]
[%w[A E], %w[E A], %w[C B], %w[D all], %w[A E]].each do |src, dst|
  got = []
  links = 1
  queue = [host.fetch(src)]
  until queue.empty?
    switch, ingress = queue.shift
    fdb[switch][src] = ingress
    known = fdb[switch][dst]
    (0..2).each do |port|
      next if known ? port != known || known == ingress : port == ingress

      links += 1
      peer = net[switch][port]
      peer.is_a?(Array) ? queue.push(peer) : got.push(peer)
    end
  end
  extra = dst == 'all' ? 0 : got.count { |host| host != dst }
  puts "#{src}->#{dst} links=#{links} copies=#{got.sort.join(',')} unwanted=#{extra}"
end
fdb.each_with_index do |table, switch|
  row = table.keys.sort.map { |host| "#{host}:#{table.fetch(host) + 1}" }.join(',')
  puts "S#{switch + 1} #{row}"
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

sealed interface Peer

data class Host(
    val name: String,
) : Peer

data class Link(
    val switch: Int,
    val port: Int,
) : Peer

fun main() {
    val net =
        listOf(
            listOf(Host("A"), Host("B"), Link(1, 0)),
            listOf(Link(0, 2), Host("C"), Link(2, 0)),
            listOf(Link(1, 2), Host("D"), Host("E")),
        )
    val host =
        mapOf(
            "A" to Link(0, 0),
            "B" to Link(0, 1),
            "C" to Link(1, 1),
            "D" to Link(2, 1),
            "E" to Link(2, 2),
        )
    val fdb = List(3) { mutableMapOf<String, Int>() }
    val frames = listOf("A" to "E", "E" to "A", "C" to "B", "D" to "all", "A" to "E")
    for ((src, dst) in frames) {
        val got = mutableListOf<String>()
        val queue = ArrayDeque<Link>()
        queue.add(host.getValue(src))
        var links = 1
        while (queue.isNotEmpty()) {
            val here = queue.removeFirst()
            fdb[here.switch][src] = here.port
            val known = fdb[here.switch][dst]
            for (port in 0..2) {
                val skip =
                    if (known != null) {
                        port != known || known == here.port
                    } else {
                        port == here.port
                    }
                if (skip) continue
                links++
                when (val peer = net[here.switch][port]) {
                    is Host -> got.add(peer.name)
                    is Link -> queue.add(peer)
                }
            }
        }
        val extra = if (dst == "all") 0 else got.count { it != dst }
        val copies = got.sorted().joinToString(",")
        println("$src->$dst links=$links copies=$copies unwanted=$extra")
    }
    for ((sw, table) in fdb.withIndex()) {
        val row = table.keys.sorted().joinToString(",") { "$it:${table.getValue(it) + 1}" }
        println("S${sw + 1} $row")
    }
}

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

sealed class Peer {}

final class Host extends Peer {
  final String name;
  Host(this.name);
}

final class Link extends Peer {
  final int sw, port;
  Link(this.sw, this.port);
}

void main() {
  final net = <List<Peer>>[
    [Host('A'), Host('B'), Link(1, 0)],
    [Link(0, 2), Host('C'), Link(2, 0)],
    [Link(1, 2), Host('D'), Host('E')],
  ];
  final host = {
    'A': Link(0, 0),
    'B': Link(0, 1),
    'C': Link(1, 1),
    'D': Link(2, 1),
    'E': Link(2, 2),
  };
  final fdb = List.generate(3, (_) => <String, int>{});
  for (final (src, dst) in [
    ('A', 'E'),
    ('E', 'A'),
    ('C', 'B'),
    ('D', 'all'),
    ('A', 'E'),
  ]) {
    final origin = host[src];
    if (origin == null) continue;
    final got = <String>[], queue = <Link>[origin];
    var links = 1;
    for (var at = 0; at < queue.length; at++) {
      final here = queue[at];
      fdb[here.sw][src] = here.port;
      final known = fdb[here.sw][dst];
      for (var port = 0; port < 3; port++) {
        if (known != null
            ? port != known || known == here.port
            : port == here.port)
          continue;
        links++;
        switch (net[here.sw][port]) {
          case Host(:final name):
            got.add(name);
          case final Link peer:
            queue.add(peer);
        }
      }
    }
    final extra = dst == 'all' ? 0 : got.where((h) => h != dst).length;
    got.sort();
    print('$src->$dst links=$links copies=${got.join(',')} unwanted=$extra');
  }
  for (var sw = 0; sw < 3; sw++) {
    final keys = fdb[sw].keys.toList()..sort();
    final row = keys
        .map((host) => '$host:${(fdb[sw][host] ?? -1) + 1}')
        .join(',');
    print('S${sw + 1} $row');
  }
}

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

A->E links=7 copies=B,C,D,E unwanted=3
E->A links=4 copies=A unwanted=0
C->B links=7 copies=A,B,D,E unwanted=3
D->all links=7 copies=A,B,C,E unwanted=0
A->E links=4 copies=E unwanted=0
S1 A:1,C:3,D:3,E:3
S2 A:1,C:2,D:3,E:3
S3 A:1,C:1,D:2,E:3
  • A to E is unknown everywhere, so each switch floods it: it crosses all 7 links, the sender’s included, and three hosts receive a copy they discard. On the way, every switch learns where A is.
  • E to A follows the learned entries straight back: 4 links and no extra copies. The switches learned E’s position on the way.
  • C to B is flooded through the whole network again, because B has never sent a frame and no table holds it.
  • The broadcast from D reaches every host, as it must.
  • A to E again now crosses only the 4 links of the path.

The final tables show how much each switch can know about hosts it is not connected to: S2 places A on its port 1 and E on its port 3, and S3 places A and C both behind its port 1. The tables are where the network’s knowledge lives, and each switch only knows the next step.

Follow all five original frames through S1, S2 and S3, recording each switch’s own learned ingress and delivered copies.

Interactive · stage by stage Each switch learns only where a source arrives Follow all five original frames through S1, S2 and S3, recording each switch's own learned ingress and delivered copies.
Each switch learns only where a source arrives, step 3 of 3. Showing First A to E, E replies, Other original frames. In focus: Other original frames.First A to EUnknown along the chainE repliesOne learned path backOther original framesC to B, D to all, A to E

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

Every step as text
  1. The original S1–S2–S3 chain learns A separately at the ingress of each switch that receives the frame.
  2. E-to-A learns E along the returning path and uses the previously learned A entries.
  3. Keep all five original frames and their table updates rather than substituting a shorter trace.

Checkpoint

Check: Each switch learns only where a source arrives

  1. Question 1 of 2 What does S2 learn when a frame from A arrives from S1?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A is reachable through S2's ingress toward S1

    Each switch records its own frame arrival port. Port numbers describe local switch attachments.

  2. Question 2 of 2 After the original A-to-E exchange, S2 knows A on port 1 and E on port 3. An E-to-A frame arrives on port 3. Which egress port does S2 choose?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1 port

    S2 learns E on ingress 3 and forwards to the already learned A entry on port 1.

A hub is a repeater: it copies every bit that arrives on one port to all the others, without reading addresses. All its ports share one medium, so they form one collision domain, the set of interfaces whose transmissions can collide, and they must take turns using half-duplex CSMA/CD, as the Ethernet timing lesson described. A switch gives each port its own link: two hosts on different ports can send at the same moment, and a full-duplex port has no collisions at all. A switch floods a broadcast to eligible ports of its VLAN, which form one broadcast domain, the set of interfaces that broadcast reaches. A router ends a broadcast domain: RFC 1812 forbids an IPv4 router to forward a packet it received as a link-layer broadcast unless the packet is addressed to an IP multicast group.

Hub, switch or router?
Criterion HubSwitchRouter
Works with bits (physical layer)frames and MAC addresses (link layer)packets and IP addresses (network layer)
Sends a unicast frame to every other portone port once the address is learned; every other port beforethe next hop on the best route
Collision domains one for all its portsone per port (none on full-duplex links)one per interface
Broadcast domains oneone per VLANone per interface: broadcasts stop here
When to choose Nowhere in a new network; you may meet one in old installationsConnecting hosts inside one network or VLANConnecting different networks or VLANs to each other

The difference is easy to measure. The program sends the same 2,000 random unicast frames between 8 hosts through a hub and through a learning switch, and counts the copies that reach a host they were not meant for:

2,000 frames through a hub and through a switch Python · original-hub_vs_switch/hub_vs_switch.py
"""The same 2,000 unicast frames between 8 hosts, through a hub and through a learning switch."""
import random

HOSTS = 8  # host h sits on port h
rng = random.Random(2026)
frames = [tuple(rng.sample(range(HOSTS), 2)) for _ in range(2000)]  # (source, destination)


def deliver(frames, learning):
    """Copies sent out of each port, copies that reached a host they were not for, and frames flooded."""
    out = [0] * HOSTS
    unwanted = floods = 0
    table = {}  # host -> port (here host h is always on port h)
    for src, dst in frames:
        if learning:
            table[src] = src
        if learning and dst in table:
            ports = [table[dst]]
        else:  # a hub repeats every frame; a switch floods what it cannot place
            ports = [p for p in range(HOSTS) if p != src]
            floods += 1
        for p in ports:
            out[p] += 1
            unwanted += p != dst
    return out, unwanted, floods


for name, learning in (("hub", False), ("switch", True)):
    out, unwanted, floods = deliver(frames, learning)
    print(f"{name:<6}  sent to every port: {floods:>4}  copies: {sum(out):>5}  "
          f"unwanted: {unwanted:>5}  busiest port: {max(out)}")

seen = set()
for n, (src, _) in enumerate(frames, start=1):
    seen.add(src)
    if len(seen) == HOSTS:
        print(f"all {HOSTS} hosts had sent a frame by frame {n}: from then on the switch knew every destination")
        break

Output

hub     sent to every port: 2000  copies: 14000  unwanted: 12000  busiest port: 1773
switch  sent to every port:   14  copies:  2084  unwanted:    84  busiest port: 281
all 8 hosts had sent a frame by frame 32: from then on the switch knew every destination

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

The hub delivered 12,000 unwanted copies, six for every frame. The switch flooded only 14 frames, all of them before it had heard from every host, and after frame 32 every frame went to exactly one port. The busiest switch port carried 281 frames, against 1,773 on the hub, and on the hub all of those shared one collision domain.

A worked count. A router has two interfaces. The first goes to a switch with 10 PCs; the second to a hub with 5 PCs. On the switch side, every switch port in use is a collision domain: 10 PC ports plus the port to the router make 11. The hub, its 5 PCs and the router interface on that side share 1 more, so there are 12 collision domains. The router separates the two sides, so there are 2 broadcast domains.

Count the original seventeen active switch links and one shared hub segment, while keeping full-duplex collisions separate from the conventional domain count.

Interactive · stage by stage Switch ports separate links and VLANs separate broadcasts Count the original seventeen active switch links and one shared hub segment, while keeping full-duplex collisions separate from the conventional domain count.
Switch ports separate links and VLANs separate broadcasts, step 3 of 3. Showing Switch in use, Hub segment, Duplex scope. In focus: Duplex scope.Switch in use16 PCs and one routerHub segmentSix PCs and routerDuplex scopeNo full-duplex collisions

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

Every step as text
  1. The original switch has seventeen active point-to-point port links.
  2. The traditional hub segment is one shared collision domain.
  3. The conventional per-link count does not mean collisions happen on full-duplex Ethernet.

Checkpoint

Check: Switch ports separate links and VLANs separate broadcasts

  1. Question 1 of 2 What does full duplex change on a point-to-point Ethernet link?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Both ends can transmit without shared-medium collisions

    Full duplex removes the shared-medium collision mechanism on that link. The hub's shared segment model is different.

  2. Question 2 of 2 In the original comparison, what is the traditional collision-domain total for seventeen switch links plus one hub segment?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 18 domains

    Seventeen plus one gives eighteen.

A finite table can stop learning new destinations

A hardware switch has a finite forwarding-table capacity, with admission or replacement behavior that depends on its implementation. A Linux bridge’s fdb_max_learned setting can cap learned entries; 0 disables that configured cap, while memory and other resource limits still exist. At the configured cap, new learning can stop. Under the stated unknown-unicast flood policy, destinations without usable entries receive copies on eligible VLAN ports.

Normal growth rarely fills a table, so Cisco’s notes on unicast flooding point at the other cause: a single host generating frames from a stream of different source addresses, which shows up as most of the table’s addresses sitting behind the same port. The defence is to limit how many addresses each untrusted port may teach the switch, the feature called port security, which the VLAN and switch security lesson of this module covers with the other switch protections.

MAC Address Lookup Look up the maker of an address you find in a MAC table, or check whether it is a randomised local address.

Commands for a real switch

On a Linux bridge, bridge fdb show br br0 lists the table, bridge link set dev eth1 learning off stops one port from learning, and ip link set dev br0 type bridge ageing_time 600 sets the ageing time in seconds. On a Cisco Catalyst switch, mac address-table aging-time sets the same timer.

Switching in GATE CS

The data link layer of Section 10 of the GATE 2027 Computer Science syllabus lists Ethernet; the 2026 syllabus named Ethernet bridging explicitly, which is the subject of this module, so papers written for it are good practice. Counting collision and broadcast domains, as in the worked count above, is a quick exam question worth practising.

The CCNA 200-301 exam topics also name these ideas under switching concepts: MAC learning and aging, frame switching, frame flooding and the MAC address table.

Subtract forty entries from the stated 16,000 slots, then divide the 15,960 remaining slots by 10,000 new sources per second.

Interactive · stage by stage A finite table can stop learning new destinations Subtract forty entries from the stated 16,000 slots, then divide the 15,960 remaining slots by 10,000 new sources per second.
A finite table can stop learning new destinations, step 3 of 3. Showing Capacity, Already present, Incoming rate. In focus: Incoming rate.Capacity16,000 entriesAlready present40 host entriesIncoming rate10,000 new sources/s

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

Every step as text
  1. The owned lab model assumes a fixed 16,000-entry learning capacity.
  2. Forty existing entries leave 15,960 available slots under the stated no-expiry calculation.
  3. A full table outcome depends on implementation policy; the arithmetic alone does not prove every device handles capacity identically.

Checkpoint

Check: A finite table can stop learning new destinations

  1. Question 1 of 2 Does a table-capacity calculation determine every switch's full-table policy?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, replacement and admission policies depend on the implementation

    Capacity arithmetic states how many slots remain. It does not establish a universal replacement or forwarding policy.

  2. Question 2 of 2 How many slots remain in the stated 16,000-entry table after forty entries?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 15960 entries

    Subtract forty from sixteen thousand.

Exercise

Exercise · Medium · Python

Build a learning switch with ageing

Complete the class LearningSwitch in switch.py. Addresses are lower-case text with colons, such as 00:00:5e:00:53:0a, and times are seconds.

lookup(mac, now) returns the port on which mac was last seen as a source address. It returns None if the address was never seen, or if its entry has aged out: an entry recorded at time t is used while now - t is less than self.ageing, and from now - t == self.ageing on it counts as gone.

receive(port, src, dst, now) handles one frame that arrived on port and returns the ports it leaves by, as a sorted list:

  1. If src is a group address (the lowest bit of its first byte is 1, as in 01:00:5e:90:10:01 or the broadcast address), the frame is invalid: return [] and learn nothing.
  2. Otherwise learn first: record that src is on port at time now, replacing any older entry (a host that moved is found at its new port at once).
  3. Then decide by dst:
    • a group address (broadcast or multicast) is flooded: every port except the one the frame arrived on;
    • a unicast address that lookup finds on another port is forwarded to that port alone;
    • a unicast address that lookup finds on the arrival port is filtered: return [];
    • a unicast address that lookup does not find (unknown or aged out) is flooded.

The sample tests replay six short traces: a first frame and its reply, broadcast and multicast, two hosts behind one port, a host that moves, the exact moment an entry ages out, and an invalid group source address.

Starter code · switch.py

BROADCAST = "ff:ff:ff:ff:ff:ff"


class LearningSwitch:
    """A learning switch with numbered ports and an ageing time in seconds."""

    def __init__(self, ports, ageing=300):
        self.ports = sorted(ports)
        self.ageing = ageing
        self.table = {}  # MAC address -> (port, time it was last seen as a source)

    def lookup(self, mac, now):
        """Return the port where mac was last seen, or None if it is unknown or its entry has aged out."""
        # Replace this line with your code.
        return None

    def receive(self, port, src, dst, now):
        """Learn from one frame that arrived on port, and return the sorted list of ports it is sent out of."""
        # Replace this line with your code.
        return []
The sample tests · test_switch.py
from switch import BROADCAST, LearningSwitch

A, B, C, D, E = (f"00:00:5e:00:53:0{x}" for x in "abcde")
GROUP = "01:00:5e:90:10:01"


def test_first_frame_and_reply():
    """floods a frame for an unknown host, then forwards the reply and the next frame"""
    sw = LearningSwitch([1, 2, 3, 4])
    assert sw.receive(1, A, B, 0) == [2, 3, 4]
    assert sw.lookup(A, 0) == 1 and sw.lookup(B, 0) is None
    assert sw.receive(2, B, A, 1) == [1]
    assert sw.receive(1, A, B, 2) == [2]


def test_broadcast_and_multicast():
    """floods broadcast and multicast frames even when the switch knows other hosts"""
    sw = LearningSwitch([4, 1, 3, 2])
    sw.receive(1, A, B, 0)
    sw.receive(2, B, A, 0)
    assert sw.receive(3, C, BROADCAST, 1) == [1, 2, 4]
    assert sw.receive(2, B, GROUP, 2) == [1, 3, 4]
    assert sw.lookup(C, 2) == 3


def test_two_hosts_behind_one_port():
    """filters a frame whose destination is on the port it came from"""
    sw = LearningSwitch([1, 2, 3, 4])
    assert sw.receive(4, D, C, 0) == [1, 2, 3]
    assert sw.receive(4, E, D, 1) == []
    assert sw.receive(1, A, E, 2) == [4]


def test_host_moves():
    """moves an entry to the port where its address shows up next"""
    sw = LearningSwitch([1, 2, 3, 4, 5])
    sw.receive(2, B, A, 0)
    sw.receive(1, A, B, 1)
    assert sw.receive(5, B, A, 20) == [1]
    assert sw.lookup(B, 20) == 5
    assert sw.receive(1, A, B, 21) == [5]


def test_ageing_boundary():
    """keeps an entry until ageing seconds after it was last refreshed, and not one moment longer"""
    sw = LearningSwitch([1, 2, 3], ageing=300)
    sw.receive(1, A, B, 10)
    assert sw.lookup(A, 309) == 1
    assert sw.lookup(A, 310) is None
    sw.receive(1, A, B, 200)
    assert sw.lookup(A, 499) == 1
    assert sw.receive(2, B, A, 499) == [1]
    assert sw.receive(3, C, A, 500) == [1, 2]

    short = LearningSwitch([1, 2, 3], ageing=15)
    short.receive(1, A, B, 0)
    assert short.receive(2, B, A, 14) == [1]
    assert short.receive(2, B, A, 15) == [1, 3]


def test_group_source_is_dropped():
    """drops a frame from a group source address and learns nothing from it"""
    sw = LearningSwitch([1, 2, 3])
    assert sw.receive(1, GROUP, A, 0) == []
    assert sw.receive(2, BROADCAST, A, 0) == []
    assert sw.lookup(GROUP, 0) is None and sw.lookup(BROADCAST, 0) is None
    assert sw.receive(3, A, GROUP, 1) == [1, 2]
A hint

int(mac[:2], 16) & 1 is 1 for a group address and 0 for a unicast one. In lookup, take the entry with self.table.get(mac): it is (port, seen) or None, and it is still fresh while now - seen < self.ageing. In receive, the flood list is [p for p in self.ports if p != port].

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

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

  1. Question 1 of 13 Six eligible ports exist and one is ingress. How many carry the stated unknown unicast?

    Type a number.

    Show the answer: Question 1 of 13

    Answer: 5 ports

    The frame leaves the other five ports.

  2. Question 2 of 13 A destination is learned on the arrival port. How many egress ports does the stated filter decision select?

    Type a number.

    Show the answer: Question 2 of 13

    Answer: 0 ports

    Filtering chooses no egress port.

  3. Question 3 of 13 The same host then transmits from port 7. What port does the next source-based lookup return?

    Type a number.

    Show the answer: Question 3 of 13

    Answer: 7 port

    The stated newest source location is port 7.

  4. Question 4 of 13 Choose among the original ageing times 15, 60, 300 and 1800 seconds. Which removes flooding for the 400-second sensor interval, if an 1800-second silent-move wait is acceptable?

    Type a number.

    Show the answer: Question 4 of 13

    Answer: 1800 seconds

    The original record shows zero flooded frames at 1800 seconds, with that longer worst-case wait after a silent move; the other three intervals still flood.

  5. Question 5 of 13 In the final original A-to-E frame, how many links carry it, including A's own cable and the link from S3 to E?

    Type a number.

    Show the answer: Question 5 of 13

    Answer: 4 links

    The learned path is A-to-S1, S1-to-S2, S2-to-S3 and S3-to-E: four links without unwanted copies.

  6. Question 6 of 13 A comparable model has eight switch links plus one shared hub segment. What traditional total does it count?

    Type a number.

    Show the answer: Question 6 of 13

    Answer: 9 domains

    The stated grouping gives eight plus one.

  7. Question 7 of 13 A stated 1,000-entry table already has 100 entries. How many slots remain?

    Type a number.

    Show the answer: Question 7 of 13

    Answer: 900 entries

    One thousand minus one hundred is nine hundred.

  8. Question 8 of 13 A switch has just started, so its MAC address table is empty. Host A on port 1 sends a frame to host B. What does the switch do?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: It records A on port 1 and floods the frame out of every other port

    The switch always learns from the source first, so A is now known on port 1. The destination B is unknown, so the frame is flooded to every other port of the VLAN. When B replies, the switch learns B's port and sends the reply out of port 1 only.

  9. Question 9 of 13 Which frames does a learning switch flood out of every port except the arrival port?

    Choose every answer that is right.

    Show the answer: Question 9 of 13

    Answer:

    • A unicast frame for an address whose entry has just aged out
    • A unicast frame for an address that is not in the table
    • A broadcast to ff:ff:ff:ff:ff:ff

    Broadcasts are always flooded, and so is any unicast frame without a usable entry: one that was never learned or whose entry aged out. A fresh entry on another port means forwarding out of that port alone; an entry on the arrival port means the frame is filtered.

  10. Question 10 of 13 A laptop's entry says port 3. The laptop is plugged into port 7 and sends a frame at once. What happens to its entry?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: It moves to port 7 immediately, because the frame's source address was seen there

    Learning happens on every frame: a known address that shows up on another port is moved to that port. Ageing only matters when a host stays silent; port security, if configured, may treat a move as a violation, but plain learning simply updates the entry.

  11. Question 11 of 13 A router connects to a 24-port switch with 16 PCs and the router's own link plugged in, and to a hub with 6 PCs on it. How many collision domains are there?

    Type a number.

    Show the answer: Question 11 of 13

    Answer: 18 collision domains

    Every switch port in use is its own collision domain: 16 PC ports plus the port to the router make 17. The hub, its 6 PCs and the router interface on that side share a single collision domain, so there are 17 + 1 = 18. The router also makes two broadcast domains, one per interface.

  12. Question 12 of 13 A server only ever receives a one-way video stream on its network and never sends a frame there. What do the switches do with the stream?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: They flood it, because they never see the server's address as a source and so never learn its port

    Switches learn only from source addresses. A destination that never sends anything is never in the table, so every frame for it is unknown unicast and is flooded through the VLAN. Cisco's notes on unicast flooding describe this effect in campus networks with asymmetric routing.

  13. Question 13 of 13 Put the steps a learning switch takes for each frame in order.

    Give each item its position, from 1 (first).

    Show the answer: Question 13 of 13

    Answer:

    1. Receive the frame on a port
    2. Record or refresh the source address against that port
    3. Look up the destination address
    4. Forward, filter or flood the frame

    Learning comes before the decision, so even a frame that is then filtered teaches the switch where its sender is. The lookup uses only entries that have not aged out.

Interview questions

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

  1. Mid to senior

    A monitoring PC on an access port keeps capturing a large unicast stream between two servers it has nothing to do with, and every port of that VLAN carries it. The servers' replies return through a different router from their requests. Explain why the switches flood the stream, how you would confirm it, and two ways to stop it.

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

  2. Fresher to mid

    A lab switch with a 16,000-entry MAC address table serves 40 PCs. A faulty test program on one PC starts sending frames from a new random source address 10,000 times a second. How long until the table is full, what do the other users notice, and which switch settings would have limited the damage?

    Model answer, Pro. First step and follow-ups

What does a switch do with a frame whose destination MAC address is not in its table? Under the stated flood policy, a copy goes out of every eligible forwarding port in the frame’s VLAN except ingress. Before deciding, the switch has already learned the frame’s source address on the arrival port, so when the destination answers, the reply leaves by that one port and the destination is learned in turn. From then on, frames between the two hosts use a single port each way. Broadcasts, multicast frames the switch has no group information for, and unicast frames whose entry has aged out are flooded the same way. Hosts that are not the destination simply discard their copies.

How many collision domains and broadcast domains does a 24-port switch create with all 24 ports in use? With all ports in one VLAN, the conventional count is 24 collision domains, one per port, and one broadcast domain. Hosts on different ports can send at the same moment without colliding, and on full-duplex links there are no collisions at all. A broadcast, though, is flooded to every port, so all 24 ports form one broadcast domain until a router or VLANs split it. A hub with 24 ports, by contrast, would be one collision domain.

Key takeaways

  • A switch learns from source addresses: each frame records which port its sender is behind, and the timer of a known address restarts.
  • It forwards a frame for a known address out of one port, filters a frame whose destination is on the arrival port, and floods broadcasts, unknown group addresses, and unicast frames for unknown or aged-out addresses.
  • Entries age out, 300 seconds by default on a Linux bridge and on Catalyst 9300 switches: shorter ageing floods more, longer ageing leaves a silently moved host unreachable for longer.
  • In a multi-switch network each switch applies the same rules to its own table; trace a frame hop by hop.
  • Every switch port is its own collision domain; a broadcast domain ends at a router or a VLAN boundary.
  • At a configured cap that stops learning, unknown unicast follows the VLAN’s flooding policy; hardware admission and replacement policies vary, and per-port address limits can contain excess learning.

Terms in this lesson

Forwarding entry age
The elapsed time since the source observation that last refreshed a dynamic forwarding entry, compared with its configured ageing rule.
Source address learning
The update that records the port and time where a valid source address was most recently observed in a bridge forwarding context.
Switch filter decision
A forwarding decision that selects no outgoing port when the usable destination entry points to the frame arrival port.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

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