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 4 – Medium access control and Ethernet

Ethernet frames and MAC addresses

Every field of an Ethernet II frame and its size limits, EtherType or length, VLAN tags, the flag bits of a MAC address, and what randomised addresses break.

  • 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

  • Identify every field of an Ethernet II frame and its size limits
  • Decode a MAC address, its OUI and its individual-or-group and universal-or-local bits
  • Recognise EtherType values for IPv4, IPv6, ARP and VLAN tags
  • Explain why phones and laptops use randomised MAC addresses and what that breaks

Before you start

What you’ll learn · Before you start

What you will learn

  • Identify every field of an Ethernet II frame and its size limits
  • Decode a MAC address, its OUI and its individual-or-group and universal-or-local bits
  • Recognise EtherType values for IPv4, IPv6, ARP and VLAN tags
  • Explain why phones and laptops use randomised MAC addresses and what that breaks

Before you start

On this page

The original three 64-byte frames show an ARP broadcast, a VLAN 100 experimental frame and a length frame with padding. Read their actual fields and FCS, interpret the first-octet flags, and distinguish interface limits from a path’s packet-size contract.

Frame bytes and line occupancy use different boundaries

An Ethernet frame is a short header, the data and a checksum: a 6-byte destination address, a 6-byte source address, a 2-byte field that says what the data is (the EtherType, such as 0x0800 for IPv4), 46 to 1,500 bytes of data and a 4-byte frame check sequence (FCS). Untagged frames span 64 to 1,518 bytes. One VLAN tag raises the usual maximum to 1,522 while the minimum stays 64; the tag uses four bytes that would otherwise be data, so a minimum tagged frame has 42 data bytes. The addresses are MAC addresses, 48 bits written as six hexadecimal bytes, and two bits of their first byte say whether the address names one interface or a group, and whether it comes from a manufacturer’s block or was set locally. Phones and laptops now pick random local addresses on Wi-Fi for privacy, which quietly breaks anything that recognises devices by their MAC address.

The format here is Ethernet II, the frame of the 1982 Ethernet specification of Digital, Intel and Xerox. It is how IPv4 and IPv6 packets travel over Ethernet, and IEEE 802.3 defines the same layout.

An Ethernet II frame: destination 6 bytes, source 6, optional 4-byte tag, type or length 2, data 46 to 1,500, frame check sequence 4.An Ethernet II frame on the wirepreamble and SFD8 Bdestination address6 Bsource address6 B802.1Q tag, if any4 BEtherType or length2 Bdatapadded if shorter46 to 1,500 Bframe check sequence4 Binterframe gap12 Bthe frame:64 to 1,518 B,1,522 B witha tagDashed: not always there, or not part of the frame.

The fields of an Ethernet II frame

Text description of the diagram

The fields from top to bottom, in the order they are sent:

  1. Preamble and start-of-frame delimiter, 8 bytes. They let the receiver lock on to the signal and are not counted as part of the frame.
  2. Destination address, 6 bytes.
  3. Source address, 6 bytes.
  4. An 802.1Q VLAN tag, 4 bytes, present only on tagged frames.
  5. EtherType or length, 2 bytes.
  6. Data, 46 to 1,500 bytes; shorter data is padded to 46 bytes.
  7. Frame check sequence, a 4-byte CRC-32 of the fields from the destination address to the end of the data.
  8. The interframe gap, 12 byte times of silence before the next frame.

The frame itself runs from the destination address to the frame check sequence: 64 to 1,518 bytes, or up to 1,522 bytes with one VLAN tag.

  • Preamble and start-of-frame delimiter, 8 bytes of a fixed bit pattern. They let the receiver lock on to the signal and find where the frame begins, and they are not counted as part of the frame.
  • Destination address first, so that every receiver can decide as early as possible whether the frame is for it.
  • Source address, the sender’s own address.
  • EtherType or length, 2 bytes, explained in the next section.
  • Data, 46 to 1,500 bytes. Data shorter than 46 bytes is padded with zero bytes up to 46. The padding belongs to Ethernet, not to the packet inside: an IPv4 packet carries its own length, so the receiver knows where it ends.
  • Frame check sequence, a CRC-32 computed over everything from the destination address to the end of the data. The receiver computes it again, and a frame whose FCS does not match is thrown away without any message.

After the FCS the line stays quiet for an interframe gap of 96 bit times, 12 byte times, before the next frame may start. The line occupancy: The transmission and spacing time attributed to a frame under a stated wire model, including preamble and gap costs outside the frame byte count. includes preamble and gap time on the wire even though they are not part of the frame:

What frames cost on a 1 Gb/s link Python · original-wire-time/wire_time.py
"""What a frame really costs on the wire.

Every frame is sent after an 8-byte preamble and start-of-frame delimiter, and is
followed by an interframe gap of 12 byte times. Frame sizes include the header
and the frame check sequence.
"""

PREAMBLE_AND_SFD = 8
GAP = 12
RATE = 1_000_000_000          # 1 Gb/s

CASES = [
    # name, frame bytes, payload bytes
    ("smallest", 64, 46),
    ("largest", 1518, 1500),
    ("largest, VLAN-tagged", 1522, 1500),
    ("jumbo, 9000-byte MTU", 9018, 9000),
]

print("frame                  on wire   frames/s at 1 Gb/s  payload")
for name, size, payload in CASES:
    wire = size + PREAMBLE_AND_SFD + GAP
    print(f"{name:21} {wire:6,} B  {RATE // (wire * 8):18,}  {payload / wire:6.1%}")

Output

frame                  on wire   frames/s at 1 Gb/s  payload
smallest                  84 B           1,488,095   54.8%
largest                1,538 B              81,274   97.5%
largest, VLAN-tagged   1,542 B              81,063   97.3%
jumbo, 9000-byte MTU   9,038 B              13,830   99.6%

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

A minimum-size frame occupies 84 byte times, so a gigabit link can carry at most 1,488,095 of them a second, and only 54.8 % of that time is data. Full-size frames put 97.5 % of the line’s time into data, and 9,000-byte jumbo frames 99.6 %.

Count the original 64 frame bytes separately from its 84 byte times on the line.

Interactive · stage by stage Frame bytes and line occupancy use different boundaries Count the original 64 frame bytes separately from its 84 byte times on the line.
Frame bytes and line occupancy use different boundaries, step 3 of 3. Showing Original minimum, Outside the frame, Original full payload. In focus: Original full payload.Original minimum64-byte frameOutside the frameEight plus twelveOriginal full payload1,538 byte times

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

Every step as text
  1. The original untagged frame counts destination, source, type/length, data and FCS from the destination address onward.
  2. Eight preamble/SFD bytes and twelve gap byte times add to its original line occupancy.
  3. The original 1,518-byte untagged frame occupies 1,538 byte times including those twenty outside byte times.

Checkpoint

Check: Frame bytes and line occupancy use different boundaries

  1. Question 1 of 2 Are the preamble and gap counted in the original 64-byte frame size?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, they are additional line occupancy

    The original boundary starts at the destination address. Preamble and gap occur outside that declared frame boundary.

  2. Question 2 of 2 The original 64-byte frame adds eight preamble/SFD and twelve gap byte times. What is its occupancy?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 84 byte times

    Sixty-four plus eight plus twelve is eighty-four.

The field threshold and VLAN position determine payload parsing

The 2-byte field after the source address has two meanings separated by the type/length interval: The value ranges that distinguish an Ethernet length field from an EtherType, including the reserved values between the two ranges.:

  • 0x0600 (1,536) or more: it is an EtherType, naming the protocol of the data.
  • 1,500 (0x05DC) or less: it is a length, the number of data bytes, as the IEEE 802.3 frame format allowed. The data then starts with a short LLC header, two one-byte protocol numbers and a control byte, which says what follows. Spanning tree, which a later lesson covers, still sends its frames this way.
  • 1,501 to 1,535 is neither, so a frame with such a value is malformed.

The EtherTypes you meet most often, from the IANA list of IEEE 802 numbers:

EtherType Protocol
0x0800 IPv4
0x0806 ARP, which finds the MAC address for an IPv4 address
0x86DD IPv6
0x8100 an IEEE 802.1Q VLAN tag (the C-tag)
0x88A8 an outer service VLAN tag (the S-tag)
0x88CC LLDP, which switches use to announce themselves to their neighbours
0x8863, 0x8864 PPPoE discovery and session, used by many broadband connections
0x88B5, 0x88B6 reserved for local experiments

A VLAN tag is four extra bytes between the source address and the EtherType: the value 0x8100, then a 16-bit tag control field holding a 3-bit priority, a 1-bit drop-eligible indicator and a 12-bit VLAN ID. A switch uses the VLAN ID to keep separate virtual networks apart on the same cables. A tagged frame can be up to 1,522 bytes long, and a provider network can wrap a frame in a second, outer tag with EtherType 0x88A8. VLANs have a lesson of their own later in the track.

Here are three frames built for this lesson, in hexadecimal as a capture tool would show them, but with the FCS still on the end. Their made-up hosts use the addresses reserved for documentation, except the random-looking local address in frame 2. The example reads each field, follows a VLAN tag, tells an EtherType from a length, and then checks the FCS:

Reading three frames byte by byte Python · original-read-frames/read_frames.py
"""Three Ethernet frames, field by field.

The frames were built for this lesson, mostly with addresses reserved for
documentation (RFC 9542), and are written in hex the way a capture tool shows
them, but with the frame check sequence (FCS) still at the end. Each is 64 bytes,
the minimum.
"""
import zlib

FRAMES = [
    "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
    "000000000000c00002020000000000000000000000000000000000008e30fb7f",
    "01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c41"
    "4e20313030000000000000000000000000000000000000000000000032937702",
    "0180c200000000005e00530a00264242030000000000800000005e00530a0000"
    "0000800000005e00530a80010000140002000f000000000000000000d4ae4f93",
]
NAMES = {0x0800: "IPv4", 0x0806: "ARP", 0x86DD: "IPv6", 0x88B5: "local experimental"}


def address(raw):
    text = ":".join(f"{byte:02x}" for byte in raw)
    if raw == b"\xff" * 6:
        return f"{text}  broadcast: every station"
    kind = "group" if raw[0] & 0b01 else "individual"
    admin = "local" if raw[0] & 0b10 else "universal"
    return f"{text}  {kind}, {admin}"


def fcs_matches(frame):
    return zlib.crc32(frame[:-4]).to_bytes(4, "little") == frame[-4:]


for number, text in enumerate(FRAMES, 1):
    frame = bytes.fromhex(text)
    print(f"frame {number}, {len(frame)} bytes")
    print(f"  destination {address(frame[0:6])}")
    print(f"  source      {address(frame[6:12])}")
    at = 12
    field = int.from_bytes(frame[at:at + 2])
    if field == 0x8100:
        tci = int.from_bytes(frame[at + 2:at + 4])
        print(f"  802.1Q tag  priority {tci >> 13}, VLAN {tci & 0x0FFF}")
        at += 4
        field = int.from_bytes(frame[at:at + 2])
    at += 2
    data = len(frame) - 4 - at
    if field >= 0x0600:
        print(f"  EtherType   0x{field:04x} ({NAMES.get(field, 'unknown')}), {data} bytes of data")
    else:
        print(f"  length      {field}: {field} bytes of LLC data, {data - field} of padding")
        print(f"  LLC header  DSAP 0x{frame[at]:02x}, SSAP 0x{frame[at + 1]:02x}, control 0x{frame[at + 2]:02x}")
    print(f"  FCS         {frame[-4:].hex()}, {'correct' if fcs_matches(frame) else 'wrong'}")

damaged = bytearray(bytes.fromhex(FRAMES[0]))
damaged[25] ^= 0b00000100          # one bit changed on the way
print(f"frame 1 with one bit changed: FCS {'correct' if fcs_matches(damaged) else 'wrong, so it is dropped'}")

Output

frame 1, 64 bytes
  destination ff:ff:ff:ff:ff:ff  broadcast: every station
  source      00:00:5e:00:53:01  individual, universal
  EtherType   0x0806 (ARP), 46 bytes of data
  FCS         8e30fb7f, correct
frame 2, 64 bytes
  destination 01:00:5e:90:10:01  group, universal
  source      3a:7f:12:c4:9e:01  individual, local
  802.1Q tag  priority 0, VLAN 100
  EtherType   0x88b5 (local experimental), 42 bytes of data
  FCS         32937702, correct
frame 3, 64 bytes
  destination 01:80:c2:00:00:00  group, universal
  source      00:00:5e:00:53:0a  individual, universal
  length      38: 38 bytes of LLC data, 8 of padding
  LLC header  DSAP 0x42, SSAP 0x42, control 0x03
  FCS         d4ae4f93, correct
frame 1 with one bit changed: FCS wrong, so it is dropped

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

One program · 14 languages Parse the original addresses, tag, type and FCS O(n) for n frame bytes

Python 3.14 · main.py

import zlib


def fields(f: bytes) -> tuple[str, str, int, int, int]:
    o, v = 12, 0
    k = int.from_bytes(f[o : o + 2], "big")
    if k == 0x8100:
        v = int.from_bytes(f[o + 2 : o + 4], "big") & 0x0FFF
        o += 4
        k = int.from_bytes(f[o : o + 2], "big")
    size = len(f) - 4 - o - 2
    return f[:6].hex(":"), f[6:12].hex(":"), v, k, size


def fcs_matches(f: bytes | bytearray) -> bool:
    return zlib.crc32(f[:-4]).to_bytes(4, "little") == f[-4:]


data = (
    "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
    "000000000000c00002020000000000000000000000000000000000008e30fb7f"
    " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4"
    "14e2031303000000000000000000000000000000000000000000000003293770"
    "2 0180c200000000005e00530a00264242030000000000800000005e00530a00"
    "000000800000005e00530a80010000140002000f000000000000000000d4ae4f"
    "93"
)
a = [bytes.fromhex(line) for line in data.split()]
for n, f in enumerate(a, 1):
    dst, src, v, k, size = fields(f)
    cat = "type" if k >= 1536 else "length" if k <= 1500 else "reserved"
    valid = 1 if fcs_matches(f) else 0
    print(f"{n}: dst={dst} src={src} vlan={v} {cat}=0x{k:04x} data={size} fcs={valid}")
d = bytearray(a[0])
d[25] ^= 4
print(f"damaged: fcs={1 if fcs_matches(d) else 0}")

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 h = (bytes) => bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(":");
const u = (f, o) => (f[o] << 8) | f[o + 1];
function fcs(f) {
  let r = 0xffffffff;
  for (const byte of f.slice(0, -4)) {
    r ^= byte;
    for (let bit = 0; bit < 8; bit++) {
      r = r & 1 ? (r >>> 1) ^ 0xedb88320 : r >>> 1;
    }
  }
  r = (r ^ 0xffffffff) >>> 0;
  return f.slice(-4).every((byte, i) => byte === ((r >>> (i * 8)) & 255));
}
const input =
  "ffffffffffff00005e0053010806000108000604000100005e005301c0000201" +
  "000000000000c00002020000000000000000000000000000000000008e30fb7f" +
  " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4" +
  "14e2031303000000000000000000000000000000000000000000000003293770" +
  "2 0180c200000000005e00530a00264242030000000000800000005e00530a00" +
  "000000800000005e00530a80010000140002000f000000000000000000d4ae4f" +
  "93";
const a = input
  .split(/\s+/)
  .map((text) => [...text.matchAll(/../g)].map((match) => Number.parseInt(match[0], 16)));
for (const [n, f] of a.entries()) {
  let o = 12;
  let v = 0;
  let k = u(f, o);
  if (k === 0x8100) {
    v = u(f, o + 2) & 0x0fff;
    o += 4;
    k = u(f, o);
  }
  const cat = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved";
  const prefix = `${n + 1}: dst=${h(f.slice(0, 6))} src=${h(f.slice(6, 12))}`;
  console.log(
    `${prefix} vlan=${v} ${cat}=0x${k.toString(16).padStart(4, "0")} ` +
      `data=${f.length - 4 - o - 2} fcs=${Number(fcs(f))}`,
  );
}
const d = [...a[0]];
d[25] ^= 4;
console.log(`damaged: fcs=${Number(fcs(d))}`);

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.Locale;
import java.util.zip.CRC32;

public class Main {
  static String m(byte[] f, int p) {
    String s = "";
    for (int i = p; i < p + 6; i++) s += (i == p ? "" : ":") + String.format("%02x", f[i] & 255);
    return s;
  }

  static int u(byte[] f, int o) {
    return (f[o] & 255) << 8 | (f[o + 1] & 255);
  }

  static boolean fcs(byte[] f) {
    var r = new CRC32();
    r.update(f, 0, f.length - 4);
    int v = 0;
    for (int i = 0; i < 4; i++) v |= (f[f.length - 4 + i] & 255) << (8 * i);
    return Integer.toUnsignedLong(v) == r.getValue();
  }

  public static void main(String[] a) {
    Locale.setDefault(Locale.ROOT);
    String data =
        "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
            .concat("000000000000c00002020000000000000000000000000000000000008e30fb7f")
            .concat(" 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4")
            .concat("14e2031303000000000000000000000000000000000000000000000003293770")
            .concat("2 0180c200000000005e00530a00264242030000000000800000005e00530a00")
            .concat("000000800000005e00530a80010000140002000f000000000000000000d4ae4f")
            .concat("93");
    byte[] b = null;
    int n = 0;
    for (String s : data.split(" ")) {
      byte[] f = new byte[s.length() / 2];
      for (int i = 0; i < f.length; i++)
        f[i] = (byte) Integer.parseInt(s.substring(2 * i, 2 * i + 2), 16);
      if (n == 0) b = f;
      int o = 12, v = 0, k = u(f, o);
      if (k == 0x8100) {
        v = u(f, o + 2) & 0x0fff;
        o += 4;
        k = u(f, o);
      }
      String t = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved";
      System.out.printf(
          "%d: dst=%s src=%s vlan=%d %s=0x%04x data=%d fcs=%d%n",
          ++n, m(f, 0), m(f, 6), v, t, k, f.length - 4 - o - 2, fcs(f) ? 1 : 0);
    }
    b[25] ^= 4;
    System.out.println("damaged: fcs=" + (fcs(b) ? 1 : 0));
  }
}

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 <cstdint>
#include <format>
#include <print>
#include <span>
#include <sstream>
#include <string>
#include <vector>

using Byte = std::uint8_t;
using Size = std::size_t;
using Bytes = std::span<const Byte>;

std::string m(Bytes b) {
  return std::format("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b[0], b[1],
                     b[2], b[3], b[4], b[5]);
}

unsigned u(Bytes f, Size o) { return unsigned(f[o]) << 8 | f[o + 1]; }

bool fcs(Bytes f) {
  std::uint32_t r = 0xffffffff;
  for (const auto byte : f.first(f.size() - 4)) {
    r ^= byte;
    for (int bit = 0; bit < 8; ++bit) {
      r = r & 1 ? (r >> 1) ^ 0xedb88320 : r >> 1;
    }
  }
  std::uint32_t stored = 0;
  for (Size i = 0; i < 4; ++i)
    stored |= std::uint32_t(f[f.size() - 4 + i]) << (8 * i);
  return stored == (r ^ 0xffffffff);
}

int main() {
  using Frame = std::vector<Byte>;
  Frame first;
  Size n = 0;
  std::istringstream input(
      "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
      "000000000000c00002020000000000000000000000000000000000008e30fb7f"
      " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4"
      "14e2031303000000000000000000000000000000000000000000000003293770"
      "2 0180c200000000005e00530a00264242030000000000800000005e00530a00"
      "000000800000005e00530a80010000140002000f000000000000000000d4ae4f"
      "93");
  for (std::string text; input >> text;) {
    Frame f;
    for (Size o = 0; o < text.size(); o += 2) {
      f.push_back(
          static_cast<Byte>(std::stoul(text.substr(o, 2), nullptr, 16)));
    }
    if (n == 0)
      first = f;
    Size o = 12;
    unsigned v = 0, k = u(f, o);
    if (k == 0x8100) {
      v = u(f, o + 2) & 0x0fff;
      o += 4;
      k = u(f, o);
    }
    const auto cat = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved";
    std::println("{}: dst={} src={} vlan={} {}=0x{:04x} data={} fcs={}", n + 1,
                 m(Bytes(f).first(6)), m(Bytes(f).subspan(6, 6)), v, cat, k,
                 f.size() - 4 - o - 2, int(fcs(f)));
    ++n;
  }
  auto d = first;
  d[25] ^= 4;
  std::println("damaged: fcs={}", int(fcs(d)));
}

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;

string input =
    "ffffffffffff00005e0053010806000108000604000100005e005301c0000201" +
    "000000000000c00002020000000000000000000000000000000000008e30fb7f" +
    " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4" +
    "14e2031303000000000000000000000000000000000000000000000003293770" +
    "2 0180c200000000005e00530a00264242030000000000800000005e00530a00" +
    "000000800000005e00530a80010000140002000f000000000000000000d4ae4f" +
    "93";
var a = input.Split().Select(Convert.FromHexString).ToArray();
for (int n = 0; n < a.Length; n++)
{
    byte[] f = a[n];
    int o = 12, v = 0, k = U(f, o);
    if (k == 0x8100)
    {
        v = U(f, o + 2) & 0x0fff;
        o += 4;
        k = U(f, o);
    }
    string cat = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved";
    Console.WriteLine($"{n + 1}: dst={M(f[..6])} src={M(f[6..12])} "
        + $"vlan={v} {cat}=0x{k:x4} data={f.Length - 4 - o - 2} "
        + $"fcs={(Fcs(f) ? 1 : 0)}");
}
byte[] d = [.. a[0]];
d[25] ^= 4;
Console.WriteLine("damaged: fcs=" + (Fcs(d) ? 1 : 0));

static int U(byte[] f, int o) => f[o] << 8 | f[o + 1];
static string M(byte[] raw) => string.Join(":", raw.Select(sum => $"{sum:x2}"));

static bool Fcs(byte[] f)
{
    uint r = 0xffffffff;
    foreach (byte sum in f[..^4])
    {
        r ^= sum;
        for (int bit = 0; bit < 8; bit++)
        {
            r = (r & 1) != 0 ? (r >> 1) ^ 0xedb88320 : r >> 1;
        }
    }
    r ^= 0xffffffff;
    for (int i = 0; i < 4; i++)
    {
        if (f[f.Length - 4 + i] != ((r >> (i * 8)) & 255))
        {
            return false;
        }
    }
    return true;
}

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

C23 · main.c

#include <stdint.h>
#include <stdio.h>
#include <string.h>

static unsigned u(const uint8_t *f, size_t o) {
  return (unsigned)f[o] << 8 | f[o + 1];
}

static void m(const uint8_t *bytes, char *text) {
  snprintf(text, 18, "%02x:%02x:%02x:%02x:%02x:%02x", bytes[0], bytes[1],
           bytes[2], bytes[3], bytes[4], bytes[5]);
}

static int fcs(const uint8_t *f, size_t size) {
  uint32_t r = 0xffffffff;
  for (size_t i = 0; i < size - 4; ++i) {
    r ^= f[i];
    for (unsigned bit = 0; bit < 8; ++bit) {
      r = r & 1 ? (r >> 1) ^ 0xedb88320 : r >> 1;
    }
  }
  r ^= 0xffffffff;
  for (size_t i = 0; i < 4; ++i) {
    if (f[size - 4 + i] != ((r >> (i * 8)) & 255)) {
      return 0;
    }
  }
  return 1;
}

