Give us a parent CIDR and the host counts you need. We'll allocate optimally sized subnets in order, with no wasted address space.
A VLSM allocator from scratch is about twenty lines: sort the host requirements largest-first, round each up to the smallest prefix that fits, then pack them contiguously starting at the parent network. The example allocates three subnets (120, 50, 25 hosts) inside 10.0.0.0/23.
import ipaddress, math
parent = ipaddress.IPv4Network("10.0.0.0/23")
demands = [120, 50, 25]
# Sort largest-first
demands.sort(reverse=True)
cursor = int(parent.network_address)
print(f"Allocations from {parent}:")
for need in demands:
# smallest prefix that fits "need" hosts (RFC standard: subtract 2)
prefix = 32 - math.ceil(math.log2(need + 2))
size = 1 << (32 - prefix)
# align cursor up to the prefix boundary
if cursor % size:
cursor += size - (cursor % size)
subnet = ipaddress.IPv4Network((cursor, prefix))
usable = size - 2
print(f" {need:>3} hosts -> {subnet.network_address}/{prefix:<2} ({usable} usable)")
cursor += sizepackage main
import (
"fmt"
"math"
"net"
"sort"
)
func main() {
_, parent, _ := net.ParseCIDR("10.0.0.0/23")
parentInt := uint32(parent.IP[0])<<24 | uint32(parent.IP[1])<<16 |
uint32(parent.IP[2])<<8 | uint32(parent.IP[3])
demands := []int{120, 50, 25}
sort.Sort(sort.Reverse(sort.IntSlice(demands)))
cursor := parentInt
fmt.Printf("Allocations from %s:\n", parent.String())
for _, need := range demands {
prefix := 32 - int(math.Ceil(math.Log2(float64(need+2))))
size := uint32(1) << (32 - prefix)
if cursor%size != 0 {
cursor += size - (cursor % size)
}
netIP := net.IPv4(byte(cursor>>24), byte(cursor>>16),
byte(cursor>>8), byte(cursor)).To4()
usable := int(size) - 2
fmt.Printf(" %3d hosts -> %s/%-2d (%d usable)\n",
need, netIP, prefix, usable)
cursor += size
}
}function ip2int(s) {
return s.split('.').reduce((a, o) => (a << 8) + +o, 0) >>> 0;
}
function int2ip(n) {
return [24, 16, 8, 0].map(s => (n >>> s) & 0xff).join('.');
}
const [parentIp, parentPrefix] = "10.0.0.0/23".split('/');
const demands = [120, 50, 25].sort((a, b) => b - a);
let cursor = ip2int(parentIp);
console.log(`Allocations from ${parentIp}/${parentPrefix}:`);
for (const need of demands) {
const prefix = 32 - Math.ceil(Math.log2(need + 2));
const size = 2 ** (32 - prefix);
if (cursor % size) cursor += size - (cursor % size);
const usable = size - 2;
console.log(` ${String(need).padStart(3)} hosts -> ${int2ip(cursor)}/${String(prefix).padEnd(2)} (${usable} usable)`);
cursor += size;
}#!/usr/bin/env bash
# Variable-length subnet allocation, largest-first, inside a parent CIDR.
PARENT="10.0.0.0/23"
DEMANDS=(120 50 25)
ip2int() { local IFS=.; local -a o=($1)
echo $(( (o[0]<<24)|(o[1]<<16)|(o[2]<<8)|o[3] )); }
int2ip() { printf "%d.%d.%d.%d" \
$(( ($1>>24)&0xff )) $(( ($1>>16)&0xff )) \
$(( ($1>>8)&0xff )) $(( $1&0xff )); }
# Smallest prefix that fits N hosts (RFC standard: subtract 2)
prefix_for() {
local need=$1 bits=1
while (( (1 << bits) - 2 < need )); do ((bits++)); done
echo $((32 - bits))
}
# Largest-first sort
IFS=$'\n' SORTED=($(sort -nr <<< "${DEMANDS[*]}" | tr ' ' '\n'))
unset IFS
cursor=$(ip2int "${PARENT%/*}")
echo "Allocations from $PARENT:"
for need in "${SORTED[@]}"; do
pfx=$(prefix_for "$need")
size=$(( 1 << (32 - pfx) ))
rem=$(( cursor % size ))
if (( rem != 0 )); then cursor=$(( cursor + size - rem )); fi
usable=$(( size - 2 ))
printf " %3d hosts -> %s/%-2d (%d usable)\n" \
"$need" "$(int2ip $cursor)" "$pfx" "$usable"
cursor=$(( cursor + size ))
doneimport java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class VlsmAlloc {
static String int2ip(long n) {
return String.format("%d.%d.%d.%d",
(n >> 24) & 0xff, (n >> 16) & 0xff,
(n >> 8) & 0xff, n & 0xff);
}
static long ip2int(String s) {
String[] p = s.split("\\.");
return ((Long.parseLong(p[0]) << 24)
| (Long.parseLong(p[1]) << 16)
| (Long.parseLong(p[2]) << 8)
| Long.parseLong(p[3])) & 0xFFFFFFFFL;
}
public static void main(String[] args) {
String parent = "10.0.0.0/23";
List<Integer> demands = new java.util.ArrayList<>(Arrays.asList(120, 50, 25));
demands.sort(Collections.reverseOrder());
long cursor = ip2int(parent.split("/")[0]);
System.out.printf("Allocations from %s:%n", parent);
for (int need : demands) {
int prefix = 32 - (int) Math.ceil(Math.log(need + 2) / Math.log(2));
long size = 1L << (32 - prefix);
if (cursor % size != 0) cursor += size - (cursor % size);
long usable = size - 2;
System.out.printf(" %3d hosts -> %s/%-2d (%d usable)%n",
need, int2ip(cursor), prefix, usable);
cursor += size;
}
}
}#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
static void int2ip(uint32_t n, char *out) {
sprintf(out, "%u.%u.%u.%u",
(n >> 24) & 0xff, (n >> 16) & 0xff,
(n >> 8) & 0xff, n & 0xff);
}
static uint32_t ip2int(const char *s) {
unsigned a, b, c, d;
sscanf(s, "%u.%u.%u.%u", &a, &b, &c, &d);
return (a << 24) | (b << 16) | (c << 8) | d;
}
static int cmp_desc(const void *a, const void *b) {
return *(const int*)b - *(const int*)a;
}
int main(void) {
const char *parent = "10.0.0.0/23";
int demands[] = {120, 50, 25};
int n = sizeof(demands) / sizeof(demands[0]);
qsort(demands, n, sizeof(int), cmp_desc);
uint32_t cursor = ip2int("10.0.0.0");
printf("Allocations from %s:\n", parent);
for (int i = 0; i < n; i++) {
int need = demands[i];
int prefix = 32 - (int)ceil(log2(need + 2));
uint32_t size = 1u << (32 - prefix);
if (cursor % size) cursor += size - (cursor % size);
char buf[20];
int2ip(cursor, buf);
printf(" %3d hosts -> %s/%-2d (%u usable)\n",
need, buf, prefix, size - 2);
cursor += size;
}
return 0;
}Allocations from 10.0.0.0/23: 120 hosts -> 10.0.0.0/25 (126 usable) 50 hosts -> 10.0.0.128/26 (62 usable) 25 hosts -> 10.0.0.192/27 (30 usable)
VLSM is the practice of using different subnet sizes within one parent CIDR. Instead of splitting a /22 into four equal /24s, you allocate a /24 for one tier, a /25 for the next, a /26 for the smallest — sized to actual need. This saves address space dramatically in heterogeneous networks.
Largest-first prevents fragmentation. If you allocate a small subnet at the start of the parent block, you may leave gaps that aren't big enough for the larger subnets you need later. Allocating largest-first guarantees alignment and minimizes wasted space.
Yes. Switch to AWS, Azure, GCP, or OCI mode and the planner adds the reserved-IP overhead to each subnet's host requirement, so the resulting plan still meets your actual host count after the cloud provider takes its IPs.
Yes — copy the plan as JSON, or send it to IaC Export to generate Terraform / CloudFormation / Pulumi modules for the whole VPC layout.