// IPv4 subnetting explainer. Vanilla JS + 2D canvas. Scoped to one post. (() => { 'use strict'; const C = { bg: '#1a1d24', panel: '#242932', fg: '#d8dce4', dim: '#8189a0', grid: '#2d323d', gridStrong: '#3a414e', blue: '#78b5f3', cyan: '#6dc5d3', green: '#9dca83', amber: '#d6a86a', red: '#e58089', yellow: '#e2c37d', magenta: '#c78de0', }; // ---- framework ---- function setupCanvas(canvas) { const ctx = canvas.getContext('2d'); const dpr = Math.max(1, window.devicePixelRatio || 1); const fit = () => { const rect = canvas.getBoundingClientRect(); canvas.width = Math.round(rect.width * dpr); canvas.height = Math.round(rect.height * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; }; fit(); new ResizeObserver(fit).observe(canvas); return { ctx, getSize: () => ({ w: canvas.clientWidth, h: canvas.clientHeight }) }; } function scene(id, { height = 360, controls = [], readout = [], caption = '', threeCol = false } = {}) { const root = document.getElementById(id); if (!root) return null; root.classList.add('scene-box'); const canvas = document.createElement('canvas'); canvas.style.width = '100%'; canvas.style.height = `${height}px`; root.appendChild(canvas); const readoutEl = document.createElement('div'); readoutEl.className = 'scene-readout'; const readoutSpans = {}; readout.forEach(r => { const item = document.createElement('span'); item.className = 'scene-readout-item'; const label = document.createElement('span'); label.textContent = r.label + ':'; const val = document.createElement('span'); val.textContent = r.init ?? ''; item.appendChild(label); item.appendChild(val); readoutEl.appendChild(item); readoutSpans[r.key] = val; }); if (readout.length) root.appendChild(readoutEl); const controlsEl = document.createElement('div'); controlsEl.className = 'scene-controls' + (threeCol ? ' scene-controls--three' : ''); const values = {}; controls.forEach(c => { const wrap = document.createElement('div'); wrap.className = 'scene-control'; const label = document.createElement('label'); const name = document.createElement('span'); name.textContent = c.label; const valSpan = document.createElement('span'); valSpan.className = 'scene-value'; label.appendChild(name); label.appendChild(valSpan); const input = document.createElement('input'); input.type = 'range'; input.min = c.min; input.max = c.max; input.step = c.step ?? 'any'; input.value = c.value; values[c.key] = parseFloat(input.value); const fmt = c.format || (v => v.toFixed(2)); valSpan.textContent = fmt(values[c.key]); input.addEventListener('input', () => { values[c.key] = parseFloat(input.value); valSpan.textContent = fmt(values[c.key]); }); wrap.appendChild(label); wrap.appendChild(input); controlsEl.appendChild(wrap); }); if (controls.length) root.appendChild(controlsEl); if (caption) { const cap = document.createElement('div'); cap.className = 'scene-caption'; cap.textContent = caption; root.appendChild(cap); } const { ctx, getSize } = setupCanvas(canvas); const setReadout = (key, text) => { if (readoutSpans[key]) readoutSpans[key].textContent = text; }; return { canvas, ctx, getSize, values, setReadout }; } function clear(ctx, w, h) { ctx.clearRect(0, 0, w, h); } function text(ctx, str, x, y, color = C.fg, align = 'left', baseline = 'alphabetic', size = 12, family = 'system-ui, sans-serif') { ctx.save(); ctx.fillStyle = color; ctx.font = `${size}px ${family}`; ctx.textAlign = align; ctx.textBaseline = baseline; ctx.fillText(str, x, y); ctx.restore(); } function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); if (ctx.roundRect) ctx.roundRect(x, y, w, h, r); else ctx.rect(x, y, w, h); } function pillLabel(ctx, str, x, y, color = C.fg, align = 'left', baseline = 'middle', size = 12) { ctx.save(); ctx.font = `${size}px system-ui, sans-serif`; ctx.textAlign = align; ctx.textBaseline = baseline; const tw = ctx.measureText(str).width; const padX = 5, padY = 3; const h = size + 2 + padY * 2; let bx; if (align === 'right') bx = x - tw - padX; else if (align === 'center') bx = x - tw / 2 - padX; else bx = x - padX; let by; if (baseline === 'middle') by = y - h / 2; else if (baseline === 'top') by = y - padY; else by = y - size - padY; ctx.fillStyle = 'rgba(26, 29, 36, 0.82)'; roundRect(ctx, bx, by, tw + padX * 2, h, 3); ctx.fill(); ctx.fillStyle = color; ctx.fillText(str, x, y); ctx.restore(); } // ---- subnet math ---- // addresses stored as unsigned 32-bit ints (0..0xFFFFFFFF) function ipToStr(n) { return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.'); } function ipBits(n) { let s = ''; for (let i = 31; i >= 0; i--) s += ((n >>> i) & 1); return s; } function maskFor(prefix) { // >>> 0 keeps it unsigned; prefix 0 is the special case. if (prefix === 0) return 0; return (0xFFFFFFFF << (32 - prefix)) >>> 0; } function networkOf(ip, prefix) { return (ip & maskFor(prefix)) >>> 0; } function broadcastOf(ip, prefix) { return (networkOf(ip, prefix) | (~maskFor(prefix) >>> 0)) >>> 0; } // ============================================================ // Scene 1: Bits and mask // ============================================================ function sceneBits() { const s = scene('scene-bits', { height: 360, controls: [ { key: 'o1', label: 'Address: octet 1', min: 0, max: 255, value: 192, step: 1, format: v => v.toFixed(0) }, { key: 'o2', label: 'Address: octet 2', min: 0, max: 255, value: 168, step: 1, format: v => v.toFixed(0) }, { key: 'o3', label: 'Address: octet 3', min: 0, max: 255, value: 10, step: 1, format: v => v.toFixed(0) }, { key: 'o4', label: 'Address: octet 4', min: 0, max: 255, value: 37, step: 1, format: v => v.toFixed(0) }, { key: 'prefix', label: 'Prefix length', min: 0, max: 32, value: 24, step: 1, format: v => '/' + v.toFixed(0) }, ], readout: [ { key: 'cidr', label: 'CIDR' }, { key: 'mask', label: 'Mask' }, { key: 'net', label: 'Network' }, { key: 'bcast', label: 'Broadcast' }, { key: 'range', label: 'Usable' }, { key: 'hosts', label: 'Usable hosts' }, ], caption: 'Every IPv4 address is 32 bits. The prefix length picks where the network portion ends and the host portion begins. The subnet mask is just a second 32-bit number with ones on the network side and zeros on the host side.', threeCol: true, }); if (!s) return; function draw() { const { w, h } = s.getSize(); const ctx = s.ctx; clear(ctx, w, h); const prefix = Math.round(s.values.prefix); const ip = ((s.values.o1 << 24) | (s.values.o2 << 16) | (s.values.o3 << 8) | s.values.o4) >>> 0; const mask = maskFor(prefix); const net = networkOf(ip, prefix); const bcast = broadcastOf(ip, prefix); // Layout: one row for the address bits, one for the mask bits, one for the network address. const margin = { l: 20, r: 20, t: 20, b: 20 }; const colW = (w - margin.l - margin.r - 30) / 32; // 30px for the label column const rowH = 28; const gapY = 8; const labelW = 110; const firstBitX = margin.l + labelW; function drawRow(y, bitsStr, rowLabel, networkColor, hostColor, textColor) { text(ctx, rowLabel, margin.l + 4, y + rowH / 2, C.dim, 'left', 'middle', 12); for (let i = 0; i < 32; i++) { const bit = bitsStr[i]; const isNet = i < prefix; const x = firstBitX + i * colW; ctx.fillStyle = isNet ? networkColor : hostColor; roundRect(ctx, x + 1, y + 1, colW - 2, rowH - 2, 3); ctx.fill(); text(ctx, bit, x + colW / 2, y + rowH / 2 + 0.5, textColor, 'center', 'middle', Math.max(10, Math.min(14, colW - 4))); // octet separators if (i % 8 === 7 && i !== 31) { ctx.strokeStyle = C.grid; ctx.beginPath(); ctx.moveTo(x + colW, y - 2); ctx.lineTo(x + colW, y + rowH + 2); ctx.stroke(); } } } // Title strip with dotted-decimal + CIDR text(ctx, `${ipToStr(ip)}/${prefix}`, margin.l + 4, margin.t + 4, C.fg, 'left', 'top', 16, 'ui-monospace, SFMono-Regular, Menlo, monospace'); text(ctx, `mask ${ipToStr(mask)}`, w - margin.r - 4, margin.t + 4, C.dim, 'right', 'top', 13, 'ui-monospace, SFMono-Regular, Menlo, monospace'); // Prefix divider bar const rowsY = margin.t + 32; // Address row drawRow(rowsY, ipBits(ip), 'address', 'rgba(120,181,243,0.22)', 'rgba(157,202,131,0.18)', C.fg); // Mask row drawRow(rowsY + rowH + gapY, ipBits(mask), 'mask', 'rgba(120,181,243,0.22)', 'rgba(129,137,160,0.12)', C.fg); // Network row drawRow(rowsY + 2 * (rowH + gapY), ipBits(net), 'network', 'rgba(120,181,243,0.22)', 'rgba(129,137,160,0.08)', C.fg); // Broadcast row drawRow(rowsY + 3 * (rowH + gapY), ipBits(bcast), 'broadcast', 'rgba(120,181,243,0.22)', 'rgba(214,168,106,0.22)', C.fg); // Draw the prefix divider line const divX = firstBitX + prefix * colW; ctx.save(); ctx.strokeStyle = C.yellow; ctx.lineWidth = 2; ctx.setLineDash([5, 4]); ctx.beginPath(); ctx.moveTo(divX, rowsY - 6); ctx.lineTo(divX, rowsY + 4 * (rowH + gapY) - gapY + 4); ctx.stroke(); ctx.restore(); // Divider label pillLabel(ctx, `prefix /${prefix}`, divX, rowsY - 10, C.yellow, 'center', 'middle', 11); // Legend beneath bit rows const legY = rowsY + 4 * (rowH + gapY) + 6; const sq = (x, color, label) => { ctx.fillStyle = color; roundRect(ctx, x, legY, 12, 12, 2); ctx.fill(); text(ctx, label, x + 18, legY + 6, C.dim, 'left', 'middle', 11); }; sq(firstBitX, 'rgba(120,181,243,0.55)', 'network portion'); sq(firstBitX + 150, 'rgba(157,202,131,0.55)', 'host portion'); // Totals on the right below the legend const total = prefix >= 32 ? 1 : Math.pow(2, 32 - prefix); const usable = prefix >= 31 ? (prefix === 32 ? 1 : 2) : total - 2; const usableNote = prefix === 31 ? '(/31: both usable per RFC 3021)' : prefix === 32 ? '(/32: host route)' : ''; text(ctx, `2^${32 - prefix} = ${total.toLocaleString()} total / ${usable.toLocaleString()} usable ${usableNote}`, w - margin.r - 4, legY + 6, C.dim, 'right', 'middle', 12); // readouts s.setReadout('cidr', `${ipToStr(ip)}/${prefix}`); s.setReadout('mask', ipToStr(mask)); s.setReadout('net', ipToStr(net)); s.setReadout('bcast', ipToStr(bcast)); if (prefix >= 31) { s.setReadout('range', prefix === 32 ? `${ipToStr(net)} only` : `${ipToStr(net)} – ${ipToStr(bcast)}`); } else { s.setReadout('range', `${ipToStr((net + 1) >>> 0)} – ${ipToStr((bcast - 1) >>> 0)}`); } s.setReadout('hosts', usable.toLocaleString()); requestAnimationFrame(draw); } draw(); } // ============================================================ // Scene 2: CIDR carving of a /22 // ============================================================ function sceneVLSM() { // Predefined request lists. The scene greedy-allocates in order from the parent /22. const strategies = [ { label: 'four /24s', requests: [24, 24, 24, 24] }, { label: '/23 + two /24s', requests: [23, 24, 24] }, { label: '/24 + two /25s + /26 + /27', requests: [24, 25, 25, 26, 27] }, { label: 'five /26s + four /28s', requests: [26, 26, 26, 26, 26, 28, 28, 28, 28] }, { label: 'sixteen /26s', requests: Array.from({ length: 16 }, () => 26) }, { label: 'many /30 point-to-points', requests: Array.from({ length: 20 }, () => 30) }, ]; const s = scene('scene-vlsm', { height: 360, controls: [ { key: 'strat', label: 'Allocation strategy', min: 0, max: strategies.length - 1, value: 2, step: 1, format: v => strategies[Math.round(v)].label }, { key: 'basePrefix', label: 'Parent block prefix', min: 20, max: 26, value: 22, step: 1, format: v => '/' + v.toFixed(0) }, ], readout: [ { key: 'parent', label: 'Parent' }, { key: 'allocated', label: 'Allocated' }, { key: 'wasted', label: 'Unused' }, { key: 'fit', label: 'Requests honored' }, ], caption: 'Greedy allocation: largest subnet first, each aligned to its own size boundary. The labels show each subnet\'s prefix, network address, and usable host count. The slider on the right lets you change the parent block size so you can see when the same strategy runs out of room.', }); if (!s) return; function draw() { const { w, h } = s.getSize(); const ctx = s.ctx; clear(ctx, w, h); const strat = strategies[Math.round(s.values.strat)]; const basePrefix = Math.round(s.values.basePrefix); const parentBase = 0xC0A80000; // 192.168.0.0 const parentSize = Math.pow(2, 32 - basePrefix); const parentEnd = parentBase + parentSize; // Greedy allocation const allocated = []; let cursor = parentBase; let honored = 0; for (const reqPrefix of strat.requests) { if (reqPrefix < basePrefix) break; // bigger than parent const size = Math.pow(2, 32 - reqPrefix); // Align cursor up to multiple of `size` const aligned = Math.ceil(cursor / size) * size; if (aligned + size > parentEnd) break; allocated.push({ start: aligned, size, prefix: reqPrefix }); cursor = aligned + size; honored++; } const used = allocated.reduce((a, b) => a + b.size, 0); const wasted = parentSize - used; const margin = { l: 20, r: 20, t: 28, b: 20 }; const pW = w - margin.l - margin.r; const pY = margin.t + 44; const pH = 70; // Parent block frame ctx.save(); ctx.fillStyle = 'rgba(120,181,243,0.05)'; roundRect(ctx, margin.l, pY - 18, pW, pH + 34, 6); ctx.fill(); ctx.strokeStyle = C.grid; ctx.stroke(); ctx.restore(); // Top label text(ctx, `parent block: ${ipToStr(parentBase)}/${basePrefix} (${parentSize.toLocaleString()} addresses)`, margin.l + 4, margin.t, C.fg, 'left', 'top', 13, 'ui-monospace, SFMono-Regular, Menlo, monospace'); // Ruler ticks at boundaries (every /24 boundary if visible, else every /N-2) const rulerY = pY - 4; const tickBits = Math.min(basePrefix + 2, 30); const tickSize = Math.pow(2, 32 - tickBits); ctx.strokeStyle = C.grid; ctx.lineWidth = 1; for (let a = parentBase; a <= parentEnd; a += tickSize) { const x = margin.l + ((a - parentBase) / parentSize) * pW; ctx.beginPath(); ctx.moveTo(x, rulerY - 3); ctx.lineTo(x, rulerY); ctx.stroke(); } // Colors for subnets (cycle) const palette = [C.blue, C.cyan, C.green, C.amber, C.magenta, C.yellow]; // Draw allocated blocks allocated.forEach((a, i) => { const x = margin.l + ((a.start - parentBase) / parentSize) * pW; const bw = (a.size / parentSize) * pW; const col = palette[i % palette.length]; ctx.fillStyle = col + '66'; // translucent roundRect(ctx, x + 1, pY + 1, bw - 2, pH - 2, 4); ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1; roundRect(ctx, x + 1, pY + 1, bw - 2, pH - 2, 4); ctx.stroke(); // Label if wide enough const label = `/${a.prefix}`; const sub = ipToStr(a.start); const hosts = a.prefix >= 31 ? (a.prefix === 32 ? 1 : 2) : a.size - 2; ctx.save(); ctx.fillStyle = C.fg; ctx.font = '12px ui-monospace, SFMono-Regular, Menlo, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (bw > 70) { ctx.fillText(label, x + bw / 2, pY + 18); ctx.fillStyle = C.dim; ctx.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace'; ctx.fillText(sub, x + bw / 2, pY + 36); ctx.fillText(`${hosts.toLocaleString()} hosts`, x + bw / 2, pY + 52); } else if (bw > 30) { ctx.fillText(label, x + bw / 2, pY + pH / 2); } else { // too narrow to label } ctx.restore(); }); // Draw unused area (diagonal stripe) if (used < parentSize) { const startX = margin.l + (used / parentSize) * pW; const endX = margin.l + pW; ctx.save(); ctx.fillStyle = 'rgba(129,137,160,0.10)'; roundRect(ctx, startX + 1, pY + 1, endX - startX - 2, pH - 2, 4); ctx.fill(); // Diagonal hatch ctx.strokeStyle = 'rgba(129,137,160,0.25)'; ctx.lineWidth = 1; ctx.beginPath(); for (let xx = startX - pH; xx < endX; xx += 8) { ctx.moveTo(xx, pY + pH); ctx.lineTo(xx + pH, pY); } ctx.stroke(); if (endX - startX > 60) { text(ctx, 'unused', (startX + endX) / 2, pY + pH / 2, C.dim, 'center', 'middle', 12); } ctx.restore(); } // Request list (bottom) const listY = pY + pH + 28; text(ctx, 'Requests (in order):', margin.l + 4, listY, C.dim, 'left', 'middle', 12); let rx = margin.l + 140; strat.requests.forEach((reqP, i) => { const honored_i = i < honored; const col = honored_i ? C.fg : C.red; const label = '/' + reqP; ctx.save(); ctx.font = '12px ui-monospace, SFMono-Regular, Menlo, monospace'; const tw = ctx.measureText(label).width + 10; if (rx + tw > w - margin.r - 4) { rx = margin.l + 140; /* wrap line not needed often */ } ctx.fillStyle = honored_i ? 'rgba(157,202,131,0.18)' : 'rgba(229,128,137,0.18)'; roundRect(ctx, rx - 3, listY - 10, tw, 20, 4); ctx.fill(); ctx.fillStyle = col; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(label, rx, listY); ctx.restore(); rx += tw + 4; }); if (honored < strat.requests.length) { text(ctx, `(${strat.requests.length - honored} didn't fit)`, margin.l + 4, listY + 22, C.red, 'left', 'middle', 11); } // Readouts s.setReadout('parent', `${ipToStr(parentBase)}/${basePrefix}`); s.setReadout('allocated', `${used.toLocaleString()} addrs`); s.setReadout('wasted', `${wasted.toLocaleString()} addrs`); s.setReadout('fit', `${honored} / ${strat.requests.length}`); requestAnimationFrame(draw); } draw(); } // ============================================================ // Scene 3: Same-subnet decision // ============================================================ function sceneSameSubnet() { const s = scene('scene-samesubnet', { height: 380, controls: [ { key: 'aOct3', label: 'Host A: octet 3', min: 0, max: 255, value: 5, step: 1, format: v => v.toFixed(0) }, { key: 'aOct4', label: 'Host A: octet 4', min: 1, max: 254, value: 17, step: 1, format: v => v.toFixed(0) }, { key: 'bOct3', label: 'Host B: octet 3', min: 0, max: 255, value: 5, step: 1, format: v => v.toFixed(0) }, { key: 'bOct4', label: 'Host B: octet 4', min: 1, max: 254, value: 200, step: 1, format: v => v.toFixed(0) }, { key: 'prefix', label: 'Shared prefix', min: 16, max: 30, value: 24, step: 1, format: v => '/' + v.toFixed(0) }, ], readout: [ { key: 'a', label: 'A' }, { key: 'b', label: 'B' }, { key: 'netA', label: 'A network' }, { key: 'netB', label: 'B network' }, { key: 'decision', label: 'Delivery' }, ], caption: "The two hosts share the base 10.0.x.x/16 space. The shared prefix determines where the network/host line falls. If both addresses' network bits match, the hosts deliver directly (one ARP, one frame). If they don't, every packet crosses the default gateway first.", threeCol: true, }); if (!s) return; function draw() { const { w, h } = s.getSize(); const ctx = s.ctx; clear(ctx, w, h); const base = 0x0A000000; // 10.0.0.0 const prefix = Math.round(s.values.prefix); const ipA = (base | (s.values.aOct3 << 8) | s.values.aOct4) >>> 0; const ipB = (base | (s.values.bOct3 << 8) | s.values.bOct4) >>> 0; const mask = maskFor(prefix); const netA = (ipA & mask) >>> 0; const netB = (ipB & mask) >>> 0; const same = netA === netB; // Layout: bits at top (3 rows: A, B, mask), decision diagram at bottom const margin = { l: 20, r: 20, t: 14, b: 14 }; const colW = (w - margin.l - margin.r - 100) / 32; const firstBitX = margin.l + 100; const rowH = 22; const gapY = 22; // room for the title strip above A and B rows const bitsA = ipBits(ipA); const bitsB = ipBits(ipB); const bitsMask = ipBits(mask); function rowBits(y, label, bits, compareBits) { text(ctx, label, margin.l + 4, y + rowH / 2, C.dim, 'left', 'middle', 12); for (let i = 0; i < 32; i++) { const x = firstBitX + i * colW; const isNet = i < prefix; const b = bits[i]; let bg; if (!isNet) { bg = 'rgba(129,137,160,0.10)'; } else if (compareBits && compareBits[i] === b) { bg = 'rgba(157,202,131,0.28)'; } else if (compareBits) { bg = 'rgba(229,128,137,0.28)'; } else { bg = 'rgba(120,181,243,0.22)'; } ctx.fillStyle = bg; roundRect(ctx, x + 1, y + 1, colW - 2, rowH - 2, 3); ctx.fill(); text(ctx, b, x + colW / 2, y + rowH / 2 + 0.5, C.fg, 'center', 'middle', Math.max(10, Math.min(13, colW - 4))); if (i % 8 === 7 && i !== 31) { ctx.strokeStyle = C.grid; ctx.beginPath(); ctx.moveTo(x + colW, y - 2); ctx.lineTo(x + colW, y + rowH + 2); ctx.stroke(); } } } const rowsY = margin.t + 12; const rowAY = rowsY; const rowBY = rowAY + rowH + gapY; const rowMY = rowBY + rowH + gapY; // Title strips above A and B rows (no title for mask; its own label in the row) text(ctx, `Host A: ${ipToStr(ipA)}/${prefix}`, margin.l + 4, rowAY - 3, C.cyan, 'left', 'bottom', 11, 'ui-monospace, SFMono-Regular, Menlo, monospace'); text(ctx, `Host B: ${ipToStr(ipB)}/${prefix}`, margin.l + 4, rowBY - 3, C.amber, 'left', 'bottom', 11, 'ui-monospace, SFMono-Regular, Menlo, monospace'); text(ctx, `mask /${prefix}`, margin.l + 4, rowMY - 3, C.dim, 'left', 'bottom', 11, 'ui-monospace, SFMono-Regular, Menlo, monospace'); rowBits(rowAY, 'A', bitsA, bitsB); rowBits(rowBY, 'B', bitsB, bitsA); rowBits(rowMY, 'mask', bitsMask, null); // Prefix divider line const divX = firstBitX + prefix * colW; ctx.save(); ctx.strokeStyle = C.yellow; ctx.lineWidth = 2; ctx.setLineDash([5, 4]); ctx.beginPath(); ctx.moveTo(divX, rowAY - 14); ctx.lineTo(divX, rowMY + rowH + 2); ctx.stroke(); ctx.restore(); pillLabel(ctx, `/${prefix}`, divX, rowAY - 20, C.yellow, 'center', 'middle', 11); // Decision diagram at the bottom const diagY = rowMY + rowH + 24; const diagH = h - diagY - margin.b; if (diagH > 60) { const cx = w / 2; const yMid = diagY + diagH / 2; // Draw two hosts and a switch/gateway function box(x, y, bw, bh, col, label, sub) { ctx.save(); ctx.fillStyle = col + '22'; roundRect(ctx, x - bw / 2, y - bh / 2, bw, bh, 5); ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke(); ctx.fillStyle = col; ctx.font = '13px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, x, y - 8); ctx.fillStyle = C.dim; ctx.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace'; ctx.fillText(sub, x, y + 10); ctx.restore(); } const hostW = 140, hostH = 56; const aX = margin.l + hostW / 2 + 10, bX = w - margin.r - hostW / 2 - 10; box(aX, yMid, hostW, hostH, C.cyan, 'Host A', ipToStr(ipA)); box(bX, yMid, hostW, hostH, C.amber, 'Host B', ipToStr(ipB)); if (same) { // direct line ctx.strokeStyle = C.green; ctx.lineWidth = 2.5; ctx.setLineDash([]); ctx.beginPath(); ctx.moveTo(aX + hostW / 2, yMid); ctx.lineTo(bX - hostW / 2, yMid); ctx.stroke(); pillLabel(ctx, 'same subnet -> direct (ARP, send)', cx, yMid - 14, C.green, 'center', 'middle', 12); pillLabel(ctx, `both in ${ipToStr(netA)}/${prefix}`, cx, yMid + 16, C.dim, 'center', 'middle', 11); } else { // via gateway const gwW = 130, gwH = 56; box(cx, yMid, gwW, gwH, C.red, 'Gateway', 'default route'); ctx.strokeStyle = C.red; ctx.lineWidth = 2.5; ctx.setLineDash([6, 4]); ctx.beginPath(); ctx.moveTo(aX + hostW / 2, yMid); ctx.lineTo(cx - gwW / 2, yMid); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cx + gwW / 2, yMid); ctx.lineTo(bX - hostW / 2, yMid); ctx.stroke(); ctx.setLineDash([]); pillLabel(ctx, 'different subnets -> via gateway', cx, yMid - 42, C.red, 'center', 'middle', 12); pillLabel(ctx, `A net ${ipToStr(netA)}/${prefix} | B net ${ipToStr(netB)}/${prefix}`, cx, yMid + 42, C.dim, 'center', 'middle', 11); } } s.setReadout('a', `${ipToStr(ipA)}/${prefix}`); s.setReadout('b', `${ipToStr(ipB)}/${prefix}`); s.setReadout('netA', ipToStr(netA)); s.setReadout('netB', ipToStr(netB)); s.setReadout('decision', same ? 'DIRECT (same subnet)' : 'GATEWAY (different subnets)'); requestAnimationFrame(draw); } draw(); } // ============================================================ // Scene 4: Supernetting / route summarization // ============================================================ function sceneSupernet() { const s = scene('scene-supernet', { height: 340, controls: [ { key: 'aIdx', label: 'Subnet A (10.20.x.0/24)', min: 0, max: 255, value: 4, step: 1, format: v => '10.20.' + v.toFixed(0) + '.0/24' }, { key: 'bIdx', label: 'Subnet B (10.20.x.0/24)', min: 0, max: 255, value: 5, step: 1, format: v => '10.20.' + v.toFixed(0) + '.0/24' }, ], readout: [ { key: 'a', label: 'A' }, { key: 'b', label: 'B' }, { key: 'super', label: 'Smallest supernet' }, { key: 'covered', label: 'Covered' }, { key: 'wasted', label: 'Collateral' }, ], caption: "Supernetting is subnetting run in reverse: take several contiguous subnets and find the smallest single prefix that covers them all. The only time this works cleanly is when the subnets are adjacent AND the group starts on a boundary aligned to its own combined size. When they aren't, the supernet balloons up to include every address in between, which is why you can't just 'summarize' any two subnets you happen to own.", }); if (!s) return; // Find the shortest prefix whose block contains both addresses. function commonPrefixLen(a, b) { let x = (a ^ b) >>> 0; if (x === 0) return 32; let p = 0; while (x !== 0) { x = x >>> 1; p++; } return 32 - p; } function draw() { const { w, h } = s.getSize(); const ctx = s.ctx; clear(ctx, w, h); const aIdx = Math.round(s.values.aIdx); const bIdx = Math.round(s.values.bIdx); const parent = 0x0A140000; // 10.20.0.0/16 const ipA = (parent | (aIdx << 8)) >>> 0; const ipB = (parent | (bIdx << 8)) >>> 0; const subnetSize = 256; // Smallest supernet containing both. A /24 is 256 addrs so the lowest // prefix worth considering is /24 (if A === B) or shorter. const lo = Math.min(ipA, ipB); const hi = (Math.max(ipA, ipB) + subnetSize - 1) >>> 0; // Need a prefix P such that (lo & mask) == (hi & mask). let superPrefix = commonPrefixLen(lo, hi); if (superPrefix > 24) superPrefix = 24; const superMask = maskFor(superPrefix); const superStart = (lo & superMask) >>> 0; const superSize = Math.pow(2, 32 - superPrefix); // Layout: a number line spanning 10.20.0.0/16 with 256 tick positions const margin = { l: 20, r: 20, t: 36, b: 14 }; const pW = w - margin.l - margin.r; const trackY = margin.t + 50; const trackH = 56; const slotW = pW / 256; // Parent frame ctx.save(); ctx.fillStyle = 'rgba(120,181,243,0.04)'; roundRect(ctx, margin.l, trackY, pW, trackH, 5); ctx.fill(); ctx.strokeStyle = C.grid; ctx.stroke(); ctx.restore(); text(ctx, `parent space: 10.20.0.0/16 (each tick = one /24, 256 slots)`, margin.l + 4, margin.t - 4, C.dim, 'left', 'bottom', 12, 'ui-monospace, SFMono-Regular, Menlo, monospace'); // Supernet overlay const superStartIdx = (superStart - parent) >>> 8; const superSlots = superSize / 256; if (superPrefix < 16) { // wider than parent - rare edge case, clamp to parent ctx.fillStyle = 'rgba(229,128,137,0.15)'; roundRect(ctx, margin.l, trackY - 10, pW, trackH + 20, 5); ctx.fill(); } else { const sx = margin.l + superStartIdx * slotW; const sw = superSlots * slotW; ctx.fillStyle = 'rgba(229,128,137,0.14)'; roundRect(ctx, sx, trackY - 10, sw, trackH + 20, 5); ctx.fill(); ctx.strokeStyle = C.red; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); roundRect(ctx, sx, trackY - 10, sw, trackH + 20, 5); ctx.stroke(); ctx.setLineDash([]); // Supernet label - clamp horizontally so it stays within the canvas const labelText = `supernet: ${ipToStr(superStart)}/${superPrefix} (${superSize.toLocaleString()} addresses)`; ctx.save(); ctx.font = '11px system-ui, sans-serif'; const labelW = ctx.measureText(labelText).width + 14; ctx.restore(); let labelX = sx + sw / 2; const minX = margin.l + labelW / 2 + 2; const maxX = w - margin.r - labelW / 2 - 2; if (labelX < minX) labelX = minX; if (labelX > maxX) labelX = maxX; pillLabel(ctx, labelText, labelX, trackY - 22, C.red, 'center', 'middle', 11); } // Tick marks every 16 slots ctx.strokeStyle = C.grid; ctx.lineWidth = 1; for (let i = 0; i <= 256; i += 16) { const x = margin.l + i * slotW; ctx.beginPath(); ctx.moveTo(x, trackY + trackH); ctx.lineTo(x, trackY + trackH + 4); ctx.stroke(); if (i % 64 === 0 && i < 256) { text(ctx, `10.20.${i}.0`, x, trackY + trackH + 6, C.dim, i === 0 ? 'left' : 'center', 'top', 10, 'ui-monospace, SFMono-Regular, Menlo, monospace'); } if (i === 256) { text(ctx, `10.21.0.0`, x, trackY + trackH + 6, C.dim, 'right', 'top', 10, 'ui-monospace, SFMono-Regular, Menlo, monospace'); } } function clampLabelX(x, estWidth) { const minX = margin.l + estWidth / 2 + 2; const maxX = w - margin.r - estWidth / 2 - 2; return Math.max(minX, Math.min(maxX, x)); } // Draw subnet A const axStart = margin.l + aIdx * slotW; ctx.fillStyle = C.cyan + 'BB'; roundRect(ctx, axStart + 0.5, trackY + 4, Math.max(2, slotW - 1), trackH - 8, 2); ctx.fill(); const aLabelW = 140; pillLabel(ctx, `A: 10.20.${aIdx}.0/24`, clampLabelX(axStart + slotW / 2, aLabelW), trackY + trackH + 22, C.cyan, 'center', 'middle', 11); // Draw subnet B const bxStart = margin.l + bIdx * slotW; ctx.fillStyle = C.amber + 'BB'; roundRect(ctx, bxStart + 0.5, trackY + 4, Math.max(2, slotW - 1), trackH - 8, 2); ctx.fill(); // Offset B's label vertically if close to A horizontally (so pills don't overlap) const labelYB = Math.abs(aIdx - bIdx) < 30 ? trackY + trackH + 40 : trackY + trackH + 22; pillLabel(ctx, `B: 10.20.${bIdx}.0/24`, clampLabelX(bxStart + slotW / 2, aLabelW), labelYB, C.amber, 'center', 'middle', 11); // Covered / collateral numbers const covered = 2 * subnetSize; const collateral = superSize - covered; s.setReadout('a', `10.20.${aIdx}.0/24`); s.setReadout('b', `10.20.${bIdx}.0/24`); s.setReadout('super', `${ipToStr(superStart)}/${superPrefix}`); s.setReadout('covered', `${covered.toLocaleString()} addrs`); s.setReadout('wasted', `${collateral.toLocaleString()} addrs swept in`); requestAnimationFrame(draw); } draw(); } // ---- init ---- function init() { sceneBits(); sceneVLSM(); sceneSameSubnet(); sceneSupernet(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();