int main(void) {
  char text[129];
  uint8_t first[64];
  size_t n = 0;
  const char *input =
      "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
      "000000000000c00002020000000000000000000000000000000000008e30fb7f"
      " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4"
      "14e2031303000000000000000000000000000000000000000000000003293770"
      "2 0180c200000000005e00530a00264242030000000000800000005e00530a00"
      "000000800000005e00530a80010000140002000f000000000000000000d4ae4f"
      "93";
  int consumed;
  while (sscanf(input, "%128s%n", text, &consumed) == 1) {
    input += consumed;
    uint8_t f[64];
    const size_t size = strlen(text) / 2;
    for (size_t i = 0; i < size; ++i) {
      unsigned byte;
      sscanf(text + i * 2, "%2x", &byte);
      f[i] = (uint8_t)byte;
    }
    if (n == 0) {
      memcpy(first, f, size);
    }
    size_t o = 12;
    unsigned v = 0, k = u(f, o);
    if (k == 0x8100) {
      v = u(f, o + 2) & 0x0fff;
      o += 4;
      k = u(f, o);
    }
    const char *cat = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved";
    char dst[18], src[18];
    m(f, dst);
    m(f + 6, src);
    printf("%zu: dst=%s src=%s vlan=%u %s=0x%04x data=%zu fcs=%d\n", ++n, dst,
           src, v, cat, k, size - 4 - o - 2, fcs(f, size));
  }
  first[25] ^= 4;
  printf("damaged: fcs=%d\n", fcs(first, sizeof first));
  return 0;
}

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

PHP 8.5 · main.php

<?php

declare(strict_types=1);

function u(string $f, int $o): int
{
    return ord($f[$o]) << 8 | ord($f[$o + 1]);
}

function m(string $raw): string
{
    return implode(':', str_split(bin2hex($raw), 2));
}

function fcs(string $f): bool
{
    return strrev(hash('crc32b', substr($f, 0, -4), true)) === substr($f, -4);
}

$input = 'ffffffffffff00005e0053010806000108000604000100005e005301c0000201'
    . '000000000000c00002020000000000000000000000000000000000008e30fb7f'
    . ' 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4'
    . '14e2031303000000000000000000000000000000000000000000000003293770'
    . '2 0180c200000000005e00530a00264242030000000000800000005e00530a00'
    . '000000800000005e00530a80010000140002000f000000000000000000d4ae4f'
    . '93';
$a = array_map('hex2bin', explode(' ', $input));
foreach ($a as $n => $f) {
    $o = 12;
    $v = 0;
    $k = u($f, $o);
    if ($k === 0x8100) {
        $v = u($f, $o + 2) & 0x0fff;
        $o += 4;
        $k = u($f, $o);
    }
    $cat = $k >= 1536 ? 'type' : ($k <= 1500 ? 'length' : 'reserved');
    printf(
        "%d: dst=%s src=%s vlan=%d %s=0x%04x data=%d fcs=%d\n",
        $n + 1,
        m(substr($f, 0, 6)),
        m(substr($f, 6, 6)),
        $v,
        $cat,
        $k,
        strlen($f) - 4 - $o - 2,
        (int) fcs($f),
    );
}
$d = $a[0];
$d[25] = chr(ord($d[25]) ^ 4);
echo 'damaged: fcs=', (int) fcs($d), "\n";

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

Rust 1.99 · main.rs

fn m(raw: &[u8]) -> String {
    raw.iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<Vec<_>>()
        .join(":")
}

fn u(f: &[u8], o: usize) -> u16 {
    u16::from(f[o]) << 8 | u16::from(f[o + 1])
}

fn fcs(f: &[u8]) -> bool {
    let mut r = 0xffffffff_u32;
    for &byte in &f[..f.len() - 4] {
        r ^= u32::from(byte);
        for _ in 0..8 {
            r = if r & 1 != 0 {
                (r >> 1) ^ 0xedb88320
            } else {
                r >> 1
            };
        }
    }
    f[f.len() - 4..] == (r ^ 0xffffffff).to_le_bytes()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = concat!(
        "ffffffffffff00005e0053010806000108000604000100005e005301c0000201",
        "000000000000c00002020000000000000000000000000000000000008e30fb7f",
        " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4",
        "14e2031303000000000000000000000000000000000000000000000003293770",
        "2 0180c200000000005e00530a00264242030000000000800000005e00530a00",
        "000000800000005e00530a80010000140002000f000000000000000000d4ae4f",
        "93"
    );
    let mut a = Vec::new();
    for text in input.split_ascii_whitespace() {
        let mut f = Vec::new();
        for o in (0..text.len()).step_by(2) {
            f.push(u8::from_str_radix(&text[o..o + 2], 16)?);
        }
        a.push(f);
    }
    for (n, f) in a.iter().enumerate() {
        let (mut o, mut v) = (12, 0);
        let mut k = u(f, o);
        if k == 0x8100 {
            v = u(f, o + 2) & 0x0fff;
            o += 4;
            k = u(f, o);
        }
        let cat = if k >= 1536 {
            "type"
        } else if k <= 1500 {
            "length"
        } else {
            "reserved"
        };
        let size = f.len() - 4 - o - 2;
        println!(
            "{}: dst={} src={} vlan={v} {cat}=0x{k:04x} data={size} fcs={}",
            n + 1,
            m(&f[..6]),
            m(&f[6..12]),
            u8::from(fcs(f))
        );
    }
    let mut d = a[0].clone();
    d[25] ^= 4;
    println!("damaged: fcs={}", u8::from(fcs(&d)));
    Ok(())
}

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 h = (bytes: readonly number[]): string =>
  bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(":");
const u = (f: readonly number[], o: number): number => ((f[o] ?? 0) << 8) | (f[o + 1] ?? 0);
function fcs(f: readonly number[]): boolean {
  let r = 0xffffffff;
  for (const byte of f.slice(0, -4)) {
    r ^= byte;
    for (let bit = 0; bit < 8; bit++) {
      r = r & 1 ? (r >>> 1) ^ 0xedb88320 : r >>> 1;
    }
  }
  r = (r ^ 0xffffffff) >>> 0;
  return f.slice(-4).every((byte, i) => byte === ((r >>> (i * 8)) & 255));
}
const input =
  "ffffffffffff00005e0053010806000108000604000100005e005301c0000201" +
  "000000000000c00002020000000000000000000000000000000000008e30fb7f" +
  " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4" +
  "14e2031303000000000000000000000000000000000000000000000003293770" +
  "2 0180c200000000005e00530a00264242030000000000800000005e00530a00" +
  "000000800000005e00530a80010000140002000f000000000000000000d4ae4f" +
  "93";
const a = input
  .split(/\s+/)
  .map((text) => [...text.matchAll(/../g)].map((match) => Number.parseInt(match[0], 16)));
for (const [n, f] of a.entries()) {
  let o = 12;
  let v = 0;
  let k = u(f, o);
  if (k === 0x8100) {
    v = u(f, o + 2) & 0x0fff;
    o += 4;
    k = u(f, o);
  }
  const cat = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved";
  const prefix = `${n + 1}: dst=${h(f.slice(0, 6))} src=${h(f.slice(6, 12))}`;
  console.log(
    `${prefix} vlan=${v} ${cat}=0x${k.toString(16).padStart(4, "0")} ` +
      `data=${f.length - 4 - o - 2} fcs=${Number(fcs(f))}`,
  );
}
const d = [...(a[0] ?? [])];
d[25] = (d[25] ?? 0) ^ 4;
console.log(`damaged: fcs=${Number(fcs(d))}`);

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 (
	"encoding/hex"
	"fmt"
	"hash/crc32"
	"strings"
)

func m(raw []byte) string {
	parts := make([]string, len(raw))
	for i, sum := range raw {
		parts[i] = fmt.Sprintf("%02x", sum)
	}
	return strings.Join(parts, ":")
}

func u(f []byte, o int) int { return int(f[o])<<8 | int(f[o+1]) }

func fcs(f []byte) int {
	sum := crc32.ChecksumIEEE(f[:len(f)-4])
	for i, actual := range f[len(f)-4:] {
		if actual != byte(sum>>(i*8)) {
			return 0
		}
	}
	return 1
}

func main() {
	input :=
		"ffffffffffff00005e0053010806000108000604000100005e005301c0000201" +
			"000000000000c00002020000000000000000000000000000000000008e30fb7f" +
			" 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4" +
			"14e2031303000000000000000000000000000000000000000000000003293770" +
			"2 0180c200000000005e00530a00264242030000000000800000005e00530a00" +
			"000000800000005e00530a80010000140002000f000000000000000000d4ae4f" +
			"93"
	a := [][]byte{}
	for n, text := range strings.Fields(input) {
		f, err := hex.DecodeString(text)
		if err != nil {
			return
		}
		a = append(a, f)
		o, v, k := 12, 0, u(f, 12)
		if k == 0x8100 {
			v = u(f, o+2) & 0x0fff
			o += 4
			k = u(f, o)
		}
		cat := "reserved"
		switch {
		case k >= 1536:
			cat = "type"
		case k <= 1500:
			cat = "length"
		}
		fmt.Printf("%d: dst=%s src=%s vlan=%d %s=0x%04x data=%d fcs=%d\n",
			n+1, m(f[:6]), m(f[6:12]), v, cat, k,
			len(f)-4-o-2, fcs(f))
	}
	d := append([]byte(nil), a[0]...)
	d[25] ^= 4
	fmt.Printf("damaged: fcs=%d\n", fcs(d))
}

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

Swift 6.4 · main.swift

func m(_ bytes: [UInt8]) -> String {
  bytes.map {
    let text = String($0, radix: 16)
    return String(repeating: "0", count: 2 - text.count) + text
  }.joined(separator: ":")
}

func u(_ f: [UInt8], at o: Int) -> Int {
  Int(f[o]) << 8 | Int(f[o + 1])
}

func fcs(_ f: [UInt8]) -> Int {
  var r: UInt32 = 0xffff_ffff
  for byte in f.dropLast(4) {
    r ^= UInt32(byte)
    for _ in 0..<8 {
      r = r & 1 != 0 ? (r >> 1) ^ 0xedb8_8320 : r >> 1
    }
  }
  r ^= 0xffff_ffff
  return f.suffix(4).enumerated().allSatisfy {
    UInt32($0.element) == ((r >> ($0.offset * 8)) & 255)
  } ? 1 : 0
}

var a: [[UInt8]] = []
let input =
  "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
  + "000000000000c00002020000000000000000000000000000000000008e30fb7f"
  + " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4"
  + "14e2031303000000000000000000000000000000000000000000000003293770"
  + "2 0180c200000000005e00530a00264242030000000000800000005e00530a00"
  + "000000800000005e00530a80010000140002000f000000000000000000d4ae4f" + "93"
for part in input.split(separator: " ") {
  let line = String(part)
  let digits = Array(line)
  var f: [UInt8] = []
  for o in stride(from: 0, to: digits.count, by: 2) {
    guard let byte = UInt8(String(digits[o...o + 1]), radix: 16) else { continue }
    f.append(byte)
  }
  a.append(f)
}
for (n, f) in a.enumerated() {
  var o = 12
  var v = 0
  var k = u(f, at: o)
  if k == 0x8100 {
    v = u(f, at: o + 2) & 0x0fff
    o += 4
    k = u(f, at: o)
  }
  let cat = k >= 1536 ? "type" : k <= 1500 ? "length" : "reserved"
  let digits = String(k, radix: 16)
  let type = String(repeating: "0", count: 4 - digits.count) + digits
  print(
    "\(n + 1): dst=\(m(Array(f.prefix(6)))) src=\(m(Array(f[6..<12]))) "
      + "vlan=\(v) \(cat)=0x\(type) data=\(f.count - 4 - o - 2) fcs=\(fcs(f))")
}
var d = a[0]
d[25] ^= 4
print("damaged: fcs=\(fcs(d))")

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

Ruby 4.0 · main.rb

# frozen_string_literal: true

require 'zlib'

def u(frame, offset)
  frame[offset, 2].unpack1('n')
end

def m(raw)
  raw.bytes.map { |byte| format('%02x', byte) }.join(':')
end

def fcs(frame)
  [Zlib.crc32(frame[0...-4])].pack('V') == frame[-4, 4]
end

def fields(frame)
  o = 12
  v = 0
  k = u(frame, o)
  if k == 0x8100
    v = u(frame, o + 2) & 0x0fff
    o += 4
    k = u(frame, o)
  end
  [v, k, frame.bytesize - 4 - o - 2]
end

input = 'ffffffffffff00005e0053010806000108000604000100005e005301c0000201' \
        '000000000000c00002020000000000000000000000000000000000008e30fb7f' \
        ' 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4' \
        '14e2031303000000000000000000000000000000000000000000000003293770' \
        '2 0180c200000000005e00530a00264242030000000000800000005e00530a00' \
        '000000800000005e00530a80010000140002000f000000000000000000d4ae4f' \
        '93'
a = input.split.map { |text| [text].pack('H*') }
a.each_with_index do |f, n|
  v, k, size = fields(f)
  cat = if k >= 1536
          'type'
        else
          (k <= 1500 ? 'length' : 'reserved')
        end
  puts "#{n + 1}: dst=#{m(f[0, 6])} src=#{m(f[6, 6])} " \
       "vlan=#{v} #{cat}=#{format('0x%04x', k)} data=#{size} fcs=#{fcs(f) ? 1 : 0}"
end
d = a[0].dup
d.setbyte(25, d.getbyte(25) ^ 4)
puts "damaged: fcs=#{fcs(d) ? 1 : 0}"

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

Kotlin 2.4 · Main.kt

fun u(
    f: List<Int>,
    o: Int,
): Int = (f[o] shl 8) or f[o + 1]

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

fun fcs(f: List<Int>): Boolean {
    var r = -1
    for (sum in f.dropLast(4)) {
        r = r xor sum
        repeat(8) {
            r = if (r and 1 != 0) (r ushr 1) xor 0xedb88320.toInt() else r ushr 1
        }
    }
    r = r xor -1
    return f.takeLast(4).withIndex().all { (i, byte) ->
        byte == ((r ushr (i * 8)) and 255)
    }
}

fun main() {
    val input =
        "ffffffffffff00005e0053010806000108000604000100005e005301c0000201" +
            "000000000000c00002020000000000000000000000000000000000008e30fb7f" +
            " 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4" +
            "14e2031303000000000000000000000000000000000000000000000003293770" +
            "2 0180c200000000005e00530a00264242030000000000800000005e00530a00" +
            "000000800000005e00530a80010000140002000f000000000000000000d4ae4f" +
            "93"
    val a =
        input.split(" ").map { line ->
            line.chunked(2).map { it.toInt(16) }
        }
    for ((n, f) in a.withIndex()) {
        var o = 12
        var v = 0
        var k = u(f, o)
        if (k == 0x8100) {
            v = u(f, o + 2) and 0x0fff
            o += 4
            k = u(f, o)
        }
        val cat =
            if (k >= 1536) {
                "type"
            } else if (k <= 1500) {
                "length"
            } else {
                "reserved"
            }
        val type = k.toString(16).padStart(4, '0')
        val valid = if (fcs(f)) 1 else 0
        println(
            "${n + 1}: dst=${m(f.take(6))} src=${m(f.subList(6, 12))} " +
                "vlan=$v $cat=0x$type data=${f.size - 4 - o - 2} fcs=$valid",
        )
    }
    val d = a[0].toMutableList()
    d[25] = d[25] xor 4
    println("damaged: fcs=" + if (fcs(d)) 1 else 0)
}

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

String m(List<int> raw) =>
    raw.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(':');
int u(List<int> f, int o) => f[o] << 8 | f[o + 1];

int fcs(List<int> f) {
  var r = 0xffffffff;
  for (final byte in f.take(f.length - 4)) {
    r ^= byte;
    for (var bit = 0; bit < 8; bit++) {
      r = r & 1 != 0 ? (r >> 1) ^ 0xedb88320 : r >> 1;
    }
  }
  r ^= 0xffffffff;
  for (var i = 0; i < 4; i++) {
    if (f[f.length - 4 + i] != ((r >> (i * 8)) & 255)) return 0;
  }
  return 1;
}

void main() {
  final a = <List<int>>[];
  const input =
      'ffffffffffff00005e0053010806000108000604000100005e005301c0000201'
      '000000000000c00002020000000000000000000000000000000000008e30fb7f'
      ' 01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c4'
      '14e2031303000000000000000000000000000000000000000000000003293770'
      '2 0180c200000000005e00530a00264242030000000000800000005e00530a00'
      '000000800000005e00530a80010000140002000f000000000000000000d4ae4f'
      '93';
  for (final line in input.split(" ")) {
    final f = RegExp('..').allMatches(line).map((match) {
      return int.parse(match.group(0) ?? '00', radix: 16);
    }).toList();
    a.add(f);
  }
  for (var n = 0; n < a.length; n++) {
    final f = a[n];
    var o = 12, v = 0, k = u(f, 12);
    if (k == 0x8100) {
      v = u(f, o + 2) & 0x0fff;
      o += 4;
      k = u(f, o);
    }
    final cat = k >= 1536
        ? 'type'
        : k <= 1500
        ? 'length'
        : 'reserved';
    final type = k.toRadixString(16).padLeft(4, '0');
    print(
      '${n + 1}: dst=${m(f.take(6).toList())} '
      'src=${m(f.sublist(6, 12))} vlan=$v $cat=0x$type '
      'data=${f.length - 4 - o - 2} fcs=${fcs(f)}',
    );
  }
  final d = [...a[0]];
  d[25] ^= 4;
  print('damaged: fcs=${fcs(d)}');
}

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

1: dst=ff:ff:ff:ff:ff:ff src=00:00:5e:00:53:01 vlan=0 type=0x0806 data=46 fcs=1
2: dst=01:00:5e:90:10:01 src=3a:7f:12:c4:9e:01 vlan=100 type=0x88b5 data=42 fcs=1
3: dst=01:80:c2:00:00:00 src=00:00:5e:00:53:0a vlan=0 length=0x0026 data=46 fcs=1
damaged: fcs=0

Frame 1 is an ARP request sent to everyone; frame 2 is tagged for VLAN 100 and carries the experimental EtherType; frame 3 is a length frame whose 38 bytes of LLC data are followed by 8 bytes of padding. The FCS is the same CRC-32, with the same generator polynomial, that gzip uses, so Python’s zlib.crc32() can check it; its four bytes sit in the frame least significant byte first, the opposite of the order in which the number is usually written. Changing a single bit of frame 1 makes the check fail, and a real receiver would drop the frame.

CRC Calculator (CRC-8/16/32/64) Choose hex bytes, paste the first 60 bytes of frame 1 and pick CRC-32/ISO-HDLC: the result, 0x7FFB308E, is that frame's FCS, 8e 30 fb 7f, in the opposite byte order.

Follow the original VLAN 100 tag and distinguish42 data bytes from the reserved field interval.

Interactive · stage by stage The field threshold and VLAN position determine payload parsing Follow the original VLAN 100 tag and distinguish42 data bytes from the reserved field interval.
The field threshold and VLAN position determine payload parsing, step 3 of 3. Showing EtherType, Length interval, Original tag. In focus: Original tag.EtherType0x86DDLength intervalAt most 1,500Original tagVLAN 100;42 data bytes

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

Every step as text
  1. The original protocol table names IPv6 for 0x86DD, which is above the EtherType threshold 0x0600.
  2. A length field supplies an LLC data length under its declared frame format; the reserved 1,501-to1,535 interval is neither a valid length nor an EtherType.
  3. The original minimum-size tagged frame has four extra tag bytes, leaving 42 bytes after its header before the FCS.

Checkpoint

Check: The field threshold and VLAN position determine payload parsing

  1. Question 1 of 2 How should a parser handle a field between 1,501 and 1,535?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Reject or flag the reserved interval under its stated contract

    The original interval rule excludes those values. Below the type threshold does not by itself imply a valid length.

  2. Question 2 of 2 The original 64-byte tagged frame has 22 non-data bytes. How many data bytes remain?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 42 bytes

    Sixty-four minus twenty-two gives forty-two.

The first octet carries group and administration flags

A MAC address is 48 bits, usually written as six bytes in hexadecimal separated by colons or hyphens: 00:00:5e:00:53:01 or 00-00-5E-00-53-01. A manufacturer’s address starts with a prefix the IEEE Registration Authority assigned to it, most often a 24-bit OUI (organizationally unique identifier); longer prefixes of 28 and 36 bits exist for organisations that need fewer addresses. The allocation scheme is intended to give unique assigned addresses; software overrides, reused values or faults still need separate handling.

The MAC flag interpretation: Reading the individual/group and universal/local bits of the first address octet without assuming those bits alone establish device identity. uses two bits of the first byte:

The first octet 3a in binary, 00111010: bit 1, the U/L bit, is 1 (local) and bit 0, the I/G bit, is 0 (individual).The first octet of 3a:7f:12:c4:9e:01bit 7bit 000111010U/L bit: 0 universal, 1 localI/G bit: 0 individual, 1 groupBit 0 is the first bit of the address on the wire.

The two flag bits of a MAC address

Text description of the diagram

The first octet of the address 3a:7f:12:c4:9e:01 is 0x3a, which is 00111010 in binary, written from bit 7 on the left to bit 0 on the right.

  • Bit 0, the least significant bit, is the I/G bit: 0 for an individual (unicast) address, 1 for a group (multicast or broadcast) address. Here it is 0.
  • Bit 1 is the U/L bit: 0 for a universally administered address made from a vendor's OUI, 1 for a locally administered one. Here it is 1, so this address was not assigned by a manufacturer; it is the kind of address a phone picks at random.

Ethernet sends each octet least significant bit first, so the I/G bit is the very first bit of the destination address that a receiver or a switch sees.

  • Bit 0, the I/G bit (individual or group): 0 for an address that names one interface, 1 for a group address that many interfaces may listen to. The all-ones address ff:ff:ff:ff:ff:ff is the broadcast address, the group of every station on the network.
  • Bit 1, the U/L bit (universal or local): 0 for a universally administered address built from an IEEE prefix, 1 for a locally administered one, set by the network’s administrator or by the device’s own software.

Ethernet sends each byte least significant bit first, so the I/G bit is the very first bit of the destination address on the wire: a receiver knows at once whether a frame is for one interface or for a group. A quick way to read the flags is the second hexadecimal digit: if it is odd, the address is a group address; if it is 2, 6, A or E, it is a locally administered individual address.

The flag bits of five addresses, and multicast mappings Python · original-first-octet/first_octet.py
"""The two flag bits in the first octet of a MAC address, and multicast mappings.

Bit 0 (the least significant) is the I/G bit: 1 means a group address. Bit 1 is
the U/L bit: 1 means a locally administered address.
"""
import ipaddress

ADDRESSES = [
    "ff:ff:ff:ff:ff:ff",
    "01:00:5e:00:00:fb",
    "33:33:00:00:00:01",
    "00:00:5e:00:53:01",
    "3a:7f:12:c4:9e:01",
]

print("address            octet 1   I/G         U/L")
for text in ADDRESSES:
    first = int(text[:2], 16)
    ig = "group" if first & 1 else "individual"
    ul = "local" if first & 2 else "universal"
    print(f"{text}  {first:08b}  {ig:10}  {ul}")


def ipv4_group_mac(group):
    low_23_bits = int(ipaddress.IPv4Address(group)) & 0x7FFFFF
    return bytes([0x01, 0x00, 0x5E]) + low_23_bits.to_bytes(3)


def ipv6_group_mac(group):
    return bytes([0x33, 0x33]) + ipaddress.IPv6Address(group).packed[-4:]


print()
for group in ["224.0.0.251", "239.128.0.251"]:
    print(f"IPv4 group {group:14} -> {ipv4_group_mac(group).hex(':')}")
for group in ["ff02::1", "ff02::1:ff00:1"]:
    print(f"IPv6 group {group:14} -> {ipv6_group_mac(group).hex(':')}")

Output

address            octet 1   I/G         U/L
ff:ff:ff:ff:ff:ff  11111111  group       local
01:00:5e:00:00:fb  00000001  group       universal
33:33:00:00:00:01  00110011  group       local
00:00:5e:00:53:01  00000000  individual  universal
3a:7f:12:c4:9e:01  00111010  individual  local

IPv4 group 224.0.0.251    -> 01:00:5e:00:00:fb
IPv4 group 239.128.0.251  -> 01:00:5e:00:00:fb
IPv6 group ff02::1        -> 33:33:00:00:00:01
IPv6 group ff02::1:ff00:1 -> 33:33:ff:00:00:01

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

One program · 14 languages The first-octet flags and original multicast mappings O(n) for n displayed addresses

Python 3.14 · main.py

import ipaddress


def ipv4_group_mac(group: str) -> bytes:
    value = int(ipaddress.IPv4Address(group)) & 0x7FFFFF
    return b"\x01\x00\x5e" + value.to_bytes(3, "big")


def ipv6_group_mac(group: str) -> bytes:
    return b"\x33\x33" + ipaddress.IPv6Address(group).packed[-4:]


addresses = [
    "ff:ff:ff:ff:ff:ff",
    "01:00:5e:00:00:fb",
    "33:33:00:00:00:01",
    "00:00:5e:00:53:01",
    "3a:7f:12:c4:9e:01",
]
print("address            octet 1   I/G         U/L")
for text in addresses:
    first = int(text[:2], 16)
    kind = "group" if first & 1 else "individual"
    admin = "local" if first & 2 else "universal"
    print(f"{text}  {first:08b}  {kind:10}  {admin}")
print()
for group in ("224.0.0.251", "239.128.0.251"):
    print(f"IPv4 group {group:14} -> {ipv4_group_mac(group).hex(':')}")
for group in ("ff02::1", "ff02::1:ff00:1"):
    print(f"IPv6 group {group:14} -> {ipv6_group_mac(group).hex(':')}")

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 hex = (values) => values.map((value) => value.toString(16).padStart(2, "0")).join(":");
function ipv4GroupMac(group) {
  const parts = group.split(".").map(Number);
  const value = parts.reduce((total, part) => total * 256 + part, 0) & 0x7fffff;
  return [1, 0, 0x5e, (value >>> 16) & 255, (value >>> 8) & 255, value & 255];
}
function ipv6GroupMac(group) {
  const [left, right = ""] = group.split("::");
  const before = left.split(":").filter(Boolean);
  const after = right.split(":").filter(Boolean);
  const words = [...before, ...Array(8 - before.length - after.length).fill("0"), ...after];
  const low = words.slice(-2).map((word) => Number.parseInt(word, 16));
  return [0x33, 0x33, low[0] >>> 8, low[0] & 255, low[1] >>> 8, low[1] & 255];
}
const addresses = [
  "ff:ff:ff:ff:ff:ff",
  "01:00:5e:00:00:fb",
  "33:33:00:00:00:01",
  "00:00:5e:00:53:01",
  "3a:7f:12:c4:9e:01",
];
console.log("address            octet 1   I/G         U/L");
for (const text of addresses) {
  const first = Number.parseInt(text.slice(0, 2), 16);
  const kind = first & 1 ? "group" : "individual";
  const admin = first & 2 ? "local" : "universal";
  console.log(`${text}  ${first.toString(2).padStart(8, "0")}  ${kind.padEnd(10)}  ${admin}`);
}
console.log("");
for (const group of ["224.0.0.251", "239.128.0.251"]) {
  console.log(`IPv4 group ${group.padEnd(14)} -> ${hex(ipv4GroupMac(group))}`);
}
for (const group of ["ff02::1", "ff02::1:ff00:1"]) {
  console.log(`IPv6 group ${group.padEnd(14)} -> ${hex(ipv6GroupMac(group))}`);
}

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.ArrayList;
import java.util.Locale;

public class Main {
  static String hex(byte[] bytes) {
    var text = new StringBuilder();
    for (byte value : bytes) {
      if (text.length() != 0) {
        text.append(':');
      }
      text.append(String.format("%02x", value & 255));
    }
    return text.toString();
  }

  static byte[] ipv4GroupMac(String group) {
    long value = 0;
    for (String part : group.split("\\.")) {
      value = value * 256 + Integer.parseInt(part);
    }
    value &= 0x7fffff;
    return new byte[] {1, 0, 0x5e, (byte) (value >> 16), (byte) (value >> 8), (byte) value};
  }

  static byte[] ipv6GroupMac(String group) {
    String[] pieces = group.split("::", -1);
    var before = new ArrayList<String>();
    var after = new ArrayList<String>();
    for (String word : pieces[0].split(":")) {
      if (!word.isEmpty()) {
        before.add(word);
      }
    }
    for (String word : pieces[1].split(":")) {
      if (!word.isEmpty()) {
        after.add(word);
      }
    }
    int zeros = 8 - before.size() - after.size();
    for (int index = 0; index < zeros; index++) {
      before.add("0");
    }
    before.addAll(after);
    int high = Integer.parseInt(before.get(6), 16);
    int low = Integer.parseInt(before.get(7), 16);
    return new byte[] {0x33, 0x33, (byte) (high >> 8), (byte) high, (byte) (low >> 8), (byte) low};
  }

  public static void main(String[] args) {
    Locale.setDefault(Locale.ROOT);
    String[] addresses = {
      "ff:ff:ff:ff:ff:ff",
      "01:00:5e:00:00:fb",
      "33:33:00:00:00:01",
      "00:00:5e:00:53:01",
      "3a:7f:12:c4:9e:01"
    };
    System.out.println("address            octet 1   I/G         U/L");
    for (String text : addresses) {
      int first = Integer.parseInt(text.substring(0, 2), 16);
      String kind = (first & 1) != 0 ? "group" : "individual";
      String admin = (first & 2) != 0 ? "local" : "universal";
      String binary = "00000000" + Integer.toBinaryString(first);
      System.out.printf(
          "%s  %s  %-10s  %s%n", text, binary.substring(binary.length() - 8), kind, admin);
    }
    System.out.println();
    for (String group : new String[] {"224.0.0.251", "239.128.0.251"}) {
      System.out.printf("IPv4 group %-14s -> %s%n", group, hex(ipv4GroupMac(group)));
    }
    for (String group : new String[] {"ff02::1", "ff02::1:ff00:1"}) {
      System.out.printf("IPv6 group %-14s -> %s%n", group, hex(ipv6GroupMac(group)));
    }
  }
}

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 <array>
#include <charconv>
#include <format>
#include <print>
#include <span>
#include <string>
#include <string_view>
#include <vector>

std::string hex(std::span<const unsigned> values) {
  std::string text;
  for (const auto value : values) {
    text += (text.empty() ? "" : ":") + std::format("{:02x}", value);
  }
  return text;
}

std::vector<unsigned> split(std::string_view text, char delimiter, int base) {
  std::vector<unsigned> values;
  while (!text.empty()) {
    const auto end = text.find(delimiter);
    const auto part = text.substr(0, end);
    unsigned value = 0;
    std::from_chars(part.data(), part.data() + part.size(), value, base);
    values.push_back(value);
    if (end == std::string_view::npos) {
      break;
    }
    text.remove_prefix(end + 1);
  }
  return values;
}

std::array<unsigned, 6> ipv4_group_mac(std::string_view group) {
  unsigned value = 0;
  for (const auto part : split(group, '.', 10)) {
    value = value * 256 + part;
  }
  value &= 0x7fffff;
  return {1, 0, 0x5e, (value >> 16) & 255, (value >> 8) & 255, value & 255};
}

std::array<unsigned, 6> ipv6_group_mac(std::string_view group) {
  const auto separator = group.find("::");
  auto words = split(group.substr(0, separator), ':', 16);
  const auto tail = split(group.substr(separator + 2), ':', 16);
  words.resize(8 - tail.size(), 0);
  words.insert(words.end(), tail.begin(), tail.end());
  return {0x33,           0x33,          words[6] >> 8,
          words[6] & 255, words[7] >> 8, words[7] & 255};
}

int main() {
  const std::vector<std::string> addresses = {
      "ff:ff:ff:ff:ff:ff", "01:00:5e:00:00:fb", "33:33:00:00:00:01",
      "00:00:5e:00:53:01", "3a:7f:12:c4:9e:01"};
  std::println("address            octet 1   I/G         U/L");
  for (const auto &text : addresses) {
    const auto first = split(text.substr(0, 2), ':', 16)[0];
    const auto kind = first & 1 ? "group" : "individual";
    const auto admin = first & 2 ? "local" : "universal";
    std::println("{}  {:08b}  {:<10}  {}", text, first, kind, admin);
  }
  std::println("");
  for (const auto group : {"224.0.0.251", "239.128.0.251"}) {
    std::println("IPv4 group {:<14} -> {}", group, hex(ipv4_group_mac(group)));
  }
  for (const auto group : {"ff02::1", "ff02::1:ff00:1"}) {
    std::println("IPv6 group {:<14} -> {}", group, hex(ipv6_group_mac(group)));
  }
}

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;

string[] addresses =
[
    "ff:ff:ff:ff:ff:ff", "01:00:5e:00:00:fb", "33:33:00:00:00:01",
    "00:00:5e:00:53:01", "3a:7f:12:c4:9e:01"
];
Console.WriteLine("address            octet 1   I/G         U/L");
foreach (var text in addresses)
{
    int first = int.Parse(text[..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
    string kind = (first & 1) != 0 ? "group" : "individual";
    string admin = (first & 2) != 0 ? "local" : "universal";
    var binary = Convert.ToString(first, 2).PadLeft(8, '0');
    Console.WriteLine($"{text}  {binary}  {kind,-10}  {admin}");
}
Console.WriteLine();
foreach (var group in new[] { "224.0.0.251", "239.128.0.251" })
{
    Console.WriteLine($"IPv4 group {group,-14} -> {Hex(Ipv4GroupMac(group))}");
}
foreach (var group in new[] { "ff02::1", "ff02::1:ff00:1" })
{
    Console.WriteLine($"IPv6 group {group,-14} -> {Hex(Ipv6GroupMac(group))}");
}

static string Hex(byte[] bytes) => string.Join(":", bytes.Select(value => $"{value:x2}"));

static byte[] Ipv4GroupMac(string group)
{
    uint value = 0;
    foreach (var part in group.Split('.'))
    {
        value = value * 256 + uint.Parse(part, CultureInfo.InvariantCulture);
    }
    value &= 0x7fffff;
    return [1, 0, 0x5e, (byte)(value >> 16), (byte)(value >> 8), (byte)value];
}

static byte[] Ipv6GroupMac(string group)
{
    var pieces = group.Split("::", StringSplitOptions.None);
    var before = pieces[0].Split(':', StringSplitOptions.RemoveEmptyEntries).ToList();
    var after = pieces[1].Split(':', StringSplitOptions.RemoveEmptyEntries);
    int zeros = 8 - before.Count - after.Length;
    before.AddRange(Enumerable.Repeat("0", zeros));
    before.AddRange(after);
    int high = int.Parse(before[6], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
    int low = int.Parse(before[7], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
    return [0x33, 0x33, (byte)(high >> 8), (byte)high, (byte)(low >> 8), (byte)low];
}

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

C23 · main.c

#include <stdint.h>
#include <stdio.h>
#include <string.h>

static void hex_mac(const uint8_t bytes[6], char output[18]) {
  snprintf(output, 18, "%02x:%02x:%02x:%02x:%02x:%02x", bytes[0], bytes[1],
           bytes[2], bytes[3], bytes[4], bytes[5]);
}

static void ipv4_group_mac(const char *group, uint8_t bytes[6]) {
  unsigned first, second, third, fourth;
  sscanf(group, "%u.%u.%u.%u", &first, &second, &third, &fourth);
  const uint32_t value =
      ((first << 24) | (second << 16) | (third << 8) | fourth) & 0x7fffff;
  const uint8_t r[] = {1,
                       0,
                       0x5e,
                       (uint8_t)(value >> 16),
                       (uint8_t)(value >> 8),
                       (uint8_t)value};
  memcpy(bytes, r, 6);
}

static void ipv6_group_mac(const char *group, uint8_t bytes[6]) {
  const char *tail = strstr(group, "::") + 2;
  unsigned words[8] = {0};
  size_t used = 0;
  while (*tail != '\0') {
    unsigned word = 0;
    sscanf(tail, "%x", &word);
    words[used++] = word;
    const char *next = strchr(tail, ':');
    if (next == NULL) {
      break;
    }
    tail = next + 1;
  }
  const unsigned high = used >= 2 ? words[used - 2] : 0;
  const unsigned low = words[used - 1];
  const uint8_t r[] = {0x33,
                       0x33,
                       (uint8_t)(high >> 8),
                       (uint8_t)high,
                       (uint8_t)(low >> 8),
                       (uint8_t)low};
  memcpy(bytes, r, 6);
}

int main(void) {
  const char *addresses[] = {"ff:ff:ff:ff:ff:ff", "01:00:5e:00:00:fb",
                             "33:33:00:00:00:01", "00:00:5e:00:53:01",
                             "3a:7f:12:c4:9e:01"};
  puts("address            octet 1   I/G         U/L");
  for (size_t i = 0; i < 5; ++i) {
    unsigned first;
    sscanf(addresses[i], "%2x", &first);
    char binary[9];
    for (unsigned bit = 0; bit < 8; ++bit) {
      binary[bit] = (char)('0' + ((first >> (7 - bit)) & 1));
    }
    binary[8] = '\0';
    printf("%s  %s  %-10s  %s\n", addresses[i], binary,
           first & 1 ? "group" : "individual",
           first & 2 ? "local" : "universal");
  }
  puts("");
  const char *groups[] = {"224.0.0.251", "239.128.0.251", "ff02::1",
                          "ff02::1:ff00:1"};
  for (size_t i = 0; i < 4; ++i) {
    uint8_t bytes[6];
    if (i < 2) {
      ipv4_group_mac(groups[i], bytes);
    } else {
      ipv6_group_mac(groups[i], bytes);
    }
    char text[18];
    hex_mac(bytes, text);
    printf("IPv%u group %-14s -> %s\n", i < 2 ? 4 : 6, groups[i], text);
  }
}

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 list<int> $values */
function hexMac(array $values): string
{
    return implode(':', array_map(fn(int $value): string => sprintf('%02x', $value), $values));
}

/** @return list<int> */
function ipv4GroupMac(string $group): array
{
    $parts = array_map('intval', explode('.', $group));
    return [1, 0, 0x5e, $parts[1] & 0x7f, $parts[2], $parts[3]];
}

/** @return list<int> */
function ipv6GroupMac(string $group): array
{
    [$left, $right] = explode('::', $group);
    $before = explode(':', $left);
    $after = explode(':', $right);
    $words = [...$before, ...array_fill(0, 8 - count($before) - count($after), '0'), ...$after];
    $high = hexdec($words[6]);
    $low = hexdec($words[7]);
    return [0x33, 0x33, $high >> 8, $high & 255, $low >> 8, $low & 255];
}

$addresses = [
    'ff:ff:ff:ff:ff:ff', '01:00:5e:00:00:fb', '33:33:00:00:00:01',
    '00:00:5e:00:53:01', '3a:7f:12:c4:9e:01',
];
echo "address            octet 1   I/G         U/L\n";
foreach ($addresses as $text) {
    $first = hexdec(substr($text, 0, 2));
    $kind = $first & 1 ? 'group' : 'individual';
    $admin = $first & 2 ? 'local' : 'universal';
    $binary = str_pad(decbin($first), 8, '0', STR_PAD_LEFT);
    printf("%s  %s  %-10s  %s\n", $text, $binary, $kind, $admin);
}
echo "\n";
foreach (['224.0.0.251', '239.128.0.251'] as $group) {
    printf("IPv4 group %-14s -> %s\n", $group, hexMac(ipv4GroupMac($group)));
}
foreach (['ff02::1', 'ff02::1:ff00:1'] as $group) {
    printf("IPv6 group %-14s -> %s\n", $group, hexMac(ipv6GroupMac($group)));
}

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::net::{Ipv4Addr, Ipv6Addr};

fn hex(values: &[u8]) -> String {
    values
        .iter()
        .map(|value| format!("{value:02x}"))
        .collect::<Vec<_>>()
        .join(":")
}

fn ipv4_group_mac(group: Ipv4Addr) -> [u8; 6] {
    let value = u32::from(group) & 0x7fffff;
    [
        1,
        0,
        0x5e,
        (value >> 16) as u8,
        (value >> 8) as u8,
        value as u8,
    ]
}

fn ipv6_group_mac(group: Ipv6Addr) -> [u8; 6] {
    let bytes = group.octets();
    [0x33, 0x33, bytes[12], bytes[13], bytes[14], bytes[15]]
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addresses = [
        "ff:ff:ff:ff:ff:ff",
        "01:00:5e:00:00:fb",
        "33:33:00:00:00:01",
        "00:00:5e:00:53:01",
        "3a:7f:12:c4:9e:01",
    ];
    println!("address            octet 1   I/G         U/L");
    for text in addresses {
        let first = u8::from_str_radix(&text[..2], 16)?;
        let kind = if first & 1 != 0 {
            "group"
        } else {
            "individual"
        };
        let admin = if first & 2 != 0 { "local" } else { "universal" };
        println!("{text}  {first:08b}  {kind:<10}  {admin}");
    }
    println!();
    for group in ["224.0.0.251", "239.128.0.251"] {
        println!(
            "IPv4 group {group:<14} -> {}",
            hex(&ipv4_group_mac(group.parse()?))
        );
    }
    for group in ["ff02::1", "ff02::1:ff00:1"] {
        println!(
            "IPv6 group {group:<14} -> {}",
            hex(&ipv6_group_mac(group.parse()?))
        );
    }
    Ok(())
}

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 hex = (values: readonly number[]): string =>
  values.map((value) => value.toString(16).padStart(2, "0")).join(":");
function ipv4GroupMac(group: string): number[] {
  const parsed = group
    .split(".")
    .map(Number)
    .reduce((total, part) => total * 256 + part, 0);
  const value = parsed & 0x7fffff;
  return [1, 0, 0x5e, (value >>> 16) & 255, (value >>> 8) & 255, value & 255];
}
function ipv6GroupMac(group: string): number[] {
  const [left = "", right = ""] = group.split("::");
  const before = left.split(":").filter(Boolean);
  const after = right.split(":").filter(Boolean);
  const words = [...before, ...Array<string>(8 - before.length - after.length).fill("0"), ...after];
  const low = words.slice(-2).map((word) => Number.parseInt(word, 16));
  const highWord = low[0] ?? 0;
  const lowWord = low[1] ?? 0;
  return [0x33, 0x33, highWord >>> 8, highWord & 255, lowWord >>> 8, lowWord & 255];
}
const addresses = [
  "ff:ff:ff:ff:ff:ff",
  "01:00:5e:00:00:fb",
  "33:33:00:00:00:01",
  "00:00:5e:00:53:01",
  "3a:7f:12:c4:9e:01",
];
console.log("address            octet 1   I/G         U/L");
for (const text of addresses) {
  const first = Number.parseInt(text.slice(0, 2), 16);
  const kind = first & 1 ? "group" : "individual";
  const admin = first & 2 ? "local" : "universal";
  console.log(`${text}  ${first.toString(2).padStart(8, "0")}  ${kind.padEnd(10)}  ${admin}`);
}
console.log("");
for (const group of ["224.0.0.251", "239.128.0.251"]) {
  console.log(`IPv4 group ${group.padEnd(14)} -> ${hex(ipv4GroupMac(group))}`);
}
for (const group of ["ff02::1", "ff02::1:ff00:1"]) {
  console.log(`IPv6 group ${group.padEnd(14)} -> ${hex(ipv6GroupMac(group))}`);
}

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"
	"net/netip"
	"strconv"
	"strings"
)

func hex(values []byte) string {
	parts := []string{}
	for _, value := range values {
		parts = append(parts, fmt.Sprintf("%02x", value))
	}
	return strings.Join(parts, ":")
}

func ipv4GroupMac(group string) ([]byte, error) {
	address, err := netip.ParseAddr(group)
	if err != nil {
		return nil, err
	}
	parts := address.As4()
	return []byte{1, 0, 0x5e, parts[1] & 0x7f, parts[2], parts[3]}, nil
}

func ipv6GroupMac(group string) ([]byte, error) {
	address, err := netip.ParseAddr(group)
	if err != nil {
		return nil, err
	}
	parts := address.As16()
	return []byte{0x33, 0x33, parts[12], parts[13], parts[14], parts[15]}, nil
}

func main() {
	addresses := []string{"ff:ff:ff:ff:ff:ff", "01:00:5e:00:00:fb", "33:33:00:00:00:01",
		"00:00:5e:00:53:01", "3a:7f:12:c4:9e:01"}
	fmt.Println("address            octet 1   I/G         U/L")
	for _, text := range addresses {
		first, err := strconv.ParseUint(text[:2], 16, 8)
		if err != nil {
			return
		}
		kind, admin := "individual", "universal"
		if first&1 != 0 {
			kind = "group"
		}
		if first&2 != 0 {
			admin = "local"
		}
		fmt.Printf("%s  %08b  %-10s  %s\n", text, first, kind, admin)
	}
	fmt.Println()
	for _, group := range []string{"224.0.0.251", "239.128.0.251", "ff02::1", "ff02::1:ff00:1"} {
		version := 4
		var values []byte
		var err error
		if strings.Contains(group, ":") {
			version = 6
			values, err = ipv6GroupMac(group)
		} else {
			values, err = ipv4GroupMac(group)
		}
		if err != nil {
			return
		}
		fmt.Printf("IPv%d group %-14s -> %s\n", version, group, hex(values))
	}
}

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

Swift 6.4 · main.swift

func hex(_ values: [Int]) -> String {
  values.map {
    let text = String($0, radix: 16)
    return String(repeating: "0", count: 2 - text.count) + text
  }.joined(separator: ":")
}

func ipv4GroupMac(_ group: String) -> [Int] {
  let parts = group.split(separator: ".").compactMap { Int($0) }
  return [1, 0, 0x5e, parts[1] & 0x7f, parts[2], parts[3]]
}

func ipv6GroupMac(_ group: String) -> [Int] {
  let pieces = Array(group)
  var split = 0
  while !(pieces[split] == ":" && pieces[split + 1] == ":") {
    split += 1
  }
  let before = String(pieces[..<split]).split(separator: ":").compactMap { Int($0, radix: 16) }
  let after = String(pieces[(split + 2)...]).split(separator: ":").compactMap { Int($0, radix: 16) }
  let words = before + Array(repeating: 0, count: 8 - before.count - after.count) + after
  return [0x33, 0x33, words[6] >> 8, words[6] & 255, words[7] >> 8, words[7] & 255]
}

func pad(_ text: String, to width: Int) -> String {
  text + String(repeating: " ", count: max(0, width - text.count))
}

let addresses = [
  "ff:ff:ff:ff:ff:ff", "01:00:5e:00:00:fb", "33:33:00:00:00:01",
  "00:00:5e:00:53:01", "3a:7f:12:c4:9e:01",
]
print("address            octet 1   I/G         U/L")
for text in addresses {
  guard let first = Int(text.prefix(2), radix: 16) else { continue }
  let kind = first & 1 != 0 ? "group" : "individual"
  let admin = first & 2 != 0 ? "local" : "universal"
  let digits = String(first, radix: 2)
  let binary = String(repeating: "0", count: 8 - digits.count) + digits
  print("\(text)  \(binary)  \(pad(kind, to: 10))  \(admin)")
}
print()
for group in ["224.0.0.251", "239.128.0.251"] {
  print("IPv4 group \(pad(group, to: 14)) -> \(hex(ipv4GroupMac(group)))")
}
for group in ["ff02::1", "ff02::1:ff00:1"] {
  print("IPv6 group \(pad(group, to: 14)) -> \(hex(ipv6GroupMac(group)))")
}

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 hex(values)
  values.map { |value| format('%02x', value) }.join(':')
end

def v4(group)
  value = group.split('.').reduce(0) { |total, octet| total * 256 + octet.to_i } & 0x7fffff
  [1, 0, 0x5e, (value >> 16) & 255, (value >> 8) & 255, value & 255]
end

def v6(group)
  parts = group.split(':')
  high = parts[-2].to_i(16)
  low = parts[-1].to_i(16)
  [0x33, 0x33, high >> 8, high & 255, low >> 8, low & 255]
end

addresses = [
  'ff:ff:ff:ff:ff:ff', '01:00:5e:00:00:fb', '33:33:00:00:00:01',
  '00:00:5e:00:53:01', '3a:7f:12:c4:9e:01'
]
puts 'address            octet 1   I/G         U/L'
addresses.each do |text|
  first = text[0, 2].to_i(16)
  kind = first.odd? ? 'group' : 'individual'
  admin = first.anybits?(2) ? 'local' : 'universal'
  puts "#{text}  #{first.to_s(2).rjust(8, '0')}  #{kind.ljust(10)}  #{admin}"
end
puts
['224.0.0.251', '239.128.0.251'].each do |group|
  puts "IPv4 group #{group.ljust(14)} -> #{hex(v4(group))}"
end
['ff02::1', 'ff02::1:ff00:1'].each do |group|
  puts "IPv6 group #{group.ljust(14)} -> #{hex(v6(group))}"
end

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

Kotlin 2.4 · Main.kt

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

fun ipv4GroupMac(group: String): List<Int> {
    val parts = group.split(".").map { it.toInt() }
    return listOf(1, 0, 0x5e, parts[1] and 0x7f, parts[2], parts[3])
}

fun ipv6GroupMac(group: String): List<Int> {
    val (left, right) = group.split("::")
    val before = left.split(":").filter { it.isNotEmpty() }
    val after = right.split(":").filter { it.isNotEmpty() }
    val words = (before + List(8 - before.size - after.size) { "0" } + after).map { it.toInt(16) }
    return listOf(0x33, 0x33, words[6] ushr 8, words[6] and 255, words[7] ushr 8, words[7] and 255)
}

fun main() {
    val addresses =
        listOf(
            "ff:ff:ff:ff:ff:ff",
            "01:00:5e:00:00:fb",
            "33:33:00:00:00:01",
            "00:00:5e:00:53:01",
            "3a:7f:12:c4:9e:01",
        )
    println("address            octet 1   I/G         U/L")
    for (text in addresses) {
        val first = text.take(2).toInt(16)
        val kind = if (first and 1 != 0) "group" else "individual"
        val admin = if (first and 2 != 0) "local" else "universal"
        println("$text  ${first.toString(2).padStart(8, '0')}  ${kind.padEnd(10)}  $admin")
    }
    println()
    for (group in listOf("224.0.0.251", "239.128.0.251")) {
        println("IPv4 group ${group.padEnd(14)} -> ${hex(ipv4GroupMac(group))}")
    }
    for (group in listOf("ff02::1", "ff02::1:ff00:1")) {
        println("IPv6 group ${group.padEnd(14)} -> ${hex(ipv6GroupMac(group))}")
    }
}

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

String hex(List<int> values) =>
    values.map((value) => value.toRadixString(16).padLeft(2, '0')).join(':');

List<int> ipv4GroupMac(String group) {
  final parts = group.split('.').map(int.parse).toList();
  return [1, 0, 0x5e, parts[1] & 0x7f, parts[2], parts[3]];
}

List<int> ipv6GroupMac(String group) {
  final pieces = group.split('::');
  final before = pieces[0].split(':').where((part) => part.isNotEmpty).toList();
  final after = pieces[1].split(':').where((part) => part.isNotEmpty).toList();
  final words = [
    ...before,
    ...List.filled(8 - before.length - after.length, '0'),
    ...after,
  ].map((word) => int.parse(word, radix: 16)).toList();
  return [
    0x33,
    0x33,
    words[6] >> 8,
    words[6] & 255,
    words[7] >> 8,
    words[7] & 255,
  ];
}

void main() {
  final addresses = [
    'ff:ff:ff:ff:ff:ff',
    '01:00:5e:00:00:fb',
    '33:33:00:00:00:01',
    '00:00:5e:00:53:01',
    '3a:7f:12:c4:9e:01',
  ];
  print('address            octet 1   I/G         U/L');
  for (final text in addresses) {
    final first = int.parse(text.substring(0, 2), radix: 16);
    final kind = first & 1 != 0 ? 'group' : 'individual';
    final admin = first & 2 != 0 ? 'local' : 'universal';
    print(
      '${text}  ${first.toRadixString(2).padLeft(8, '0')}  ${kind.padRight(10)}  $admin',
    );
  }
  print('');
  for (final group in ['224.0.0.251', '239.128.0.251']) {
    print('IPv4 group ${group.padRight(14)} -> ${hex(ipv4GroupMac(group))}');
  }
  for (final group in ['ff02::1', 'ff02::1:ff00:1']) {
    print('IPv6 group ${group.padRight(14)} -> ${hex(ipv6GroupMac(group))}');
  }
}

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

address            octet 1   I/G         U/L
ff:ff:ff:ff:ff:ff  11111111  group       local
01:00:5e:00:00:fb  00000001  group       universal
33:33:00:00:00:01  00110011  group       local
00:00:5e:00:53:01  00000000  individual  universal
3a:7f:12:c4:9e:01  00111010  individual  local

IPv4 group 224.0.0.251    -> 01:00:5e:00:00:fb
IPv4 group 239.128.0.251  -> 01:00:5e:00:00:fb
IPv6 group ff02::1        -> 33:33:00:00:00:01
IPv6 group ff02::1:ff00:1 -> 33:33:ff:00:00:01

Group addresses are how IP multicast reaches Ethernet. An IPv4 multicast group is sent to 01:00:5e followed by the low 23 bits of the group address, so 224.0.0.251 and 239.128.0.251 share one MAC address and the receiver’s IP layer has to filter. An IPv6 multicast group is sent to 33:33 followed by the last 32 bits of the group address. Notice that 33 has both flag bits set: the IPv6 mapping uses the locally administered group range.

MAC Address Lookup Look up who an OUI belongs to, and check whether an address is a group address or a randomised local one. Number Base Converter Turn the first byte of an address into binary to read its I/G and U/L bits yourself.

On Windows, getmac /v lists the MAC address of each network adapter.

Read0x3A’s two flag bits and compare the original two IPv4 group mappings.

Interactive · stage by stage The first octet carries group and administration flags Read0x3A's two flag bits and compare the original two IPv4 group mappings.
The first octet carries group and administration flags, step 3 of 3. Showing Original 0x3A, Meaning, Multicast mapping. In focus: Multicast mapping.Original 0x3A00111010MeaningIndividual and localMulticast mappingTwoIPv4 groups;one MAC

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

Every step as text
  1. The original local address has bit 0 clear and bit 1 set in its first octet.
  2. Those two original bits describe an individual destination and local administration, rather than a manufacturer allocation claim.
  3. The original two IPv4 group examples share their low 23 bits and map to the same Ethernet address, so the higher protocol must distinguish them.

Checkpoint

Check: The first octet carries group and administration flags

  1. Question 1 of 2 Does a multicast Ethernet mapping uniquely identify everyIPv4 multicast group?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, multipleIP groups can share the mapped address

    The original mapping retains only the low 23 address bits. Discarded bits allow different groups to share the same mapped MAC.

  2. Question 2 of 2 The two IPv4 examples map to one Ethernet address. How many distinct mapped addresses appear?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1 address

    Both original groups have the same mapped address.

A private-address policy depends on the device and network

A universally administered address is assigned from an administered allocation, but the address presented by an interface can change and is sent in the clear on every Wi-Fi network a device joins and in the probe requests it sends while searching for networks, so anyone listening can recognise the device, and so the person carrying it, wherever it goes. Operating systems now replace it with a random, locally administered address: an individual address with the U/L bit set. Android uses persistent randomisation by default, deriving an address from the network profile and keeping it until a factory reset. Android 10 and 11 use persistent randomisation for all networks when enabled; Android 12 and later also permit non-persistent randomisation in specified cases. Recent Apple devices use a different private address on each network: by default it stays fixed on networks with WPA2 or stronger security and rotates every two weeks on networks with weak or no security. Windows can likewise use a random address per network and change it every day.

The privacy gain has costs for anything that identified devices by their address:

  • DHCP reservations and MAC allowlists stop matching when a device’s address is new, so the device gets a different IP address or is refused.
  • Parental controls and blocklists keyed to an address no longer recognise the device.
  • Address pools run out faster when devices come back with new addresses while the leases of their old ones are still held.
  • Enterprise Wi-Fi that ties the port to the device’s address must authenticate the device again after a change.
  • Support and presence detection, such as an office that opens a door when it sees a known phone, lose track of the device.

Networks that need to know who is connecting therefore move to identities that do not depend on the MAC address, and users can turn the private address off for a network they trust; on Apple devices the network’s details page in the Wi-Fi settings holds the setting.

Keep platform-specific private-address behaviour separate from the two original MAC flags.

Interactive · stage by stage A private-address policy depends on the device and network Keep platform-specific private-address behaviour separate from the two original MAC flags.
A private-address policy depends on the device and network, step 3 of 3. Showing Original risk, Private policy, Network identity. In focus: Network identity.Original riskStable link identifierPrivate policyPlatform and modeNetwork identityAddress-keyed rules

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

Every step as text
  1. A repeatedly observed stable interface identifier can support linking observations under the relevant radio and capture context.
  2. Address generation and rotation depend on the actual platform version, network settings and operating mode. Verify current official behaviour rather than treating one policy as universal.
  3. Reservations and allowlists keyed to the changed address need their own identity policy. A changed link identifier does not itself prove who is connecting.

Checkpoint

Check: A private-address policy depends on the device and network

  1. Question 1 of 2 What should support a claim about private-address rotation?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The relevant official platform/version/network policy

    The actual policy determines stability and rotation. A device category does not specify one shared operating-system behaviour.

  2. Question 2 of 2 The original flags use bit 0 for group and bit 1 for local. How many flag bits are inspected?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2 bits

    The stated two low bits are inspected.

The permitted packet size must fit every relevant path segment

State whether a size refers to the IP packet, Ethernet frame or link occupancy. Encapsulation can use part of a lower layer’s allowed payload, while tags add their own frame overhead. Check the relevant sender, receiver and intermediate limits before choosing a larger packet. A local maximum does not establish end-to-end handling, and a failed oversized transfer needs evidence about the actual path rather than an assumption based on one interface setting.

The 1,500-byte data limit is also the largest IP packet a standard Ethernet link carries, its MTU. Many devices also accept larger, jumbo frames of about 9,000 bytes, which carry more data per frame, but no IEEE standard defines them and the path MTU contract: The compatible packet-size and encapsulation limits required across the relevant path, rather than only one interface's supported maximum. requires every relevant device to accept them, or the large frames are lost. For example, a path that supports a 9,001-byte MTU can still contain a gateway or VPN segment limited to 1,500 bytes. The path must accommodate the packet end to end; a local jumbo setting alone does not establish the usable path MTU. Smaller MTUs are common too: PPPoE adds 8 bytes of headers inside the data, so the default interoperable PPPoE limit is 1,492 bytes. RFC 4638 describes larger negotiated values when the complete path can carry the additional Ethernet payload.

Subtract the original PPPoE overhead and retain the path support conditions for larger packets.

Interactive · stage by stage The permitted packet size must fit every relevant path segment Subtract the original PPPoE overhead and retain the path support conditions for larger packets.
The permitted packet size must fit every relevant path segment, step 3 of 3. Showing Original Ethernet, OriginalPPPoE, Larger frames. In focus: Larger frames.Original Ethernet1,500-byteIP MTUOriginalPPPoEEight-byte overheadLarger framesPath support required

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

Every step as text
  1. The original standard untagged payload supports a stated1,500-byteIP packet within its Ethernet frame.
  2. Under the original PPPoE convention, eight bytes inside that payload leave1,492 for theIP packet.
  3. A jumbo size requires compatible relevant devices and handling. A segment's supported maximum is not a guarantee for every destination or encapsulated path.

Checkpoint

Check: The permitted packet size must fit every relevant path segment

  1. Question 1 of 2 What makes a locally supported jumbo size usable end to end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Compatible limits and handling across the relevant path

    Every relevant path boundary must handle the intended packet and encapsulation. A first-hop setting does not define the remaining route's limit.

  2. Question 2 of 2 The original 1,500-byte payload reserves eight bytes for PPPoE. How many bytes remain for IP?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1492 bytes

    1,500 minus eight gives 1,492.

Practise the original access and frame contracts

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 11 A 100-byte frame uses the same twenty outside byte times. What is its occupancy?

    Type a number.

    Show the answer: Question 1 of 11

    Answer: 120 byte times

    One hundred plus twenty is one hundred twenty.

  2. Question 2 of 11 A 100-byte tagged frame has 22 non-data bytes under the same layout. How many remain?

    Type a number.

    Show the answer: Question 2 of 11

    Answer: 78 bytes

    One hundred minus twenty-two gives seventy-eight.

  3. Question 3 of 11 ThreeIP groups share one mapped MAC. How many distinct mapped MACs appear?

    Type a number.

    Show the answer: Question 3 of 11

    Answer: 1 address

    The stated three mappings all produce one address.

  4. Question 4 of 11 A check inspects three independent flag positions. How many bits does it inspect?

    Type a number.

    Show the answer: Question 4 of 11

    Answer: 3 bits

    The stated three positions give three bits.

  5. Question 5 of 11 A 1,200-byte payload reserves twelve encapsulation bytes. How many remain?

    Type a number.

    Show the answer: Question 5 of 11

    Answer: 1188 bytes

    1,200 minus twelve gives 1,188.

  6. Question 6 of 11 The two bytes after a frame's source address are 0x86DD. What follows them?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: An IPv6 packet, because values of 0x0600 or more are EtherTypes and 0x86DD is IPv6

    A type/length value of 0x0600 (1,536) or more is an EtherType, and 0x86DD is IPv6 (0x0800 is IPv4, 0x0806 ARP, 0x8100 a VLAN tag). Only values of 1,500 or less are lengths, which is why no length can reach 34,525.

  7. Question 7 of 11 Which of these are locally administered individual (unicast) addresses?

    Choose every answer that is right.

    Show the answer: Question 7 of 11

    Answer:

    • 3a:7f:12:c4:9e:01
    • 02:00:5e:10:00:01

    Read the second hex digit. 3a and 02 end in a, which is 1010, and 2, which is 0010: the U/L bit is 1 and the I/G bit is 0. 00 has both bits 0, a universal address; 33 and ff are odd, so they are group addresses (ff:ff:ff:ff:ff:ff is the broadcast address).

  8. Question 8 of 11 A minimum-size, 64-byte frame carries one 802.1Q VLAN tag. How many bytes of data can it hold?

    Type a number.

    Show the answer: Question 8 of 11

    Answer: 42 bytes

    The two addresses take 12 bytes, the tag 4, the EtherType 2 and the FCS 4: 22 bytes. 64 − 22 leaves 42 bytes of data, four fewer than the 46 of an untagged minimum frame.

  9. Question 9 of 11 An untagged frame carries 1,500 bytes of data. How many byte times does it occupy on the wire, counting the preamble, the start-of-frame delimiter and the interframe gap?

    Type a number.

    Show the answer: Question 9 of 11

    Answer: 1538 byte times

    The frame itself is 14 bytes of header, 1,500 of data and 4 of FCS, 1,518 bytes. The 8 bytes of preamble and SFD come before it and 12 byte times of gap after it: 1,518 + 8 + 12 = 1,538.

  10. Question 10 of 11 How can a switch tell that a frame is for a group of stations before it has read the rest of the destination address?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: Ethernet sends each byte least significant bit first, so the I/G bit is the first bit of the destination address

    The I/G bit is bit 0 of the first byte, and Ethernet transmits each byte starting with its least significant bit, so it is the very first address bit on the wire. Broadcast is only one of many group addresses: IPv4 and IPv6 multicast use 01:00:5e and 33:33 prefixes.

  11. Question 11 of 11 An address-keyed allowlist stops matching after devices present new private addresses. Which listed explanation fits that stated observation?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: The phones now use a random, locally administered address on each network instead of their built-in one

    A changed presented identifier need not match a list keyed to the previous one. The actual platform/version/network policy determines whether and when the private address changes.

Interview questions

What do the two lowest bits of the first byte of a MAC address mean? Bit 0 is the I/G bit: 0 means the address belongs to one interface (unicast), 1 means it is a group address (multicast, or broadcast when all 48 bits are 1). Bit 1 is the U/L bit: 0 means a universally administered address built from an IEEE-assigned prefix, 1 means a locally administered address set by software or an administrator, which is what phones use as random private addresses. In hexadecimal, an odd second digit means a group address, and 2, 6, A or E means a local unicast address. Ethernet sends the least significant bit of each byte first, so the group bit is the first bit of the destination that a switch sees.

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

  1. Fresher to mid

    A college's open guest Wi-Fi hands out addresses from a /22 DHCP pool with 24-hour leases. About 700 devices use it each day, yet by evening new devices get no address. Many devices present a new random MAC address when they rejoin the network. Explain the arithmetic and redesign the setup.

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

  2. Mid to senior

    Your team enables 9,000-byte jumbo frames on new servers and their switches. Large file copies to one storage system now hang, while small requests still work. What is happening, how do you prove it, and why can a 9,000-byte frame never be an IEEE 802.3 length frame?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An Ethernet II frame is destination (6 bytes), source (6), EtherType or length (2), data (46 to 1,500, padded) and FCS (4): 64 to 1,518 bytes, or 1,522 with one 802.1Q tag. Preamble, SFD and the 12-byte gap come on top.
  • A type/length value of 0x0600 or more is an EtherType (0x0800 IPv4, 0x0806 ARP, 0x86DD IPv6, 0x8100 VLAN tag); 1,500 or less is a length followed by an LLC header.
  • The FCS is a CRC-32 of the destination to the end of the data; a frame that fails it is dropped silently.
  • In a MAC address, bit 0 of the first byte marks a group address and bit 1 a locally administered one; ff:ff:ff:ff:ff:ff is the broadcast address.
  • Randomised, locally administered addresses protect privacy on Wi-Fi but break DHCP reservations, MAC allowlists and anything else keyed to a device’s address.
  • Standard Ethernet carries 1,500 bytes of data per frame; jumbo frames need every device on the path to support them.

Terms in this lesson

line occupancy
The transmission and spacing time attributed to a frame under a stated wire model, including preamble and gap costs outside the frame byte count.
MAC flag interpretation
Reading the individual/group and universal/local bits of the first address octet without assuming those bits alone establish device identity.
path MTU contract
The compatible packet-size and encapsulation limits required across the relevant path, rather than only one interface's supported maximum.
type/length interval
The value ranges that distinguish an Ethernet length field from an EtherType, including the reserved values between the two ranges.

Exercise

Exercise · Medium · Python

Parse an Ethernet frame and read a MAC address's flags

Write two functions in ethernet.py.

mac_flags(mac) takes a MAC address written with colons or hyphens, in either case (3a:7f:12:c4:9e:01, 00-00-5E-00-53-01), and returns a dictionary with three booleans: "group" (the I/G bit, the lowest bit of the first octet), "local" (the U/L bit, the next one up) and "broadcast" (all 48 bits are 1).

parse_ethernet(frame) takes the bytes of a frame from the destination address to the end of the data, without the preamble and without the frame check sequence, the way capture tools usually show frames. It returns a dictionary:

  • "destination" and "source": the addresses as lower-case text with colons, such as ff:ff:ff:ff:ff:ff
  • "vlan": the VLAN ID (the low 12 bits of the tag control field) when an 802.1Q tag (0x8100) follows the source address, otherwise None
  • "ethertype": the EtherType when the type/length field is 0x0600 or more, otherwise None
  • "length": the length when the field is 1,500 or less, otherwise None
  • "payload": the bytes after the type/length field; for a length frame, only the first length of them, without the padding

A type/length value from 1,501 to 1,535 is neither a length nor an EtherType: raise ValueError. The sample tests use an ARP broadcast, an IPv4 frame, a VLAN-tagged frame, an 802.3 length frame with padding and an invalid frame, and check mac_flags on four addresses.

Starter code · ethernet.py

def mac_flags(mac):
    """Return {"group": ..., "local": ..., "broadcast": ...} for a MAC address written in text."""
    # Replace this line with your code.
    return {"group": False, "local": False, "broadcast": False}


def parse_ethernet(frame):
    """Split frame bytes (no preamble, no FCS) into addresses, VLAN, EtherType or length, and payload."""
    # Replace this line with your code.
    return {}
The sample tests · test_ethernet.py
from ethernet import mac_flags, parse_ethernet

ARP = bytes.fromhex(
    "ffffffffffff00005e0053010806000108000604000100005e005301c0000201"
    "000000000000c0000202000000000000000000000000000000000000"
)
TAGGED = bytes.fromhex(
    "01005e9010013a7f12c49e018100006488b568656c6c6f2066726f6d20564c41"
    "4e203130300000000000000000000000000000000000000000000000"
)
LENGTH = bytes.fromhex(
    "0180c200000000005e00530a00264242030000000000800000005e00530a0000"
    "0000800000005e00530a80010000140002000f000000000000000000"
)


def test_arp_broadcast():
    """reads the addresses and the EtherType of an ARP broadcast"""
    f = parse_ethernet(ARP)
    assert f["destination"] == "ff:ff:ff:ff:ff:ff"
    assert f["source"] == "00:00:5e:00:53:01"
    assert f["ethertype"] == 0x0806
    assert f["vlan"] is None and f["length"] is None
    assert len(f["payload"]) == 46


def test_ipv4_frame():
    """keeps the whole payload of an EtherType frame"""
    frame = bytes.fromhex("00005e00530200005e0053010800") + bytes(range(50))
    f = parse_ethernet(frame)
    assert f["ethertype"] == 0x0800
    assert f["payload"] == bytes(range(50))


def test_vlan_tag():
    """finds the VLAN ID and the EtherType behind an 802.1Q tag"""
    f = parse_ethernet(TAGGED)
    assert f["vlan"] == 100
    assert f["ethertype"] == 0x88B5
    assert f["payload"].startswith(b"hello from VLAN 100") and len(f["payload"]) == 42


def test_length_frame():
    """reads an 802.3 length and drops the padding"""
    f = parse_ethernet(LENGTH)
    assert f["destination"] == "01:80:c2:00:00:00"
    assert f["length"] == 38 and f["ethertype"] is None
    assert len(f["payload"]) == 38 and f["payload"][:3] == bytes([0x42, 0x42, 0x03])


def test_neither_length_nor_type():
    """rejects a type/length value between 1,501 and 1,535"""
    frame = bytes.fromhex("00005e00530200005e00530105ee") + bytes(46)
    try:
        parse_ethernet(frame)
    except ValueError:
        return
    raise AssertionError("expected ValueError for 0x05ee")


def test_mac_flags():
    """reads the I/G and U/L bits and spots the broadcast address"""
    assert mac_flags("ff:ff:ff:ff:ff:ff") == {"group": True, "local": True, "broadcast": True}
    assert mac_flags("33:33:00:00:00:01") == {"group": True, "local": True, "broadcast": False}
    assert mac_flags("00-00-5E-00-53-01") == {"group": False, "local": False, "broadcast": False}
    assert mac_flags("3a:7f:12:c4:9e:01") == {"group": False, "local": True, "broadcast": False}
A hint

Slice the frame: frame[0:6] is the destination and frame[6:12] the source; frame[12:14] is the type/length field, which int.from_bytes(frame[12:14], "big") turns into a number. If it is 0x8100, the next two bytes are the tag control field and the real type/length field follows at frame[16:18]. For the text form of an address, raw.hex(":") does the work.

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.

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.