DEV Community

SEN LLC
SEN LLC

Posted on

An IPv4 Subnet Calculator With Binary Visualization and /31 Handling

An IPv4 Subnet Calculator With Binary Visualization and /31 Handling

IP address math is bitwise arithmetic on 32-bit unsigned integers. ipToInt('192.168.1.100') gives you 0xC0A80164. Anding with a mask gives the network. Oring with the wildcard gives the broadcast. Subnet splitting is bit shifts on the CIDR. It's satisfying to implement because it's mostly one-line functions.

Subnet math is one of those things network engineers do in their heads and developers panic over. What's the broadcast address for 10.0.5.37/22? What's a /31 even used for? An interactive calculator makes the bitwise operations visible.

🔗 Live demo: https://sen.ltd/portfolio/ipv4-subnet/
📦 GitHub: https://github.com/sen-ltd/ipv4-subnet

Screenshot

Features:

  • IP + CIDR input parsing
  • Network, broadcast, first/last host, total/usable hosts
  • Subnet mask, wildcard mask, network class
  • RFC 1918 private address detection
  • Binary visualization (network vs host bits highlighted)
  • Subnet splitting (split /16 into 256 /24s etc.)
  • IP-in-range check
  • CIDR reference table
  • Japanese / English UI
  • Zero dependencies, 63 tests

IP as 32-bit unsigned int

The fundamental trick: store IPs as 32-bit integers, not strings:

export function ipToInt(ip) {
  const parts = ip.split('.').map(Number);
  return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
}

export function intToIp(n) {
  return [
    (n >>> 24) & 0xFF,
    (n >>> 16) & 0xFF,
    (n >>> 8) & 0xFF,
    n & 0xFF,
  ].join('.');
}
Enter fullscreen mode Exit fullscreen mode

The >>> 0 is critical — JavaScript's bitwise operations produce signed 32-bit integers, so a high bit like 192.x.x.x becomes negative. Unsigned right shift by 0 forces conversion to the unsigned 32-bit representation.

Subnet math

Once IPs are integers, the rest is trivial bitwise:

export function calculateSubnet(ip, cidr) {
  const mask = cidrToMask(cidr);
  const network = (ip & mask) >>> 0;
  const broadcast = (network | (~mask >>> 0)) >>> 0;

  // /31 and /32 are special
  let firstHost, lastHost, usableHosts;
  if (cidr >= 31) {
    firstHost = network;
    lastHost = broadcast;
    usableHosts = cidr === 32 ? 1 : 2; // RFC 3021 for /31
  } else {
    firstHost = (network + 1) >>> 0;
    lastHost = (broadcast - 1) >>> 0;
    usableHosts = Math.pow(2, 32 - cidr) - 2;
  }

  return { network, broadcast, firstHost, lastHost, usableHosts, mask };
}

function cidrToMask(cidr) {
  return cidr === 0 ? 0 : (0xFFFFFFFF << (32 - cidr)) >>> 0;
}
Enter fullscreen mode Exit fullscreen mode

Network = IP AND mask (zero out host bits). Broadcast = network OR inverted mask (set all host bits). First usable host = network + 1, last = broadcast - 1. Simple enough that the whole function fits in 20 lines.

/31 and /32: the special cases

Normal subnets lose 2 addresses to network and broadcast. But:

  • /32: A single host route. 1 address. No broadcast, no "usable hosts" concept. Used for loopback and host-specific routes.
  • /31: Point-to-point link per RFC 3021. Both addresses are usable — no network or broadcast. 2 usable hosts out of 2 total. Saves addresses on router-to-router links.

The function has to special-case these because the formula 2^(32-cidr) - 2 gives -1 and 0 respectively, which is wrong.

RFC 1918 private detection

Three address ranges are "private" (not routable on the public internet):

  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
export function isPrivate(ip) {
  return (
    isInSubnet(ip, ipToInt('10.0.0.0'), 8) ||
    isInSubnet(ip, ipToInt('172.16.0.0'), 12) ||
    isInSubnet(ip, ipToInt('192.168.0.0'), 16)
  );
}
Enter fullscreen mode Exit fullscreen mode

Subnet splitting

Given a /16, how do you split into /24s? You generate 2^(newCidr - oldCidr) subnets, each advancing by the new subnet size:

export function splitSubnet(networkIp, oldCidr, newCidr) {
  if (newCidr <= oldCidr) return [];
  const count = Math.pow(2, newCidr - oldCidr);
  const step = Math.pow(2, 32 - newCidr);
  const subnets = [];
  for (let i = 0; i < count; i++) {
    subnets.push({
      network: (networkIp + i * step) >>> 0,
      cidr: newCidr,
    });
  }
  return subnets;
}
Enter fullscreen mode Exit fullscreen mode

Splitting a /16 into /24s gives 256 subnets; into /25s gives 512; into /30s gives 16384. The UI caps display at 1024 for sanity.

Binary visualization

The most educational part: show the IP in binary with the mask line highlighted:

192.168.  1.100  (IP)
11000000.10101000.00000001.01100100  (binary)
NNNNNNNN.NNNNNNNN.NNNNNNNN.NNNN____  (/28)
Enter fullscreen mode Exit fullscreen mode

The N's are network bits, the _'s are host bits. Seeing it like this makes the & with the mask obvious — the network bits stay, the host bits become 0.

Series

This is entry #96 in my 100+ public portfolio series.

Top comments (0)