Draft subnetting post + Atom/RSS feed autodiscovery
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".
This commit is contained in:
parent
a96d43cfbe
commit
805560000f
5 changed files with 720 additions and 0 deletions
2
IDEAS.md
2
IDEAS.md
|
|
@ -37,6 +37,8 @@ Working scratch list. Same format as the NLOS post: vanilla JS, 2D canvas, scene
|
||||||
- **Garbage collection, three flavors.** Same heap, same allocation pattern, three collectors running in parallel panes: mark-sweep, copying, generational. Watch pause times and fragmentation diverge. Slider for allocation rate to find where each collector falls over.
|
- **Garbage collection, three flavors.** Same heap, same allocation pattern, three collectors running in parallel panes: mark-sweep, copying, generational. Watch pause times and fragmentation diverge. Slider for allocation rate to find where each collector falls over.
|
||||||
- **Branch prediction history.** A loop with a data-dependent branch. Toggle between always-taken, two-bit saturating counter, and a small perceptron predictor. Mispredict rate and pipeline bubbles rendered as wasted clock ticks.
|
- **Branch prediction history.** A loop with a data-dependent branch. Toggle between always-taken, two-bit saturating counter, and a small perceptron predictor. Mispredict rate and pipeline bubbles rendered as wasted clock ticks.
|
||||||
- **A load balancer at work.** Incoming requests, a pool of backends with varying response times. Switch between round robin, least connections, and consistent hashing. Kill a backend and watch the failure modes differ.
|
- **A load balancer at work.** Incoming requests, a pool of backends with varying response times. Switch between round robin, least connections, and consistent hashing. Kill a backend and watch the failure modes differ.
|
||||||
|
- **Subnetting IPv4.** An address and a prefix length. Slider from /8 to /30. Bits light up in the network and host portions. Host count, usable range, broadcast address, and subnet mask all recompute live. Second scene: take a /22 and carve it into a /24 + /25 + /26 + /27 + /28 to fit a specific set of site sizes. Overlap and waste rendered visually so VLSM clicks. Third scene: two hosts with given addresses and masks; show whether they're on the same subnet and, if not, what the gateway decision looks like.
|
||||||
|
- **Layer 2 and layer 3, side by side.** A small network: a few hosts, a switch, a router, two subnets. A packet leaves a host with a destination IP on the other subnet. Watch the ARP for the default gateway, the L2 frame addressed to the router's MAC, the router rewriting the L2 header and forwarding out a different interface, another ARP on the far side, final delivery. Toggle between a hub (collisions visible), a learning switch (MAC table filling in), and a router (routing table consulted per packet). Second scene introduces VLANs and trunking: the same switch logically split, a trunk port carrying tagged frames, the router-on-a-stick doing inter-VLAN routing. The phrase "broadcast domain" stops being abstract.
|
||||||
- **TCP in the weeds (follow-up to how-tcp-and-udp-work).** MSS negotiation, Path MTU Discovery, segmentation of a 10 KB write into 1460-byte segments, fast retransmit and SACK, delayed/accumulated ACKs, Nagle's algorithm, the PSH and URG flags, connection teardown with FIN/FIN-ACK and the TIME-WAIT state, keepalives and persist timers. One scene per mechanism with a small packet diagram each. Written for the reader who already understands the basics.
|
- **TCP in the weeds (follow-up to how-tcp-and-udp-work).** MSS negotiation, Path MTU Discovery, segmentation of a 10 KB write into 1460-byte segments, fast retransmit and SACK, delayed/accumulated ACKs, Nagle's algorithm, the PSH and URG flags, connection teardown with FIN/FIN-ACK and the TIME-WAIT state, keepalives and persist timers. One scene per mechanism with a small packet diagram each. Written for the reader who already understands the basics.
|
||||||
|
|
||||||
## Ham radio
|
## Ham radio
|
||||||
|
|
|
||||||
57
content/blog/subnetting-ipv4.md
Normal file
57
content/blog/subnetting-ipv4.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
+++
|
||||||
|
title = "Subnetting IPv4: the bit-bending behind the slash"
|
||||||
|
date = 2026-06-05
|
||||||
|
draft = true
|
||||||
|
+++
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/scene.css">
|
||||||
|
|
||||||
|
<div class="scene-host" data-scene-host>
|
||||||
|
|
||||||
|
An [IPv4 address](https://en.wikipedia.org/wiki/IPv4) is 32 bits. That is the whole of it. Everything else you ever hear about subnets, masks, supernets, VLSM, CIDR, and the `/24` that turns up in every man page on Earth is just an elaborate argument about where to draw a line in those 32 bits and what the two halves mean.
|
||||||
|
|
||||||
|
The address gets dressed up in dotted decimal for the benefit of humans who do not like reading binary, and it is easy to forget there are only ever bits underneath. The prefix length (the number after the slash) is the scoreboard: everything to the left is the network, everything to the right is a host within that network. Move the line, change the number of networks, change the number of hosts per network. The math is not complicated. It only feels that way because it is usually taught by somebody reading out a subnet mask table instead of letting you see the bits.
|
||||||
|
|
||||||
|
Below, the same territory in three scenes. Start with the address and the mask, then carve a block into a mix of smaller blocks (VLSM), then do the one routing decision every host makes on every outbound packet: am I talking to you directly, or do I need to hand this to my gateway?
|
||||||
|
|
||||||
|
## The address, the mask, and the slash
|
||||||
|
|
||||||
|
<div id="scene-bits"></div>
|
||||||
|
|
||||||
|
Drag the prefix slider. The bits on the left are the network portion and will match across every address in the subnet. The bits on the right are the host portion and are free to be anything. The subnet mask is just a second 32-bit number with ones in the network portion and zeros in the host portion. There is no third thing. If you understand the bits and the mask, you understand [CIDR](https://datatracker.ietf.org/doc/html/rfc4632).
|
||||||
|
|
||||||
|
Two host values are reserved in each subnet. The all-zeros host is the network address itself, used as a label for the subnet and legal nowhere else. The all-ones host is the directed broadcast address. Everything in between is usable for actual hosts, which is why a /24 holds 254 usable addresses even though it technically contains 256. The two you cannot use are not a waste so much as a tax, and the tax is fixed regardless of subnet size, which is why small subnets feel expensive. A /30 (two usable hosts out of four) is half overhead. A /31 is a special case that [RFC 3021](https://datatracker.ietf.org/doc/html/rfc3021) legalized for point-to-point links because losing half your addresses to ceremony on every link between two routers was getting embarrassing.
|
||||||
|
|
||||||
|
A /32 is a single address with no room for anyone else, which is useful for loopbacks and host routes. A /0 is the whole internet, which is useful as a default route and for almost nothing else.
|
||||||
|
|
||||||
|
## Carving a block with VLSM
|
||||||
|
|
||||||
|
Early IPv4 had exactly three network sizes: /8, /16, /24, called Class A, B, and C. If you needed 500 addresses you got a /16 with 65,534 of them and threw 65,034 in the bin. [Classless Inter-Domain Routing](https://datatracker.ietf.org/doc/html/rfc4632) arrived in 1993 to stop doing that. With CIDR and its close cousin [Variable-Length Subnet Masking](https://en.wikipedia.org/wiki/Subnetwork#Variable-length_subnet_masking), you can take any block and carve it into smaller blocks of whatever sizes you actually need, and you can do this recursively until the block has nothing left to give.
|
||||||
|
|
||||||
|
<div id="scene-vlsm"></div>
|
||||||
|
|
||||||
|
The parent block above is a /22: four /24s, 1022 usable hosts. The slider picks a VLSM strategy that allocates the largest subnet first, then the next, and so on until the parent is full or the remaining space is too small to honor the request. Each subnet gets drawn at its correct size, which makes it visible how fast the address space disappears when you start asking for /24s, and how much further you can stretch the block when the things on it actually fit in /27s.
|
||||||
|
|
||||||
|
VLSM is greedy: largest first, then walk down. The rule is that every subnet must be aligned to its own size. A /24 has to start on a multiple of 256. A /26 has to start on a multiple of 64. You cannot hand out a /26 that starts at offset 32 any more than you can hand out a 64-byte block of memory that starts at a 32-byte boundary. If this feels familiar it is because IP address allocation and memory allocation are more or less the same problem, and in both cases the scheme that works is the one where everything lines up on powers of two and you do not argue with it.
|
||||||
|
|
||||||
|
## Same subnet or gateway: the decision a host makes on every packet
|
||||||
|
|
||||||
|
Every host that wants to send a packet asks itself one question first: is the destination on my local subnet, or not? If it is, the packet goes directly (ARP for the destination's MAC, send). If it is not, the packet goes to the [default gateway](https://en.wikipedia.org/wiki/Default_gateway), which is some router on the local subnet that has agreed to take packets off the host's hands and figure out where they belong. The host never thinks about where the router will send the packet next. That is the router's problem, and in exchange for that favor the router gets to charge you an extra MAC-layer frame rewrite on every packet that crosses it.
|
||||||
|
|
||||||
|
<div id="scene-samesubnet"></div>
|
||||||
|
|
||||||
|
Two hosts, two addresses, one shared mask. The bits under the network portion either match or they don't. Slide the mask to change where the line falls and watch the decision flip. A /24 might put both hosts on the same subnet; a /26, on the same underlying ethernet, might not, because the line has moved and now the network bits disagree.
|
||||||
|
|
||||||
|
This is the reason a misconfigured subnet mask breaks connectivity in the exact pattern it does. If your mask is too long, you think your neighbors are on other networks and hand every packet to the gateway, which will either route them back out the same interface (wasteful but functional) or drop them on the floor (silent and infuriating). If your mask is too short, you think hosts on other networks are your neighbors, ARP for their MAC addresses, get no answer, and conclude that the network is down, when in fact the network is fine and you are the one who has misunderstood the map.
|
||||||
|
|
||||||
|
## The bit that makes the rest make sense
|
||||||
|
|
||||||
|
Subnetting is not a different skill from address arithmetic. It is address arithmetic, performed on 32-bit numbers, with the bits grouped into octets for dotted decimal only because humans are bad at binary. Every question about a subnet (what is the network address, what is the broadcast, what is the next subnet, can these two hosts talk directly) is answered by looking at the bits with the mask applied. Whole books have been written to avoid saying this. The books are longer than the idea.
|
||||||
|
|
||||||
|
Once the bits are visible, the rest is geometry. A /24 is not smaller than a /16 because somebody said so; it is smaller because 24 is more than 16, and those extra 8 bits of network portion leave 8 fewer bits of host portion, and 2^8 is 256 instead of 65536. The whole of IPv4 subnetting is in that sentence, with a rounding error for the two addresses you can't use.
|
||||||
|
|
||||||
|
IPv6 does the same thing with 128 bits instead of 32, which fixes the scarcity problem and changes essentially nothing about how the math works. Which is good, because having learned this once is plenty.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/subnetting.js" defer></script>
|
||||||
653
static/js/subnetting.js
Normal file
653
static/js/subnetting.js
Normal file
|
|
@ -0,0 +1,653 @@
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="stylesheet" href="{{ get_url(path="style.css") }}">
|
<link rel="stylesheet" href="{{ get_url(path="style.css") }}">
|
||||||
<link rel="stylesheet" href="{{ get_url(path="overrides.css") }}">
|
<link rel="stylesheet" href="{{ get_url(path="overrides.css") }}">
|
||||||
|
<link rel="alternate" type="application/atom+xml" title="{{ config.title }} - Atom" href="{{ get_url(path="atom.xml", trailing_slash=false) }}">
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="{{ config.title }} - RSS" href="{{ get_url(path="rss.xml", trailing_slash=false) }}">
|
||||||
<!-- Privacy-friendly analytics by Plausible -->
|
<!-- Privacy-friendly analytics by Plausible -->
|
||||||
<script async src="https://a.w5isp.com/js/pa-1S9-SH5BaakmzQOLB5zQw.js"></script>
|
<script async src="https://a.w5isp.com/js/pa-1S9-SH5BaakmzQOLB5zQw.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
base_url = "https://w5isp.com"
|
base_url = "https://w5isp.com"
|
||||||
|
|
||||||
title = "W5ISP"
|
title = "W5ISP"
|
||||||
|
description = "Amateur radio, RF, and software — notes from W5ISP."
|
||||||
|
author = "Graham McIntire"
|
||||||
|
|
||||||
# Theme
|
# Theme
|
||||||
theme = "zolanight"
|
theme = "zolanight"
|
||||||
|
|
@ -12,6 +14,10 @@ compile_sass = true
|
||||||
# Whether to build a search index to be used later on by a JavaScript library
|
# Whether to build a search index to be used later on by a JavaScript library
|
||||||
build_search_index = false
|
build_search_index = false
|
||||||
|
|
||||||
|
# Generate a feed for the site (Atom + RSS)
|
||||||
|
generate_feeds = true
|
||||||
|
feed_filenames = ["atom.xml", "rss.xml"]
|
||||||
|
|
||||||
[markdown]
|
[markdown]
|
||||||
|
|
||||||
[markdown.highlighting]
|
[markdown.highlighting]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue