Adds a draft of subnetting-ipv4 (scheduled 2026-06-05) with three interactive scenes: bit/mask visualization with prefix slider, greedy VLSM carving of a parent block into variable-size children, and the same-subnet-vs-gateway decision every host makes on every outbound packet. Also adds IDEAS entries for layer 2/3 routing and IPv4 subnetting. Enables Zola feed generation (Atom + RSS) and adds autodiscovery link tags in the base template. Sets site description and author so feed entries have a real byline instead of "Unknown".
653 lines
26 KiB
JavaScript
653 lines
26 KiB
JavaScript
// 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: VLSM 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 VLSM allocation: largest subnet first, each aligned to its own size boundary. Hover or read the labels to see 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 = 24;
|
||
const gapY = 6;
|
||
|
||
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 + 4;
|
||
// A title strip above the bits
|
||
text(ctx, `Host A: ${ipToStr(ipA)}/${prefix}`, margin.l + 4, rowsY - 2, C.cyan, 'left', 'bottom', 12, 'ui-monospace, SFMono-Regular, Menlo, monospace');
|
||
rowBits(rowsY + 6, 'A', bitsA, bitsB);
|
||
text(ctx, `Host B: ${ipToStr(ipB)}/${prefix}`, margin.l + 4, rowsY + 6 + rowH + gapY - 2, C.amber, 'left', 'bottom', 12, 'ui-monospace, SFMono-Regular, Menlo, monospace');
|
||
rowBits(rowsY + 6 + rowH + gapY + 14, 'B', bitsB, bitsA);
|
||
rowBits(rowsY + 6 + 2 * (rowH + gapY) + 14, '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, rowsY); ctx.lineTo(divX, rowsY + 3 * (rowH + gapY) + 20);
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
pillLabel(ctx, `/${prefix}`, divX, rowsY - 4, C.yellow, 'center', 'middle', 11);
|
||
|
||
// Decision diagram at the bottom
|
||
const diagY = rowsY + 3 * (rowH + gapY) + 40;
|
||
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();
|
||
}
|
||
|
||
// ---- init ----
|
||
function init() {
|
||
sceneBits();
|
||
sceneVLSM();
|
||
sceneSameSubnet();
|
||
}
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|