Skip to content

Subnet / CIDR Calculator — TypeScript source

Compute IPv4 subnet details from a CIDR block - network, broadcast, host range, netmask, wildcard, host count, IP class, and private-range detection. Split a block into smaller subnets, all in your browser.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// IPv4 subnet / CIDR math. Pure, deterministic, never throws - invalid input
// returns null (or [] for the splitter) so the UI can show a graceful error.

export type IpClass = 'A' | 'B' | 'C' | '-';

export interface SubnetInfo {
  network: string;
  broadcast: string;
  firstHost: string;
  lastHost: string;
  netmask: string;
  wildcard: string;
  hostCount: number;
  prefix: number;
  ipClass: IpClass;
  isPrivate: boolean;
}

/** Convert a dotted-quad IPv4 string to an unsigned 32-bit integer. null if invalid. */
export function ipToInt(ip: string): number | null {
  if (typeof ip !== 'string') return null;
  const parts = ip.split('.');
  if (parts.length !== 4) return null;
  let result = 0;
  for (const part of parts) {
    if (!/^\d+$/.test(part)) return null;
    const octet = Number(part);
    if (octet > 255) return null;
    result = result * 256 + octet;
  }
  return result >>> 0;
}

/** Convert an unsigned 32-bit integer to a dotted-quad IPv4 string. */
export function intToIp(n: number): string {
  const v = Math.floor(n) >>> 0;
  return [`${(v >>> 24) & 255}`, `${(v >>> 16) & 255}`, `${(v >>> 8) & 255}`, `${v & 255}`].join('.');
}

/** Parse "ip/prefix" into { ip, prefix } with prefix in 0-32. null if invalid. */
export function parseCidr(cidr: string): { ip: string; prefix: number } | null {
  if (typeof cidr !== 'string') return null;
  const slash = cidr.indexOf('/');
  if (slash === -1) return null;
  // Reject multiple slashes ("a.b.c.d/24/16").
  if (cidr.indexOf('/', slash + 1) !== -1) return null;
  const ipPart = cidr.slice(0, slash);
  const prefixPart = cidr.slice(slash + 1);
  if (!/^\d+$/.test(prefixPart)) return null;
  const prefix = Number(prefixPart);
  if (prefix > 32) return null;
  const ip = ipToInt(ipPart);
  if (ip === null) return null;
  return { ip: intToIp(ip), prefix };
}

/** Return the 32-bit netmask for a prefix length (0-32). */
function maskFromPrefix(prefix: number): number {
  return prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
}

/** Full details for a CIDR block. null if the CIDR is invalid. */
export function subnetDetails(cidr: string): SubnetInfo | null {
  const parsed = parseCidr(cidr);
  if (!parsed) return null;
  const { prefix } = parsed;

  const mask = maskFromPrefix(prefix);
  const wildcard = ~mask >>> 0;
  const network = (ipToInt(parsed.ip)! & mask) >>> 0;
  const broadcast = (network | wildcard) >>> 0;

  // /31 (point-to-point) and /32 (single host) have no host/broadcast split.
  const firstHost = prefix >= 31 ? network : (network + 1) >>> 0;
  const lastHost = prefix >= 31 ? broadcast : (broadcast - 1) >>> 0;

  let hostCount: number;
  if (prefix >= 32) hostCount = 1;
  else if (prefix === 31) hostCount = 2;
  else hostCount = 2 ** (32 - prefix) - 2;

  const firstOctet = (network >>> 24) & 255;
  const ipClass: IpClass =
    firstOctet < 128 ? 'A' : firstOctet < 192 ? 'B' : firstOctet < 224 ? 'C' : '-';

  const secondOctet = (network >>> 16) & 255;
  const isPrivate =
    firstOctet === 10 ||
    (firstOctet === 172 && secondOctet >= 16 && secondOctet <= 31) ||
    (firstOctet === 192 && secondOctet === 168);

  return {
    network: intToIp(network),
    broadcast: intToIp(broadcast),
    firstHost: intToIp(firstHost),
    lastHost: intToIp(lastHost),
    netmask: intToIp(mask),
    wildcard: intToIp(wildcard),
    hostCount,
    prefix,
    ipClass,
    isPrivate,
  };
}

/**
 * Split a CIDR block into smaller subnets of `newPrefix`.
 * Returns [] if the CIDR is invalid or newPrefix is outside (prefix, 32].
 * The base IP is normalized to the network address before splitting.
 */
export function splitSubnet(cidr: string, newPrefix: number): { network: string; prefix: number }[] {
  const parsed = parseCidr(cidr);
  if (!parsed) return [];
  if (!Number.isInteger(newPrefix) || newPrefix < parsed.prefix || newPrefix > 32) return [];

  const mask = maskFromPrefix(parsed.prefix);
  const network = (ipToInt(parsed.ip)! & mask) >>> 0;
  const count = 2 ** (newPrefix - parsed.prefix);
  const blockSize = 2 ** (32 - newPrefix);

  const out: { network: string; prefix: number }[] = [];
  for (let i = 0; i < count; i++) {
    out.push({ network: intToIp(network + i * blockSize), prefix: newPrefix });
  }
  return out;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →