DEV Community

mi8bi
mi8bi

Posted on

Implementing VLSM (Variable Length Subnet Masking) subnet packing

What this is

Torinoa Tools includes a subnet calculator that splits a CIDR block into subnets. Beyond simple "split into N equal subnets," it also supports VLSM (Variable Length Subnet Masking).

VLSM handles the case where each subnet needs a different number of hosts — say, HQ needs 100 hosts, Branch A needs 50, Branch B needs 25, and a DMZ needs 10 — and you want to allocate address space without wasting it. Here's how the packing algorithm is implemented.

What actually needs computing

For each subnet in a VLSM plan, you need:

  1. The smallest prefix length (subnet mask) that can fit the requested host count
  2. A starting address inside the parent network that doesn't overlap any other subnet

Part 1 is a straightforward formula. Part 2 requires processing requirements largest-first and packing them into the address space in order.

Computing the required host bits

In IPv4, usable hosts per subnet = total addresses in the subnet minus 2 (network address and broadcast address). So you need the smallest n (host bits) such that 2^n - 2 >= requested hosts.

const hostBits = ceilLog2(hosts + 2);
const subnetPrefix = 32 - hostBits;
const subnetSize = Math.pow(2, hostBits);
Enter fullscreen mode Exit fullscreen mode

Passing hosts + 2 into ceilLog2 bakes in the "minus network and broadcast address" adjustment up front, rather than handling it separately later.

Pack largest requirements first (greedy)

The standard approach for VLSM address allocation is to place the largest subnets first. If you pack smallest-first, you end up with gaps when a later, larger subnet has to align to its own boundary — wasting address space that a different ordering wouldn't have wasted.

function calculateVlsm(networkNum: number, prefix: number, parentTotal: number) {
  const reqs = vlsmRequirements.value;

  // Sort descending for efficient packing
  const sorted = [...reqs].sort((a, b) => b - a);

  let currentNum = networkNum;
  const subnets: SubnetResult[] = [];

  for (const hosts of sorted) {
    const hostBits = ceilLog2(hosts + 2);
    const subnetPrefix = 32 - hostBits;
    const subnetSize = Math.pow(2, hostBits);

    // Align to subnet boundary
    const alignedNum = (Math.ceil(currentNum / subnetSize) * subnetSize) >>> 0;

    if (alignedNum + subnetSize - 1 > networkNum + parentTotal - 1) {
      // Doesn't fit in the parent network
      errors.value.value = t("errVlsmOverflow");
      results.value = [];
      return;
    }

    const subnetMask = maskFromPrefix(subnetPrefix);
    const broadcastNum = (alignedNum + subnetSize - 1) >>> 0;

    subnets.push({
      network: numToIp(alignedNum),
      mask: numToIp(subnetMask),
      broadcast: numToIp(broadcastNum),
      firstHost: numToIp(alignedNum + 1),
      lastHost: numToIp(broadcastNum - 1),
      usableHosts: subnetSize - 2,
      totalAddresses: subnetSize,
      prefix: subnetPrefix,
    });

    currentNum = alignedNum + subnetSize;
  }

  results.value = subnets;
}
Enter fullscreen mode Exit fullscreen mode

Aligning to a subnet boundary

CIDR subnets can only start at an address that's a multiple of their own size (a /23, 512 addresses, can only start on an address divisible by 512). This line finds "the next valid boundary at or after currentNum":

const alignedNum = (Math.ceil(currentNum / subnetSize) * subnetSize) >>> 0;
Enter fullscreen mode Exit fullscreen mode

Dividing currentNum (the address right after the previous subnet) by subnetSize, rounding up, then multiplying back by subnetSize gives the nearest aligned address that's >= currentNum. The >>> 0 coerces to an unsigned 32-bit integer — a common idiom when doing bitwise arithmetic on IPv4 addresses represented as numbers.

Overflowing the parent network is a hard error, not a silent truncation

if (alignedNum + subnetSize - 1 > networkNum + parentTotal - 1) {
  errors.value.value = t("errVlsmOverflow");
  results.value = [];
  return;
}
Enter fullscreen mode Exit fullscreen mode

Boundary alignment introduces gaps, so even if the sum of requested hosts is less than or equal to the parent network's total address count, alignment overhead can still make the plan not actually fit. Rather than silently dropping subnets or truncating, this returns an explicit error — so you find out right away whether your requirements genuinely fit, instead of getting a plan that's quietly wrong.

Wrap-up

VLSM itself isn't a hard algorithm, but getting three things right — sort descending, align to boundaries, and fail loudly on overflow — is enough for a correct, simple implementation. A small but genuinely useful tool if you've ever hand-calculated a subnet plan on paper.

Try it here: https://tools.torinoa.com/tools/subnet-calculator/

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

Really interesting approach! I'm curious if the packing algorithm prioritizes contiguous blocks or if it can