IP Subnet Calculator: Bitwise Operations and CIDR Implementation#

Setting up Docker networks recently, I needed to split several isolated subnets. Manual calculations got error-prone after a few tries, so I built a calculator to lock down the logic.

The Essence of Subnetting#

An IPv4 address is a 32-bit binary number. 192.168.1.1 is actually:

11000000.10101000.00000001.00000001

CIDR notation /24 means the first 24 bits are network bits, the last 8 are host bits. IPs with matching network bits belong to the same subnet.

Three core formulas:

  • Subnet mask = -1 << (32 - cidr) (high bits all 1, low bits all 0)
  • Network address = IP & mask (zero out host bits)
  • Broadcast address = network | ~mask (set all host bits to 1)

Bitwise Implementation#

IP to Long Integer#

Convert dotted decimal 192.168.1.1 to a number:

function ipToLong(ip: string): number {
  return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0
}

// Example: 192.168.1.1
// 192 << 24 + 168 << 16 + 1 << 8 + 1
// = 3232235777

The >>> 0 is crucial. JavaScript bitwise operations treat results as signed 32-bit integers. High IP addresses can become negative. Unsigned right shift by 0 forces conversion to unsigned.

Long Integer to IP#

Reverse conversion uses bit masks to extract each octet:

function longToIp(long: number): string {
  return [
    (long >>> 24) & 255,  // Highest 8 bits
    (long >>> 16) & 255,  // Next 8 bits
    (long >>> 8) & 255,   // Following 8 bits
    long & 255            // Lowest 8 bits
  ].join('.')
}

After each right shift, & 255 (binary 11111111) extracts the low 8 bits.

Core Subnet Calculation#

function calculateSubnet(ip: string, cidr: number) {
  const ipLong = ipToLong(ip)
  const mask = -1 << (32 - cidr)           // Subnet mask
  const networkLong = ipLong & mask        // Network address
  const broadcastLong = networkLong | ~mask // Broadcast address
  
  return {
    network: longToIp(networkLong),         // Network address
    broadcast: longToIp(broadcastLong),     // Broadcast address
    firstHost: longToIp(networkLong + 1),   // First usable host
    lastHost: longToIp(broadcastLong - 1),  // Last usable host
    totalHosts: Math.pow(2, 32 - cidr) - 2, // Usable hosts
    mask: longToIp(mask >>> 0)              // Subnet mask
  }
}

// Example: 192.168.1.100/24
// network: 192.168.1.0
// broadcast: 192.168.1.255
// firstHost: 192.168.1.1
// lastHost: 192.168.1.254
// totalHosts: 254
// mask: 255.255.255.0

-1 in binary is all 1s (two’s complement). Left-shifting preserves high bits, zeroes out low bits.

The Host Count Trap#

The formula is 2^(32-cidr) - 2. Why subtract 2?

  • Network address (all host bits = 0): Identifies the subnet itself, can’t be assigned to hosts
  • Broadcast address (all host bits = 1): Used for subnet broadcast, also not assignable

But there are two edge cases:

Only 2 IPs, minus 2 equals 0 hosts? RFC 3021 allows /31 for point-to-point links where both addresses can be used as host addresses.

/32 Subnets (Single Host)#

Only 1 IP, representing a single host address. Host count should be 1, not -1.

Complete implementation handles boundaries:

function getTotalHosts(cidr: number): number {
  if (cidr === 32) return 1
  if (cidr === 31) return 2
  return Math.pow(2, 32 - cidr) - 2
}

Real-World Applications#

1. Docker Network Segmentation#

# Create three isolated subnets
docker network create --subnet=172.18.0.0/24 frontend
docker network create --subnet=172.19.0.0/24 backend
docker network create --subnet=172.20.0.0/24 database

2. Check if Two IPs Are in the Same Subnet#

function sameSubnet(ip1: string, ip2: string, cidr: number): boolean {
  const mask = -1 << (32 - cidr)
  return (ipToLong(ip1) & mask) === (ipToLong(ip2) & mask)
}

// 192.168.1.100 and 192.168.1.200 in /24 subnet → true
// 192.168.1.100 and 192.168.2.100 in /24 subnet → false

3. Subnet Splitting#

Split one /24 into four /26s:

function splitSubnet(network: string, cidr: number, newCidr: number) {
  const networkLong = ipToLong(network)
  const count = Math.pow(2, newCidr - cidr)
  const step = Math.pow(2, 32 - newCidr)
  
  return Array.from({ length: count }, (_, i) => 
    longToIp(networkLong + i * step)
  )
}

// splitSubnet('192.168.1.0', 24, 26)
// → ['192.168.1.0', '192.168.1.64', '192.168.1.128', '192.168.1.192']

What About IPv6?#

IPv6 is 128 bits. Same principle, different notation:

  • 8 groups of hexadecimal separated by colons: 2001:db8::/32
  • Address space large enough for every grain of sand
  • /64 is the standard subnet prefix (64 network bits, 64 host bits)

JavaScript doesn’t natively support 128-bit integers. Use BigInt or segment processing.

Online Tool#

Based on this, I built an IP Subnet Calculator. Enter an IP and CIDR to get all the info automatically. No more manual bitwise math.


Related: Port Scanner | DNS Lookup