Files
network-tools/src/network/vlsm.ts
T
zemion 429b55d587
Verify / verify (push) Canceled after 0s
Release Network Tools 0.2.0
2026-09-02 11:38:57 +02:00

148 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { formatIpv4, parseCidr } from "@add-ideas/toolbox-helpers";
const MAX_REQUIREMENTS = 1_024;
const MAX_NAME_LENGTH = 120;
export interface VlsmRequirement {
name: string;
hosts: number;
}
export interface VlsmAllocation extends VlsmRequirement {
requestedIndex: number;
prefixLength: number;
network: string;
firstUsable: string;
lastUsable: string;
broadcast: string;
usableHosts: number;
addresses: number;
}
export interface VlsmPlan {
base: string;
allocations: VlsmAllocation[];
usedAddresses: number;
freeAddresses: number;
warnings: string[];
}
function prefixForHosts(hosts: number): number {
if (!Number.isSafeInteger(hosts) || hosts < 1 || hosts > 4_294_967_294)
throw new Error(
"Host requirements must be integers from 1 to 4,294,967,294.",
);
const addressBits = Math.ceil(Math.log2(hosts + 2));
return 32 - Math.max(2, addressBits);
}
function align(value: bigint, block: bigint): bigint {
const remainder = value % block;
return remainder === 0n ? value : value + block - remainder;
}
export function parseVlsmRequirements(source: string): VlsmRequirement[] {
if (source.length > 256 * 1024)
throw new Error("Requirements exceed the 256 KiB limit.");
const requirements: VlsmRequirement[] = [];
for (const [index, raw] of source
.replaceAll("\r\n", "\n")
.split("\n")
.entries()) {
const line = raw.trim();
if (!line || line.startsWith("#")) continue;
if (requirements.length >= MAX_REQUIREMENTS)
throw new Error(
`More than ${MAX_REQUIREMENTS} subnet requirements were supplied.`,
);
const separator = line.lastIndexOf(",");
if (separator < 1)
throw new Error(`Line ${index + 1}: expected name,hosts.`);
const name = line.slice(0, separator).trim();
const hostsText = line.slice(separator + 1).trim();
if (!name || name.length > MAX_NAME_LENGTH)
throw new Error(
`Line ${index + 1}: name must contain 1${MAX_NAME_LENGTH} characters.`,
);
if (!/^\d+$/u.test(hostsText))
throw new Error(`Line ${index + 1}: hosts must be a positive integer.`);
const hosts = Number(hostsText);
prefixForHosts(hosts);
requirements.push({ name, hosts });
}
if (requirements.length === 0)
throw new Error("Enter at least one name,hosts requirement.");
return requirements;
}
export function planVlsm(
baseCidr: string,
requirements: readonly VlsmRequirement[],
): VlsmPlan {
if (requirements.length === 0 || requirements.length > MAX_REQUIREMENTS)
throw new Error(
`Supply between 1 and ${MAX_REQUIREMENTS} subnet requirements.`,
);
const base = parseCidr(baseCidr);
if (base.version !== 4)
throw new Error(
"The VLSM planner currently supports IPv4 base networks only.",
);
const sorted = requirements
.map((requirement, requestedIndex) => ({
...requirement,
requestedIndex,
prefixLength: prefixForHosts(requirement.hosts),
}))
.sort(
(left, right) =>
left.prefixLength - right.prefixLength ||
left.requestedIndex - right.requestedIndex,
);
let cursor = base.network.value;
const endExclusive = base.network.value + base.size;
const allocations: VlsmAllocation[] = [];
for (const requirement of sorted) {
if (requirement.prefixLength < base.prefixLength)
throw new Error(
`${requirement.name} needs /${requirement.prefixLength}, which is larger than ${base.canonical}.`,
);
const addresses = 2 ** (32 - requirement.prefixLength);
const block = BigInt(addresses);
const network = align(cursor, block);
const after = network + block;
if (after > endExclusive)
throw new Error(
`${requirement.name} does not fit. Allocation stopped before producing a partial plan.`,
);
allocations.push({
name: requirement.name,
hosts: requirement.hosts,
requestedIndex: requirement.requestedIndex,
prefixLength: requirement.prefixLength,
network: `${formatIpv4(network)}/${requirement.prefixLength}`,
firstUsable: formatIpv4(network + 1n),
lastUsable: formatIpv4(after - 2n),
broadcast: formatIpv4(after - 1n),
usableHosts: addresses - 2,
addresses,
});
cursor = after;
}
const used = allocations.reduce((total, item) => total + item.addresses, 0);
const baseSize = Number(base.size);
return {
base: base.canonical,
allocations,
usedAddresses: used,
freeAddresses: baseSize - used,
warnings: [
"Allocations are largest-first and use conventional network/broadcast reservations; point-to-point /31 semantics are not assumed.",
"Free-address count includes alignment gaps and is not itself a contiguous remainder guarantee.",
],
};
}