IP Subnet Calculator: Bitwise Operations and CIDR Implementation
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:
/31 Subnets (Point-to-Point Links)#
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