Expand subnetting post: supernet scene + reserved ranges + layout fixes
Adds a fourth scene for route summarization: two /24s in a /16 parent, slider to position each, draws the smallest supernet that covers both. Demonstrates cleanly why only consecutive and power-of-two aligned subnets aggregate without collateral. Adds a prose section on reserved IPv4 ranges (RFC 1918 private, loopback, link-local, multicast, limited broadcast, 0.0.0.0, RFC 6598 CGN space). Fixes scene-samesubnet layout where the host title strips were overlapping the bit rows; increases row gap so titles sit cleanly above their rows. Clamps pill labels in scene-supernet so they stay on canvas when subnets sit at the track edges, and fixes an off-by-one tick label that read '10.20.256.0' at the right edge. Also retitles the post.
This commit is contained in:
parent
c882c11eed
commit
87368d28d7
2 changed files with 200 additions and 13 deletions
|
|
@ -1,5 +1,5 @@
|
|||
+++
|
||||
title = "Subnetting IPv4: the bit-bending behind the slash"
|
||||
title = "Subnetting IPv4: what the slash actually means"
|
||||
date = 2026-06-05
|
||||
draft = true
|
||||
+++
|
||||
|
|
@ -34,6 +34,32 @@ The parent block above is a /22: four /24s, 1022 usable hosts. The slider picks
|
|||
|
||||
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.
|
||||
|
||||
## Going the other way: supernetting
|
||||
|
||||
Supernetting is the same operation as subnetting, run backwards. Take several small networks that happen to be adjacent and aligned, and describe them with a single shorter prefix. Routers do this constantly. Every routing table you have ever looked at is an exercise in pretending that thousands of /24s are actually a handful of /16s whenever the routers can get away with it, because the alternative is a router with a CPU on fire.
|
||||
|
||||
<div id="scene-supernet"></div>
|
||||
|
||||
The two /24 subnets live inside 10.20.0.0/16. The red bracket is the smallest single prefix that covers both. Slide A and B to consecutive even positions (4 and 5, 128 and 129, 200 and 201) and the bracket neatly becomes a /23 with no collateral. Slide them apart or onto misaligned positions (say 3 and 4, or 5 and 8) and the supernet has to expand to cover everything between, sweeping in a lot of addresses that aren't yours.
|
||||
|
||||
This is the thing that always bites people the first time they try to "summarize their networks." You cannot announce 10.20.3.0/24 and 10.20.4.0/24 as 10.20.3.0/23, because /23 requires alignment to a multiple of 2, and 3 isn't. The smallest honest supernet for those two is 10.20.0.0/21, which pulls in six neighbors you probably don't own. The rule in both directions (VLSM and supernetting) is the same: the line has to fall on a power-of-two boundary, or the math quietly decides for you.
|
||||
|
||||
The practical consequence is that address plans are designed, not discovered. If you intend to aggregate later, you allocate now in a way that lets the aggregation work: consecutive sites get consecutive, aligned blocks. Put one site in 10.1.0.0/20 and the next in 10.1.16.0/20, and later you can advertise 10.1.0.0/19 and every router on the internet silently thanks you.
|
||||
|
||||
## Reserved and special ranges
|
||||
|
||||
Not every address is fair game. The IANA has set aside a list of blocks for specific purposes, and a few are worth knowing by heart.
|
||||
|
||||
- **10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16** are [RFC 1918](https://datatracker.ietf.org/doc/html/rfc1918) private space. Route them inside your network, NAT them at the edge, never let them leak out. The internet at large will not route them back.
|
||||
- **127.0.0.0/8** is loopback. The whole /8 works, not just 127.0.0.1, but everyone pretends it's only the one address because the other 16.7 million feel wasteful and are.
|
||||
- **169.254.0.0/16** is [link-local](https://datatracker.ietf.org/doc/html/rfc3927). A host that fails to get a DHCP lease gives itself an address in this range and can at least talk to its neighbors on the same link. If you ever see a machine with a 169.254 address, its DHCP server is not well.
|
||||
- **224.0.0.0/4** is multicast, used for routing protocols (OSPF, PIM) and for any application that wants one-to-many delivery without sending N copies.
|
||||
- **255.255.255.255** is the limited broadcast address, which means "everyone on this link right now." DHCP clients use this before they know their own address.
|
||||
- **0.0.0.0** is "this host, this network" in some contexts and "any address" in others, depending on where you're listening. A service bound to 0.0.0.0 is listening on every interface the host has.
|
||||
- **100.64.0.0/10** is [RFC 6598](https://datatracker.ietf.org/doc/html/rfc6598) shared address space, introduced so that ISPs could do carrier-grade NAT without stepping on RFC 1918 space that their customers were probably already using.
|
||||
|
||||
There's a longer list, but these are the ones that come up in real troubleshooting. If a packet surprises you, the surprise usually involves one of these ranges showing up somewhere you didn't expect 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.
|
||||
|
|
|
|||
|
|
@ -517,8 +517,8 @@
|
|||
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 rowH = 22;
|
||||
const gapY = 22; // room for the title strip above A and B rows
|
||||
|
||||
const bitsA = ipBits(ipA);
|
||||
const bitsB = ipBits(ipB);
|
||||
|
|
@ -551,13 +551,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
|
|
@ -566,13 +572,13 @@
|
|||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(divX, rowsY); ctx.lineTo(divX, rowsY + 3 * (rowH + gapY) + 20);
|
||||
ctx.moveTo(divX, rowAY - 14); ctx.lineTo(divX, rowMY + rowH + 2);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
pillLabel(ctx, `/${prefix}`, divX, rowsY - 4, C.yellow, 'center', 'middle', 11);
|
||||
pillLabel(ctx, `/${prefix}`, divX, rowAY - 20, C.yellow, 'center', 'middle', 11);
|
||||
|
||||
// Decision diagram at the bottom
|
||||
const diagY = rowsY + 3 * (rowH + gapY) + 40;
|
||||
const diagY = rowMY + rowH + 24;
|
||||
const diagH = h - diagY - margin.b;
|
||||
if (diagH > 60) {
|
||||
const cx = w / 2;
|
||||
|
|
@ -639,11 +645,166 @@
|
|||
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 the inverse of VLSM: 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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue