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.
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.
The fields of an Ethernet II frame
Text description of the diagram
The fields from top to bottom, in the order they are sent:
- 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.
- Destination address, 6 bytes.
- Source address, 6 bytes.
- An 802.1Q VLAN tag, 4 bytes, present only on tagged frames.
- EtherType or length, 2 bytes.
- Data, 46 to 1,500 bytes; shorter data is padded to 46 bytes.
- Frame check sequence, a 4-byte CRC-32 of the fields from the destination address to the end of the data.
- 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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- The original untagged frame counts destination, source, type/length, data and FCS from the destination address onward.
- Eight preamble/SFD bytes and twelve gap byte times add to its original line occupancy.
- 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
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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Python 3.14 · main.py
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 102 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 157 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 20 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 28 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 727 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 188 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 18 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
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.
Follow the original VLAN 100 tag and distinguish42 data bytes from the reserved field interval.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- The original protocol table names IPv6 for 0x86DD, which is above the EtherType threshold 0x0600.
- 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.
- 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
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 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:ffis 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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Python 3.14 · main.py
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. Runs on this device: a JavaScript build of this program (about 102 KB), made when the page was built. The first run also downloads the JavaScript runner (about 0.6 MB), which is kept for the next runs. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 155 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device: a .NET build of this program (about 3 KB), made when the page was built. The first run also downloads .NET (about 3.6 MB), which is kept for the next runs. Edit runs your changes in the browser: the first edited run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads .NET (about 8.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 19 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads the C and C++ compiler (about 29.1 MB), which is kept for the next runs. It is best on a computer. Runs on this device, in your browser. The first run downloads PHP (about 4.3 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 23 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads the JavaScript runner (about 0.9 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 753 KB), made when the page was built. Edit runs your changes in the browser: the first edited run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device, in your browser. The first run downloads the Go compiler (about 18.7 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 1.7 MB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device, in your browser. The first run downloads Ruby (about 10.2 MB), which is kept for the next runs. Runs on this device: a WebAssembly build of this program (about 188 KB), made when the page was built. To change the code, copy it and run it with the command below. Runs on this device: a WebAssembly build of this program (about 17 KB), made when the page was built. To change the code, copy it and run it with the command below.Your run, in this browser
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.
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- The original local address has bit 0 clear and bit 1 set in its first octet.
- Those two original bits describe an individual destination and local administration, rather than a manufacturer allocation claim.
- 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
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- A repeatedly observed stable interface identifier can support linking observations under the relevant radio and capture context.
- 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.
- 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
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.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- The original standard untagged payload supports a stated1,500-byteIP packet within its Ethernet frame.
- Under the original PPPoE convention, eight bytes inside that payload leave1,492 for theIP packet.
- 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
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.
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.
-
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.
-
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?
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:ffis 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 asff: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, otherwiseNone"ethertype": the EtherType when the type/length field is0x0600or more, otherwiseNone"length": the length when the field is 1,500 or less, otherwiseNone"payload": the bytes after the type/length field; for a length frame, only the firstlengthof 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
References
- The Ethernet: A Local Area Network. Data Link Layer and Physical Layer Specifications, Version 2.0 (Digital, Intel and Xerox) (Digital Equipment Corporation, Intel and Xerox (scan held by bitsavers.org))
- IEEE 802.3-2022: IEEE Standard for Ethernet (IEEE Standards Association)
- IEEE 802.1Q-2022: Bridges and Bridged Networks (IEEE Standards Association)
- RFC 9542: IANA Considerations and IETF Protocol and Documentation Usage for IEEE 802 Parameters (RFC Editor (IETF))
- IEEE 802 Numbers: EtherTypes (Internet Assigned Numbers Authority (IANA))
- Guidelines for Use of Extended Unique Identifier (EUI), Organizationally Unique Identifier (OUI), and Company ID (CID) (IEEE Registration Authority)
- RFC 894: A Standard for the Transmission of IP Datagrams over Ethernet Networks (RFC Editor (IETF))
- RFC 1112: Host Extensions for IP Multicasting (mapping to Ethernet multicast addresses) (RFC Editor (IETF))
- RFC 2464: Transmission of IPv6 Packets over Ethernet Networks (RFC Editor (IETF))
- RFC 2469: A Caution On The Canonical Ordering Of Link-Layer Addresses (RFC Editor (IETF))
- RFC 1952: GZIP file format specification (the CRC-32 it uses) (RFC Editor (IETF))
- zlib.crc32() (Python Software Foundation)
- RFC 9724: State of Affairs for Randomized and Changing Media Access Control (MAC) Addresses (RFC Editor (IETF))
- RFC 9797: Randomized and Changing MAC Addresses: Context, Network Impacts, and Use Cases (RFC Editor (IETF))
- Use private Wi-Fi addresses on Apple devices (Apple)
- MAC randomization behavior (Android Open Source Project)
- getmac (Windows commands) (Microsoft)
- RFC 4638: Accommodating a Maximum Transit Unit/Maximum Receive Unit (MTU/MRU) Greater Than 1492 in PPPoE (RFC Editor (IETF))
- Network maximum transmission unit (MTU) for your EC2 instance (Amazon Web Services)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress